Squash commits for JB integration testing
This commit is contained in:
parent
d2d3c3065a
commit
b76af6a3da
|
@ -14,6 +14,7 @@ on:
|
|||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "extensions/intellij/**"
|
||||
- "extensions/vscode/**"
|
||||
- "core/**"
|
||||
- "gui/**"
|
||||
|
@ -293,3 +294,107 @@ jobs:
|
|||
run: |
|
||||
cd gui
|
||||
npm test
|
||||
|
||||
jetbrains-tests:
|
||||
needs: [install-root, core-checks]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: core/node_modules
|
||||
key: ${{ runner.os }}-core-node-modules-${{ hashFiles('core/package-lock.json') }}
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4.5.0
|
||||
with:
|
||||
distribution: zulu
|
||||
java-version: 17
|
||||
|
||||
- name: Setup FFmpeg
|
||||
uses: AnimMouse/setup-ffmpeg@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: Use Node.js from .nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: gui-cache
|
||||
with:
|
||||
path: gui/node_modules
|
||||
key: ${{ runner.os }}-gui-node-modules-${{ hashFiles('gui/package-lock.json') }}
|
||||
|
||||
- name: Build GUI
|
||||
run: |
|
||||
cd gui
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Run prepackage script
|
||||
run: |
|
||||
cd extensions/vscode
|
||||
npm ci
|
||||
npm run prepackage
|
||||
env:
|
||||
# https://github.com/microsoft/vscode-ripgrep/issues/9#issuecomment-643965333
|
||||
GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: binary-cache
|
||||
with:
|
||||
path: binary/node_modules
|
||||
key: ${{ runner.os }}-binary-node-modules-${{ hashFiles('binary/package-lock.json') }}
|
||||
|
||||
- name: Build the binaries
|
||||
run: |
|
||||
cd binary
|
||||
npm run build
|
||||
|
||||
- name: Start test IDE
|
||||
run: |
|
||||
cd extensions/intellij
|
||||
export DISPLAY=:99.0
|
||||
Xvfb -ac :99 -screen 0 1920x1080x24 &
|
||||
sleep 10
|
||||
mkdir -p build/reports
|
||||
./gradlew runIdeForUiTests &
|
||||
|
||||
- name: Wait for JB connection
|
||||
uses: jtalk/url-health-check-action@v3
|
||||
with:
|
||||
url: http://127.0.0.1:8082
|
||||
max-attempts: 15
|
||||
retry-delay: 30s
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd extensions/intellij
|
||||
export DISPLAY=:99.0
|
||||
./gradlew test
|
||||
|
||||
- name: Move video
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
cd extensions/intellij
|
||||
mv video build/reports
|
||||
|
||||
- name: Copy logs
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
cd extensions/intellij
|
||||
mv build/idea-sandbox/system/log/ build/reports
|
||||
|
||||
- name: Save fails report
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jb-failure-report
|
||||
path: |
|
||||
${{ github.workspace }}/extensions/intellij/build/reports
|
||||
|
|
|
@ -16,5 +16,10 @@
|
|||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://packages.jetbrains.team/maven/p/ij/intellij-dependencies" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
|
@ -34,7 +34,12 @@
|
|||
"extensions/vscode/tag-qry/**": true,
|
||||
"extensions/vscode/textmate-syntaxes/**": true,
|
||||
"extensions/vscode/tree-sitter/**": true,
|
||||
"gui/dist/**": true
|
||||
"gui/dist/**": true,
|
||||
"extensions/vscode/e2e/.test-extensions": true,
|
||||
"extensions/vscode/e2e/_output": true,
|
||||
"extensions/vscode/e2e/storage": true,
|
||||
"extensions/vscode/e2e/vsix": true,
|
||||
"extensions/.continue-debug": true
|
||||
// "sync/**": true
|
||||
},
|
||||
"eslint.workingDirectories": ["./core"]
|
||||
|
|
|
@ -118,6 +118,22 @@ async function installNodeModuleInTempDirAndCopyToCurrent(packageName, toCopy) {
|
|||
}
|
||||
|
||||
(async () => {
|
||||
// Informs of where to look for node_sqlite3.node https://www.npmjs.com/package/bindings#:~:text=The%20searching%20for,file%20is%20found
|
||||
// This is only needed for our `pkg` command
|
||||
fs.writeFileSync(
|
||||
"out/package.json",
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "binary",
|
||||
version: "1.0.0",
|
||||
author: "Continue Dev, Inc",
|
||||
license: "Apache-2.0",
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
console.log("[info] Downloading prebuilt lancedb...");
|
||||
for (const target of targets) {
|
||||
if (targetToLanceDb[target]) {
|
||||
|
@ -236,21 +252,6 @@ async function installNodeModuleInTempDirAndCopyToCurrent(packageName, toCopy) {
|
|||
execCmdSync(`curl -L -o ${targetDir}/build.tar.gz ${downloadUrl}`);
|
||||
execCmdSync(`cd ${targetDir} && tar -xvzf build.tar.gz`);
|
||||
|
||||
// Informs of where to look for node_sqlite3.node https://www.npmjs.com/package/bindings#:~:text=The%20searching%20for,file%20is%20found
|
||||
fs.writeFileSync(
|
||||
`${targetDir}/package.json`,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "binary",
|
||||
version: "1.0.0",
|
||||
author: "Continue Dev, Inc",
|
||||
license: "Apache-2.0",
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Copy to build directory for testing
|
||||
try {
|
||||
const [platform, arch] = target.split("-");
|
||||
|
@ -275,6 +276,11 @@ async function installNodeModuleInTempDirAndCopyToCurrent(packageName, toCopy) {
|
|||
);
|
||||
}
|
||||
|
||||
// Our dummy `package.json` is no longer needed so we can remove it.
|
||||
// If it isn't removed, then running locally via `node out/index.js` will fail
|
||||
// with a `Failed to locate bindings` error
|
||||
fs.unlinkSync("out/package.json");
|
||||
|
||||
const pathsToVerify = [];
|
||||
for (target of targets) {
|
||||
const exe = target.startsWith("win") ? ".exe" : "";
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"../../out/tree-sitter-wasms/*",
|
||||
"../../out/llamaTokenizer.mjs",
|
||||
"../../out/llamaTokenizerWorkerPool.mjs",
|
||||
"../../out/tiktokenWorkerPool.mjs"
|
||||
"../../out/tiktokenWorkerPool.mjs",
|
||||
"../../out/package.json"
|
||||
],
|
||||
"targets": [
|
||||
"node18-macos-arm64"
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"../../out/tree-sitter-wasms/*",
|
||||
"../../out/llamaTokenizer.mjs",
|
||||
"../../out/llamaTokenizerWorkerPool.mjs",
|
||||
"../../out/tiktokenWorkerPool.mjs"
|
||||
"../../out/tiktokenWorkerPool.mjs",
|
||||
"../../out/package.json"
|
||||
],
|
||||
"targets": [
|
||||
"node18-macos-x64"
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"../../out/tree-sitter-wasms/*",
|
||||
"../../out/llamaTokenizer.mjs",
|
||||
"../../out/llamaTokenizerWorkerPool.mjs",
|
||||
"../../out/tiktokenWorkerPool.mjs"
|
||||
"../../out/tiktokenWorkerPool.mjs",
|
||||
"../../out/package.json"
|
||||
],
|
||||
"targets": [
|
||||
"node18-linux-arm64"
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"../../out/tree-sitter-wasms/*",
|
||||
"../../out/llamaTokenizer.mjs",
|
||||
"../../out/llamaTokenizerWorkerPool.mjs",
|
||||
"../../out/tiktokenWorkerPool.mjs"
|
||||
"../../out/tiktokenWorkerPool.mjs",
|
||||
"../../out/package.json"
|
||||
],
|
||||
"targets": [
|
||||
"node18-linux-x64"
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"../../node_modules/win-ca/lib/roots.exe",
|
||||
"../../out/llamaTokenizer.mjs",
|
||||
"../../out/llamaTokenizerWorkerPool.mjs",
|
||||
"../../out/tiktokenWorkerPool.mjs"
|
||||
"../../out/tiktokenWorkerPool.mjs",
|
||||
"../../out/package.json"
|
||||
],
|
||||
"targets": [
|
||||
"node18-win-arm64"
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"../../node_modules/win-ca/lib/roots.exe",
|
||||
"../../out/llamaTokenizer.mjs",
|
||||
"../../out/llamaTokenizerWorkerPool.mjs",
|
||||
"../../out/tiktokenWorkerPool.mjs"
|
||||
"../../out/tiktokenWorkerPool.mjs",
|
||||
"../../out/package.json"
|
||||
],
|
||||
"targets": [
|
||||
"node18-win-x64"
|
||||
|
|
|
@ -20,11 +20,11 @@ program.action(async () => {
|
|||
let messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>;
|
||||
if (process.env.CONTINUE_DEVELOPMENT === "true") {
|
||||
messenger = new TcpMessenger<ToCoreProtocol, FromCoreProtocol>();
|
||||
console.log("Waiting for connection");
|
||||
console.log("[binary] Waiting for connection");
|
||||
await (
|
||||
messenger as TcpMessenger<ToCoreProtocol, FromCoreProtocol>
|
||||
).awaitConnection();
|
||||
console.log("Connected");
|
||||
console.log("[binary] Connected");
|
||||
} else {
|
||||
setupCoreLogging();
|
||||
// await setupCa();
|
||||
|
@ -32,10 +32,12 @@ program.action(async () => {
|
|||
}
|
||||
const ide = new IpcIde(messenger);
|
||||
const promptLogsPath = getPromptLogsPath();
|
||||
const core = new Core(messenger, ide, async (text) => {
|
||||
|
||||
new Core(messenger, ide, async (text) => {
|
||||
fs.appendFileSync(promptLogsPath, text + "\n\n");
|
||||
});
|
||||
console.log("Core started");
|
||||
|
||||
console.log("[binary] Core started");
|
||||
} catch (e) {
|
||||
fs.writeFileSync("./error.log", `${new Date().toISOString()} ${e}\n`);
|
||||
console.log("Error: ", e);
|
||||
|
|
|
@ -239,11 +239,6 @@ export class Core {
|
|||
void this.configHandler.reloadConfig();
|
||||
});
|
||||
|
||||
on("config/addOpenAiKey", (msg) => {
|
||||
addOpenAIKey(msg.data);
|
||||
void this.configHandler.reloadConfig();
|
||||
});
|
||||
|
||||
on("config/deleteModel", (msg) => {
|
||||
deleteModel(msg.data.title);
|
||||
void this.configHandler.reloadConfig();
|
||||
|
|
|
@ -11,16 +11,6 @@ import type {
|
|||
} from "../";
|
||||
|
||||
export type ToIdeFromWebviewProtocol = ToIdeFromWebviewOrCoreProtocol & {
|
||||
onLoad: [
|
||||
undefined,
|
||||
{
|
||||
windowId: string;
|
||||
serverUrl: string;
|
||||
workspacePaths: string[];
|
||||
vscMachineId: string;
|
||||
vscMediaUrl: string;
|
||||
},
|
||||
];
|
||||
openUrl: [string, void];
|
||||
// We pass the `curSelectedModel` because we currently cannot access the
|
||||
// default model title in the GUI from JB
|
||||
|
@ -42,8 +32,18 @@ export type ToIdeFromWebviewProtocol = ToIdeFromWebviewOrCoreProtocol & {
|
|||
toggleFullScreen: [{ newWindow?: boolean } | undefined, void];
|
||||
insertAtCursor: [{ text: string }, void];
|
||||
copyText: [{ text: string }, void];
|
||||
"jetbrains/editorInsetHeight": [{ height: number }, void];
|
||||
"jetbrains/isOSREnabled": [undefined, boolean];
|
||||
"jetbrains/onLoad": [
|
||||
undefined,
|
||||
{
|
||||
windowId: string;
|
||||
serverUrl: string;
|
||||
workspacePaths: string[];
|
||||
vscMachineId: string;
|
||||
vscMediaUrl: string;
|
||||
},
|
||||
];
|
||||
"jetbrains/getColors": [undefined, void];
|
||||
"vscode/openMoveRightMarkdown": [undefined, void];
|
||||
setGitHubAuthToken: [{ token: string }, void];
|
||||
acceptDiff: [{ filepath: string; streamId?: string }, void];
|
||||
|
|
|
@ -4,6 +4,8 @@ import {
|
|||
} from "./coreWebview.js";
|
||||
|
||||
// Message types to pass through from webview to core
|
||||
// Note: If updating these values, make a corresponding update in
|
||||
// extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/toolWindow/ContinueBrowser.kt
|
||||
export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
|
||||
[
|
||||
"ping",
|
||||
|
@ -13,13 +15,11 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
|
|||
"history/load",
|
||||
"history/save",
|
||||
"devdata/log",
|
||||
"config/addOpenAiKey",
|
||||
"config/addModel",
|
||||
"config/newPromptFile",
|
||||
"config/ideSettingsUpdate",
|
||||
"config/getSerializedProfileInfo",
|
||||
"config/deleteModel",
|
||||
"config/reload",
|
||||
"config/listProfiles",
|
||||
"config/openProfile",
|
||||
"context/getContextItems",
|
||||
|
|
|
@ -2,4 +2,5 @@ src/main/resources/webview
|
|||
!src/main/resources/webview/index.html
|
||||
src/main/resources/bin
|
||||
src/main/resources/config_schema.json
|
||||
src/main/resources/continue_rc_schema.json
|
||||
src/main/resources/continue_rc_schema.json
|
||||
video
|
|
@ -1,24 +0,0 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Tests" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<log_file alias="idea.log" path="$PROJECT_DIR$/extensions/intellij/build/idea-sandbox/system/log/idea.log" />
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/extensions/intellij" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="check" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -1,25 +0,0 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Verifications" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/extensions/intellij" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="runPluginVerifier" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -7,19 +7,26 @@ This file is for contribution guidelines specific to the JetBrains extension. Se
|
|||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [IDE Installation](#ide-installation)
|
||||
- [IDE configuration](#ide-configuration)
|
||||
- [Node.js Requirements](#nodejs-requirements)
|
||||
- [Install all dependencies](#install-all-dependencies)
|
||||
- [Misc](#misc)
|
||||
- [IDE Installation](#ide-installation)
|
||||
- [IDE configuration](#ide-configuration)
|
||||
- [Node.js Requirements](#nodejs-requirements)
|
||||
- [Install all dependencies](#install-all-dependencies)
|
||||
- [Misc](#misc)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Running the extension in debug mode](#running-the-extension-in-debug-mode)
|
||||
- [Accessing files in the `.continue` directory](#accessing-files-in-the-continue-directory)
|
||||
- [Reloading changes](#reloading-changes)
|
||||
- [Setting breakpoints](#setting-breakpoints)
|
||||
- [Available Gradle tasks](#available-gradle-tasks)
|
||||
- [Packaging](#packaging)
|
||||
- [Testing the packaged extension](#testing-the-packaged-extension)
|
||||
- [Running the extension in debug mode](#running-the-extension-in-debug-mode)
|
||||
- [Accessing files in the `.continue` directory](#accessing-files-in-the-continue-directory)
|
||||
- [Viewing logs](#viewing-logs)
|
||||
- [Reloading changes](#reloading-changes)
|
||||
- [Setting breakpoints](#setting-breakpoints)
|
||||
- [Available Gradle tasks](#available-gradle-tasks)
|
||||
- [Packaging](#packaging)
|
||||
- [Installing the packaged extension](#installing-the-packaged-extension)
|
||||
- [Testing](#testing)
|
||||
- [e2e testing](#e2e-testing)
|
||||
- [Overview](#overview)
|
||||
- [Setup](#setup)
|
||||
- [Running the tests](#running-the-tests)
|
||||
- [Identifying selectors](#identifying-selectors)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
|
@ -30,11 +37,14 @@ packaging it in a binary in the `binary` directory. Communication occurs over st
|
|||
|
||||
### IDE Installation
|
||||
|
||||
Continue is built with JDK version 17 (as specified in [`./build.gradle.kts`](./build.gradle.kts)), which can be downloaded from [Oracle](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html).
|
||||
Continue is built with JDK version 17 (as specified in [`./build.gradle.kts`](./build.gradle.kts)), which can be
|
||||
downloaded from [Oracle](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html).
|
||||
|
||||
We recommend using IntelliJ IDEA, which you can download from the [JetBrains website](https://www.jetbrains.com/idea/download).
|
||||
We recommend using IntelliJ IDEA, which you can download from
|
||||
the [JetBrains website](https://www.jetbrains.com/idea/download).
|
||||
|
||||
Both Ultimate and Community (free) editions are suitable for this project, although Ultimate has better debugging (see notes below).
|
||||
Both Ultimate and Community (free) editions are suitable for this project, although Ultimate has better debugging (see
|
||||
notes below).
|
||||
|
||||
### IDE configuration
|
||||
|
||||
|
@ -63,7 +73,10 @@ This project requires Node.js version 20.11.0 (LTS) or higher. You have two opti
|
|||
|
||||
Select the `Run Continue` task in the top right corner of the IDE and then select the "Debug" option.
|
||||
|
||||
> In community edition, use `Run Continue (CE)` instead, which uses shell scripts instead of Ultimate-only node configs. If you want to debug the core in CE, you'll need to quit the `Start Core Dev Server (CE)` process and run the core in a different environment that supports debugging, such as VS Code (Launch "Core Binary").
|
||||
> In community edition, use `Run Continue (CE)` instead, which uses shell scripts instead of Ultimate-only node configs.
|
||||
> If you want to debug the core in CE, you'll need to quit the `Start Core Dev Server (CE)` process and run the core in
|
||||
> a
|
||||
> different environment that supports debugging, such as VS Code (Launch "Core Binary").
|
||||
|
||||

|
||||
|
||||
|
@ -71,14 +84,22 @@ This should open a new instance on IntelliJ with the extension installed.
|
|||
|
||||
### Accessing files in the `.continue` directory
|
||||
|
||||
When running the `Start Core Dev Server` task, we set the location of your Continue directory to `./extensions/.continue-debug`. This is to
|
||||
When running the `Start Core Dev Server` task, we set the location of your Continue directory to
|
||||
`./extensions/.continue-debug`. This is to
|
||||
allow for changes to your `config.json` and other files during development, without affecting your actual configuration.
|
||||
|
||||
### Viewing logs
|
||||
|
||||
When using the `Run Continue` task, we automatically run a script that outputs logs into the "Prompt Logs" terminal tab.
|
||||
|
||||
Alternatively, you can view logs for a particular IDE instance by selecting `Help` -> `Open Log in Editor` in the window
|
||||
toolbar.
|
||||
|
||||
### Reloading changes
|
||||
|
||||
- `extensions/intellij`: Attempt to reload changed classes by selecting
|
||||
_Run | Debugging Actions | Reload Changed Classes`_
|
||||
- This will often fail on new imports, schema changes etc. In that case, you need to stop and restart the extension
|
||||
- This will often fail on new imports, schema changes etc. In that case, you need to stop and restart the extension
|
||||
- `gui`: Changes will be reloaded automatically
|
||||
- `core`: Run `npm run build` from the `binary` directory (requires restarting the `Start Core Dev Server` task)
|
||||
|
||||
|
@ -86,7 +107,7 @@ allow for changes to your `config.json` and other files during development, with
|
|||
|
||||
- `extensions/intellij`: Breakpoints can be set in Intellij
|
||||
- `gui`: You'll need to set explicit `debugger` statements in the source code, or through the browser dev tools
|
||||
- `core`: Breakpoints can be set in Intellij (requires restarting the `Start Core Dev Server` task)
|
||||
- `core`: Breakpoints can be set in Intellij (requires restarting the `Start Core Dev Server` task
|
||||
|
||||
### Available Gradle tasks
|
||||
|
||||
|
@ -114,8 +135,44 @@ verifyPluginConfiguration - Checks if Java and Kotlin compilers configuration me
|
|||
This will generate a .zip file in `./build/distributions` with the version defined in [
|
||||
`./gradle.properties`](./gradle.properties)
|
||||
|
||||
#### Testing the packaged extension
|
||||
#### Installing the packaged extension
|
||||
|
||||
- Navigate to the Plugins settings page (_Settings | Plugins_)
|
||||
- Click on the gear icon
|
||||
- Click _Install from disk_ and select the ZIP file in `./build/distributions`
|
||||
|
||||
## Testing
|
||||
|
||||
### e2e testing
|
||||
|
||||
#### Overview
|
||||
|
||||
The e2e tests are written using [intellij-ui-test-robot](`https://github.com/JetBrains/intellij-ui-test-robot`). The
|
||||
README for this project has a lot of helpful info on how to use the library.
|
||||
|
||||
#### Setup
|
||||
|
||||
If you are on macOS, you'll need to give IntelliJ permission to control your computer in order to run the e2e tests.
|
||||
Open _System Settings_ and select `Privacy & Security` -> `Accessibility` and toggle the switch for IntelliJ.
|
||||
|
||||
#### Running the tests
|
||||
|
||||
Instantiate the test IDE as a background task:
|
||||
|
||||
```sh
|
||||
./gradlew clean runIdeForUiTests &
|
||||
```
|
||||
|
||||
Once the IDE has loaded, you can run the tests. Note that you need to have the test IDE focused in order for the tests
|
||||
to run.
|
||||
|
||||
```sh
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
#### Identifying selectors
|
||||
|
||||
While the `runIdeForUiTests` task is runnung, you can visit the following URL
|
||||
to view the UI hierarchy of the running IDE:
|
||||
|
||||
http://127.0.0.1:8082/
|
|
@ -4,6 +4,13 @@ fun properties(key: String) = providers.gradleProperty(key)
|
|||
|
||||
fun environment(key: String) = providers.environmentVariable(key)
|
||||
|
||||
fun Sync.prepareSandbox() {
|
||||
from("../../binary/bin") { into("${intellij.pluginName.get()}/core/") }
|
||||
from("../vscode/node_modules/@vscode/ripgrep") { into("${intellij.pluginName.get()}/ripgrep/") }
|
||||
}
|
||||
|
||||
val remoteRobotVersion = "0.11.23"
|
||||
|
||||
plugins {
|
||||
id("java") // Java support
|
||||
alias(libs.plugins.kotlin) // Kotlin support
|
||||
|
@ -19,7 +26,10 @@ group = properties("pluginGroup").get()
|
|||
version = properties("pluginVersion").get()
|
||||
|
||||
// Configure project's dependencies
|
||||
repositories { mavenCentral() }
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = uri("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") }
|
||||
}
|
||||
|
||||
// Dependencies are managed with Gradle version catalog - read more:
|
||||
// https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog
|
||||
|
@ -29,18 +39,14 @@ dependencies {
|
|||
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
|
||||
}
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.32")
|
||||
implementation("io.ktor:ktor-server-core:2.3.7") {
|
||||
exclude(group = "org.slf4j", module = "slf4j-api")
|
||||
}
|
||||
implementation("io.ktor:ktor-server-netty:2.3.7") {
|
||||
exclude(group = "org.slf4j", module = "slf4j-api")
|
||||
}
|
||||
implementation("io.ktor:ktor-server-cors:2.3.7") {
|
||||
exclude(group = "org.slf4j", module = "slf4j-api")
|
||||
}
|
||||
implementation("com.posthog.java:posthog:1.+")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
// implementation("com.jetbrains.jsonSchema")
|
||||
testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion")
|
||||
testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2")
|
||||
testImplementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
implementation("com.automation-remarks:video-recorder-junit5:2.0")
|
||||
}
|
||||
|
||||
// Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for
|
||||
|
@ -80,9 +86,20 @@ qodana {
|
|||
koverReport { defaults { xml { onCheck = true } } }
|
||||
|
||||
tasks {
|
||||
downloadRobotServerPlugin {
|
||||
version.set(remoteRobotVersion)
|
||||
}
|
||||
|
||||
prepareSandbox {
|
||||
from("../../binary/bin") { into("${intellij.pluginName.get()}/core/") }
|
||||
from("../vscode/node_modules/@vscode/ripgrep") { into("${intellij.pluginName.get()}/ripgrep/") }
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
prepareTestingSandbox {
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
prepareUiTestingSandbox {
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
wrapper { gradleVersion = properties("gradleVersion").get() }
|
||||
|
@ -117,6 +134,19 @@ tasks {
|
|||
systemProperty("ide.mac.message.dialogs.as.sheets", "false")
|
||||
systemProperty("jb.privacy.policy.text", "<!--999.999-->")
|
||||
systemProperty("jb.consents.confirmation.enabled", "false")
|
||||
systemProperty("ide.mac.file.chooser.native", "false")
|
||||
systemProperty("jbScreenMenuBar.enabled", "false")
|
||||
systemProperty("apple.laf.useScreenMenuBar", "false")
|
||||
systemProperty("idea.trust.all.projects", "true")
|
||||
systemProperty("ide.show.tips.on.startup.default.value", "false")
|
||||
systemProperty("ide.browser.jcef.jsQueryPoolSize", "10000")
|
||||
|
||||
// This is to ensure we load the GUI with OSR enabled. We have logic that
|
||||
// renders with OSR disabled below a particular IDE version.
|
||||
// See ContinueExtensionSettingsService.kt for more info.
|
||||
intellij {
|
||||
version.set("2024.1")
|
||||
}
|
||||
}
|
||||
|
||||
signPlugin {
|
||||
|
@ -146,4 +176,8 @@ tasks {
|
|||
"${rootProject.projectDir.parentFile.parentFile}/manual-testing-sandbox/test.kt"
|
||||
).map { file(it).absolutePath }
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,7 @@ import com.intellij.openapi.components.ServiceManager
|
|||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.wm.ToolWindowManager
|
||||
import java.awt.Dimension
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.DocumentListener
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import java.awt.BorderLayout
|
||||
|
||||
fun getPluginService(project: Project?): ContinuePluginService? {
|
||||
if (project == null) {
|
||||
|
@ -37,7 +29,7 @@ class AcceptDiffAction : AnAction() {
|
|||
|
||||
private fun acceptHorizontalDiff(e: AnActionEvent) {
|
||||
val continuePluginService = getPluginService(e.project) ?: return
|
||||
continuePluginService.ideProtocolClient?.diffManager?.acceptDiff(null)
|
||||
continuePluginService?.diffManager?.acceptDiff(null)
|
||||
}
|
||||
|
||||
private fun acceptVerticalDiff(e: AnActionEvent) {
|
||||
|
@ -57,7 +49,7 @@ class RejectDiffAction : AnAction() {
|
|||
|
||||
private fun rejectHorizontalDiff(e: AnActionEvent) {
|
||||
val continuePluginService = getPluginService(e.project) ?: return
|
||||
continuePluginService.ideProtocolClient?.diffManager?.rejectDiff(null)
|
||||
continuePluginService.diffManager?.rejectDiff(null)
|
||||
}
|
||||
|
||||
private fun rejectVerticalDiff(e: AnActionEvent) {
|
||||
|
@ -87,7 +79,7 @@ fun getContinuePluginService(project: Project?): ContinuePluginService? {
|
|||
fun focusContinueInput(project: Project?) {
|
||||
val continuePluginService = getContinuePluginService(project) ?: return
|
||||
continuePluginService.continuePluginWindow?.content?.components?.get(0)?.requestFocus()
|
||||
continuePluginService.sendToWebview("focusContinueInputWithoutClear", null)
|
||||
continuePluginService.sendToWebview("focusContinueInputWithNewSession", null)
|
||||
|
||||
continuePluginService.ideProtocolClient?.sendHighlightedCode()
|
||||
}
|
||||
|
@ -104,7 +96,7 @@ class FocusContinueInputAction : AnAction() {
|
|||
val continuePluginService = getContinuePluginService(e.project) ?: return
|
||||
|
||||
continuePluginService.continuePluginWindow?.content?.components?.get(0)?.requestFocus()
|
||||
continuePluginService.sendToWebview("focusContinueInput", null)
|
||||
continuePluginService.sendToWebview("focusContinueInputWithNewSession", null)
|
||||
|
||||
continuePluginService.ideProtocolClient?.sendHighlightedCode()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.continuedev.continueintellijextension.activities
|
||||
|
||||
import IntelliJIDE
|
||||
import com.github.continuedev.continueintellijextension.auth.AuthListener
|
||||
import com.github.continuedev.continueintellijextension.auth.ContinueAuthService
|
||||
import com.github.continuedev.continueintellijextension.auth.ControlPlaneSessionInfo
|
||||
|
@ -133,6 +134,9 @@ class ContinuePluginStartupActivity : StartupActivity, DumbAware {
|
|||
project
|
||||
)
|
||||
|
||||
val diffManager = DiffManager(project)
|
||||
|
||||
continuePluginService.diffManager = diffManager
|
||||
continuePluginService.ideProtocolClient = ideProtocolClient
|
||||
|
||||
// Listen to changes to settings so the core can reload remote configuration
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package com.github.continuedev.continueintellijextension.autocomplete
|
||||
|
||||
import com.github.continuedev.continueintellijextension.`continue`.uuid
|
||||
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.google.gson.Gson
|
||||
import com.github.continuedev.continueintellijextension.utils.uuid
|
||||
import com.intellij.injected.editor.VirtualFileWindow
|
||||
import com.intellij.openapi.application.*
|
||||
import com.intellij.openapi.components.Service
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.intellij.openapi.vfs.AsyncFileListener
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
|
||||
|
||||
class AsyncFileSaveListener(private val continuePluginService: ContinuePluginService) : AsyncFileListener {
|
||||
private val configFilePatterns = listOf(
|
||||
".continue/config.json",
|
||||
".continue/config.ts",
|
||||
".continue/config.yaml",
|
||||
".continuerc.json"
|
||||
)
|
||||
|
||||
override fun prepareChange(events: MutableList<out VFileEvent>): AsyncFileListener.ChangeApplier? {
|
||||
val isConfigFile = events.any { event ->
|
||||
configFilePatterns.any { pattern ->
|
||||
event.path.endsWith(pattern) || event.path.endsWith(pattern.replace("/", "\\"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (isConfigFile) {
|
||||
object : AsyncFileListener.ChangeApplier {
|
||||
override fun afterVfsChange() {
|
||||
continuePluginService.coreMessenger?.request("config/reload", null, null) { _ -> }
|
||||
}
|
||||
}
|
||||
} else null
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import com.github.continuedev.continueintellijextension.constants.MessageTypes
|
|||
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.github.continuedev.continueintellijextension.services.TelemetryService
|
||||
import com.github.continuedev.continueintellijextension.utils.uuid
|
||||
import com.google.gson.Gson
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
|
@ -21,222 +22,223 @@ class CoreMessenger(
|
|||
private val ideProtocolClient: IdeProtocolClient,
|
||||
val coroutineScope: CoroutineScope
|
||||
) {
|
||||
private var writer: Writer? = null
|
||||
private var reader: BufferedReader? = null
|
||||
private var process: Process? = null
|
||||
private val gson = Gson()
|
||||
private val responseListeners = mutableMapOf<String, (Any?) -> Unit>()
|
||||
private val useTcp: Boolean = System.getenv("USE_TCP")?.toBoolean() ?: false
|
||||
private var writer: Writer? = null
|
||||
private var reader: BufferedReader? = null
|
||||
private var process: Process? = null
|
||||
private val gson = Gson()
|
||||
private val responseListeners = mutableMapOf<String, (Any?) -> Unit>()
|
||||
private val useTcp: Boolean = System.getenv("USE_TCP")?.toBoolean() ?: false
|
||||
|
||||
private fun write(message: String) {
|
||||
try {
|
||||
writer?.write(message + "\r\n")
|
||||
writer?.flush()
|
||||
} catch (e: Exception) {
|
||||
println("Error writing to Continue core: $e")
|
||||
}
|
||||
}
|
||||
|
||||
fun request(messageType: String, data: Any?, messageId: String?, onResponse: (Any?) -> Unit) {
|
||||
val id = messageId ?: uuid()
|
||||
val message =
|
||||
gson.toJson(mapOf("messageId" to id, "messageType" to messageType, "data" to data))
|
||||
responseListeners[id] = onResponse
|
||||
write(message)
|
||||
}
|
||||
|
||||
private fun handleMessage(json: String) {
|
||||
val responseMap = gson.fromJson(json, Map::class.java)
|
||||
val messageId = responseMap["messageId"].toString()
|
||||
val messageType = responseMap["messageType"].toString()
|
||||
val data = responseMap["data"]
|
||||
|
||||
// IDE listeners
|
||||
if (MessageTypes.ideMessageTypes.contains(messageType)) {
|
||||
ideProtocolClient.handleMessage(json) { data ->
|
||||
val message =
|
||||
gson.toJson(
|
||||
mapOf("messageId" to messageId, "messageType" to messageType, "data" to data))
|
||||
write(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to webview
|
||||
if (MessageTypes.PASS_THROUGH_TO_WEBVIEW.contains(messageType)) {
|
||||
// TODO: Currently we aren't set up to receive a response back from the webview
|
||||
// Can circumvent for getDefaultsModelTitle here for now
|
||||
if (messageType == "getDefaultModelTitle") {
|
||||
val continueSettingsService = service<ContinueExtensionSettings>()
|
||||
val defaultModelTitle = continueSettingsService.continueState.lastSelectedInlineEditModel
|
||||
val message =
|
||||
gson.toJson(
|
||||
mapOf(
|
||||
"messageId" to messageId,
|
||||
"messageType" to messageType,
|
||||
"data" to defaultModelTitle))
|
||||
write(message)
|
||||
}
|
||||
val continuePluginService = project.service<ContinuePluginService>()
|
||||
continuePluginService.sendToWebview(messageType, responseMap["data"], messageType)
|
||||
}
|
||||
|
||||
// Responses for messageId
|
||||
responseListeners[messageId]?.let { listener ->
|
||||
listener(data)
|
||||
if (MessageTypes.generatorTypes.contains(messageType)) {
|
||||
val done = (data as Map<String, Boolean?>)["done"]
|
||||
if (done == true) {
|
||||
responseListeners.remove(messageId)
|
||||
} else {}
|
||||
} else {
|
||||
responseListeners.remove(messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPermissions(destination: String) {
|
||||
val osName = System.getProperty("os.name").toLowerCase()
|
||||
if (osName.contains("mac") || osName.contains("darwin")) {
|
||||
ProcessBuilder("xattr", "-dr", "com.apple.quarantine", destination).start()
|
||||
setFilePermissions(destination, "rwxr-xr-x")
|
||||
} else if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac")) {
|
||||
setFilePermissions(destination, "rwxr-xr-x")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setFilePermissions(path: String, posixPermissions: String) {
|
||||
val perms = HashSet<PosixFilePermission>()
|
||||
if (posixPermissions.contains("r")) perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (posixPermissions.contains("w")) perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (posixPermissions.contains("x")) perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
Files.setPosixFilePermissions(Paths.get(path), perms)
|
||||
}
|
||||
|
||||
private val exitCallbacks: MutableList<() -> Unit> = mutableListOf()
|
||||
|
||||
fun onDidExit(callback: () -> Unit) {
|
||||
exitCallbacks.add(callback)
|
||||
}
|
||||
|
||||
init {
|
||||
if (useTcp) {
|
||||
try {
|
||||
val socket = Socket("127.0.0.1", 3000)
|
||||
val writer = PrintWriter(socket.getOutputStream(), true)
|
||||
this.writer = writer
|
||||
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||
this.reader = reader
|
||||
|
||||
Thread {
|
||||
try {
|
||||
while (true) {
|
||||
val line = reader.readLine()
|
||||
if (line != null && line.isNotEmpty()) {
|
||||
try {
|
||||
handleMessage(line)
|
||||
} catch (e: Exception) {
|
||||
println("Error handling message: $line")
|
||||
println(e)
|
||||
}
|
||||
} else {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
try {
|
||||
reader.close()
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
.start()
|
||||
} catch (e: Exception) {
|
||||
println("TCP Connection Error: Unable to connect to 127.0.0.1:3000")
|
||||
println("Reason: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
// Set proper permissions
|
||||
coroutineScope.launch(Dispatchers.IO) { setPermissions(continueCorePath) }
|
||||
|
||||
// Start the subprocess
|
||||
val processBuilder =
|
||||
ProcessBuilder(continueCorePath).directory(File(continueCorePath).parentFile)
|
||||
process = processBuilder.start()
|
||||
|
||||
val outputStream = process!!.outputStream
|
||||
val inputStream = process!!.inputStream
|
||||
|
||||
writer = OutputStreamWriter(outputStream, StandardCharsets.UTF_8)
|
||||
reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
||||
|
||||
process!!.onExit().thenRun {
|
||||
exitCallbacks.forEach { it() }
|
||||
var err = process?.errorStream?.bufferedReader()?.readText()?.trim()
|
||||
if (err != null) {
|
||||
// There are often "⚡️Done in Xms" messages, and we want everything after the last one
|
||||
val delimiter = "⚡ Done in"
|
||||
val doneIndex = err.lastIndexOf(delimiter)
|
||||
if (doneIndex != -1) {
|
||||
err = err.substring(doneIndex + delimiter.length)
|
||||
}
|
||||
}
|
||||
|
||||
println("Core process exited with output: $err")
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
ideProtocolClient.showToast("error", "Core process exited with output: $err")
|
||||
}
|
||||
|
||||
// Log the cause of the failure
|
||||
val telemetryService = service<TelemetryService>()
|
||||
telemetryService.capture("jetbrains_core_exit", mapOf("error" to err))
|
||||
|
||||
// Clean up all resources
|
||||
writer?.close()
|
||||
reader?.close()
|
||||
process?.destroy()
|
||||
}
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
private fun write(message: String) {
|
||||
try {
|
||||
while (true) {
|
||||
val line = reader?.readLine()
|
||||
if (line != null && line.isNotEmpty()) {
|
||||
try {
|
||||
handleMessage(line)
|
||||
} catch (e: Exception) {
|
||||
println("Error handling message: $line")
|
||||
println(e)
|
||||
}
|
||||
} else {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
try {
|
||||
reader?.close()
|
||||
writer?.close()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
process?.destroy()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
writer?.write(message + "\r\n")
|
||||
writer?.flush()
|
||||
} catch (e: Exception) {
|
||||
println("Error writing to Continue core: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun killSubProcess() {
|
||||
process?.isAlive?.let {
|
||||
exitCallbacks.clear()
|
||||
process?.destroy()
|
||||
fun request(messageType: String, data: Any?, messageId: String?, onResponse: (Any?) -> Unit) {
|
||||
val id = messageId ?: uuid()
|
||||
val message =
|
||||
gson.toJson(mapOf("messageId" to id, "messageType" to messageType, "data" to data))
|
||||
responseListeners[id] = onResponse
|
||||
write(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessage(json: String) {
|
||||
val responseMap = gson.fromJson(json, Map::class.java)
|
||||
val messageId = responseMap["messageId"].toString()
|
||||
val messageType = responseMap["messageType"].toString()
|
||||
val data = responseMap["data"]
|
||||
|
||||
// IDE listeners
|
||||
if (MessageTypes.ideMessageTypes.contains(messageType)) {
|
||||
ideProtocolClient.handleMessage(json) { data ->
|
||||
val message =
|
||||
gson.toJson(
|
||||
mapOf("messageId" to messageId, "messageType" to messageType, "data" to data)
|
||||
)
|
||||
write(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to webview
|
||||
if (MessageTypes.PASS_THROUGH_TO_WEBVIEW.contains(messageType)) {
|
||||
// TODO: Currently we aren't set up to receive a response back from the webview
|
||||
// Can circumvent for getDefaultsModelTitle here for now
|
||||
if (messageType == "getDefaultModelTitle") {
|
||||
val continueSettingsService = service<ContinueExtensionSettings>()
|
||||
val defaultModelTitle = continueSettingsService.continueState.lastSelectedInlineEditModel
|
||||
val message =
|
||||
gson.toJson(
|
||||
mapOf(
|
||||
"messageId" to messageId,
|
||||
"messageType" to messageType,
|
||||
"data" to defaultModelTitle
|
||||
)
|
||||
)
|
||||
write(message)
|
||||
}
|
||||
val continuePluginService = project.service<ContinuePluginService>()
|
||||
continuePluginService.sendToWebview(messageType, responseMap["data"], messageType)
|
||||
}
|
||||
|
||||
// Responses for messageId
|
||||
responseListeners[messageId]?.let { listener ->
|
||||
listener(data)
|
||||
if (MessageTypes.generatorTypes.contains(messageType)) {
|
||||
val done = (data as Map<String, Boolean?>)["done"]
|
||||
if (done == true) {
|
||||
responseListeners.remove(messageId)
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
responseListeners.remove(messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPermissions(destination: String) {
|
||||
val osName = System.getProperty("os.name").toLowerCase()
|
||||
if (osName.contains("mac") || osName.contains("darwin")) {
|
||||
ProcessBuilder("xattr", "-dr", "com.apple.quarantine", destination).start()
|
||||
setFilePermissions(destination, "rwxr-xr-x")
|
||||
} else if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac")) {
|
||||
setFilePermissions(destination, "rwxr-xr-x")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setFilePermissions(path: String, posixPermissions: String) {
|
||||
val perms = HashSet<PosixFilePermission>()
|
||||
if (posixPermissions.contains("r")) perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (posixPermissions.contains("w")) perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (posixPermissions.contains("x")) perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
Files.setPosixFilePermissions(Paths.get(path), perms)
|
||||
}
|
||||
|
||||
private val exitCallbacks: MutableList<() -> Unit> = mutableListOf()
|
||||
|
||||
fun onDidExit(callback: () -> Unit) {
|
||||
exitCallbacks.add(callback)
|
||||
}
|
||||
|
||||
init {
|
||||
if (useTcp) {
|
||||
try {
|
||||
val socket = Socket("127.0.0.1", 3000)
|
||||
val writer = PrintWriter(socket.getOutputStream(), true)
|
||||
this.writer = writer
|
||||
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
||||
this.reader = reader
|
||||
|
||||
Thread {
|
||||
try {
|
||||
while (true) {
|
||||
val line = reader.readLine()
|
||||
if (line != null && line.isNotEmpty()) {
|
||||
try {
|
||||
handleMessage(line)
|
||||
} catch (e: Exception) {
|
||||
println("Error handling message: $line")
|
||||
println(e)
|
||||
}
|
||||
} else {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
try {
|
||||
reader.close()
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
.start()
|
||||
} catch (e: Exception) {
|
||||
println("TCP Connection Error: Unable to connect to 127.0.0.1:3000")
|
||||
println("Reason: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
// Set proper permissions
|
||||
coroutineScope.launch(Dispatchers.IO) { setPermissions(continueCorePath) }
|
||||
|
||||
// Start the subprocess
|
||||
val processBuilder =
|
||||
ProcessBuilder(continueCorePath).directory(File(continueCorePath).parentFile)
|
||||
process = processBuilder.start()
|
||||
|
||||
val outputStream = process!!.outputStream
|
||||
val inputStream = process!!.inputStream
|
||||
|
||||
writer = OutputStreamWriter(outputStream, StandardCharsets.UTF_8)
|
||||
reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
||||
|
||||
process!!.onExit().thenRun {
|
||||
exitCallbacks.forEach { it() }
|
||||
var err = process?.errorStream?.bufferedReader()?.readText()?.trim()
|
||||
if (err != null) {
|
||||
// There are often "⚡️Done in Xms" messages, and we want everything after the last one
|
||||
val delimiter = "⚡ Done in"
|
||||
val doneIndex = err.lastIndexOf(delimiter)
|
||||
if (doneIndex != -1) {
|
||||
err = err.substring(doneIndex + delimiter.length)
|
||||
}
|
||||
}
|
||||
|
||||
println("Core process exited with output: $err")
|
||||
|
||||
// Log the cause of the failure
|
||||
val telemetryService = service<TelemetryService>()
|
||||
telemetryService.capture("jetbrains_core_exit", mapOf("error" to err))
|
||||
|
||||
// Clean up all resources
|
||||
writer?.close()
|
||||
reader?.close()
|
||||
process?.destroy()
|
||||
}
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
while (true) {
|
||||
val line = reader?.readLine()
|
||||
if (line != null && line.isNotEmpty()) {
|
||||
try {
|
||||
handleMessage(line)
|
||||
} catch (e: Exception) {
|
||||
println("Error handling message: $line")
|
||||
println(e)
|
||||
}
|
||||
} else {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
try {
|
||||
reader?.close()
|
||||
writer?.close()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
process?.destroy()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun killSubProcess() {
|
||||
process?.isAlive?.let {
|
||||
exitCallbacks.clear()
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.github.continuedev.continueintellijextension.services.TelemetryService
|
||||
import com.github.continuedev.continueintellijextension.utils.getMachineUniqueID
|
||||
import com.intellij.ide.plugins.PluginManager
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.project.Project
|
||||
|
@ -16,67 +15,65 @@ class CoreMessengerManager(
|
|||
private val coroutineScope: CoroutineScope
|
||||
) {
|
||||
|
||||
var coreMessenger: CoreMessenger? = null
|
||||
var lastBackoffInterval = 0.5
|
||||
var coreMessenger: CoreMessenger? = null
|
||||
private var lastBackoffInterval = 0.5
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
val continuePluginService =
|
||||
ServiceManager.getService(project, ContinuePluginService::class.java)
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
val myPluginId = "com.github.continuedev.continueintellijextension"
|
||||
val pluginDescriptor =
|
||||
PluginManager.getPlugin(PluginId.getId(myPluginId)) ?: throw Exception("Plugin not found")
|
||||
|
||||
val myPluginId = "com.github.continuedev.continueintellijextension"
|
||||
val pluginDescriptor =
|
||||
PluginManager.getPlugin(PluginId.getId(myPluginId)) ?: throw Exception("Plugin not found")
|
||||
val pluginPath = pluginDescriptor.pluginPath
|
||||
val osName = System.getProperty("os.name").toLowerCase()
|
||||
val os =
|
||||
when {
|
||||
osName.contains("mac") || osName.contains("darwin") -> "darwin"
|
||||
osName.contains("win") -> "win32"
|
||||
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> "linux"
|
||||
else -> "linux"
|
||||
}
|
||||
val osArch = System.getProperty("os.arch").toLowerCase()
|
||||
val arch =
|
||||
when {
|
||||
osArch.contains("aarch64") || (osArch.contains("arm") && osArch.contains("64")) ->
|
||||
"arm64"
|
||||
|
||||
val pluginPath = pluginDescriptor.pluginPath
|
||||
val osName = System.getProperty("os.name").toLowerCase()
|
||||
val os =
|
||||
when {
|
||||
osName.contains("mac") || osName.contains("darwin") -> "darwin"
|
||||
osName.contains("win") -> "win32"
|
||||
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> "linux"
|
||||
else -> "linux"
|
||||
}
|
||||
val osArch = System.getProperty("os.arch").toLowerCase()
|
||||
val arch =
|
||||
when {
|
||||
osArch.contains("aarch64") || (osArch.contains("arm") && osArch.contains("64")) ->
|
||||
"arm64"
|
||||
osArch.contains("amd64") || osArch.contains("x86_64") -> "x64"
|
||||
else -> "x64"
|
||||
}
|
||||
val target = "$os-$arch"
|
||||
osArch.contains("amd64") || osArch.contains("x86_64") -> "x64"
|
||||
else -> "x64"
|
||||
}
|
||||
val target = "$os-$arch"
|
||||
|
||||
println("Identified OS: $os, Arch: $arch")
|
||||
println("Identified OS: $os, Arch: $arch")
|
||||
|
||||
val corePath = Paths.get(pluginPath.toString(), "core").toString()
|
||||
val targetPath = Paths.get(corePath, target).toString()
|
||||
val continueCorePath =
|
||||
Paths.get(targetPath, "continue-binary" + (if (os == "win32") ".exe" else "")).toString()
|
||||
val corePath = Paths.get(pluginPath.toString(), "core").toString()
|
||||
val targetPath = Paths.get(corePath, target).toString()
|
||||
val continueCorePath =
|
||||
Paths.get(targetPath, "continue-binary" + (if (os == "win32") ".exe" else "")).toString()
|
||||
|
||||
setupCoreMessenger(continueCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCoreMessenger(continueCorePath: String): Unit {
|
||||
coreMessenger = CoreMessenger(project, continueCorePath, ideProtocolClient, coroutineScope)
|
||||
|
||||
coreMessenger?.request("config/getSerializedProfileInfo", null, null) { resp ->
|
||||
val data = resp as? Map<String, Any>
|
||||
val profileInfo = data?.get("config") as? Map<String, Any>
|
||||
val allowAnonymousTelemetry = profileInfo?.get("allowAnonymousTelemetry") as? Boolean
|
||||
val telemetryService = service<TelemetryService>()
|
||||
if (allowAnonymousTelemetry == true || allowAnonymousTelemetry == null) {
|
||||
telemetryService.setup(getMachineUniqueID())
|
||||
}
|
||||
setupCoreMessenger(continueCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// On exit, use exponential backoff to create another CoreMessenger
|
||||
coreMessenger?.onDidExit {
|
||||
lastBackoffInterval *= 2
|
||||
println("CoreMessenger exited, retrying in $lastBackoffInterval seconds")
|
||||
Thread.sleep((lastBackoffInterval * 1000).toLong())
|
||||
setupCoreMessenger(continueCorePath)
|
||||
private fun setupCoreMessenger(continueCorePath: String) {
|
||||
coreMessenger = CoreMessenger(project, continueCorePath, ideProtocolClient, coroutineScope)
|
||||
|
||||
coreMessenger?.request("config/getSerializedProfileInfo", null, null) { resp ->
|
||||
val data = resp as? Map<String, Any>
|
||||
val profileInfo = data?.get("config") as? Map<String, Any>
|
||||
val allowAnonymousTelemetry = profileInfo?.get("allowAnonymousTelemetry") as? Boolean
|
||||
val telemetryService = service<TelemetryService>()
|
||||
if (allowAnonymousTelemetry == true || allowAnonymousTelemetry == null) {
|
||||
telemetryService.setup(getMachineUniqueID())
|
||||
}
|
||||
}
|
||||
|
||||
// On exit, use exponential backoff to create another CoreMessenger
|
||||
coreMessenger?.onDidExit {
|
||||
lastBackoffInterval *= 2
|
||||
println("CoreMessenger exited, retrying in $lastBackoffInterval seconds")
|
||||
Thread.sleep((lastBackoffInterval * 1000).toLong())
|
||||
setupCoreMessenger(continueCorePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +0,0 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.intellij.codeInsight.hints.presentation.BasePresentation
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.markup.TextAttributes
|
||||
import java.awt.*
|
||||
|
||||
internal class InlineBoxPresentation(private val editor: Editor) : BasePresentation() {
|
||||
override val height: Int = 50
|
||||
override val width: Int = 200
|
||||
|
||||
override fun paint(g: Graphics2D, attributes: TextAttributes) {
|
||||
val color = attributes.foregroundColor
|
||||
val text = "Hello World!"
|
||||
g.color = color
|
||||
g.drawString(text, 0, 0)
|
||||
}
|
||||
|
||||
override fun toString(): String = "InlineBoxPresentation"
|
||||
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.intellij.codeInsight.codeVision.*
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.util.TextRange
|
||||
|
||||
class InputBoxCodeVision : CodeVisionProvider<Unit> {
|
||||
companion object {
|
||||
const val id: String = "continue.code.vision"
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "Hello World!"
|
||||
override val relativeOrderings: List<CodeVisionRelativeOrdering>
|
||||
get() = emptyList()
|
||||
|
||||
override val defaultAnchor: CodeVisionAnchorKind
|
||||
get() = CodeVisionAnchorKind.Default
|
||||
override val id: String
|
||||
get() = Companion.id
|
||||
|
||||
override fun precomputeOnUiThread(editor: Editor) {}
|
||||
|
||||
override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState {
|
||||
val lenses: List<Pair<TextRange, CodeVisionEntry>> = emptyList()
|
||||
// val range = TextRange(0, 1)
|
||||
// lenses.add(range to )
|
||||
return CodeVisionState.Ready(lenses)
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`
|
||||
|
||||
import com.intellij.codeInsight.hints.*
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JTextArea
|
||||
|
||||
class InputBoxInlayProvider : InlayHintsProvider<NoSettings> {
|
||||
override val key: SettingsKey<NoSettings>
|
||||
get() = SettingsKey<NoSettings>("InputBoxInlayProviderSettingsKey")
|
||||
override val name: String
|
||||
get() = "Continue Quick Input"
|
||||
|
||||
override val previewText: String?
|
||||
get() = "Continue Quick Input"
|
||||
|
||||
override fun createSettings() = NoSettings()
|
||||
|
||||
override fun getCollectorFor(
|
||||
file: PsiFile,
|
||||
editor: Editor,
|
||||
settings: NoSettings,
|
||||
sink: InlayHintsSink
|
||||
): InlayHintsCollector? {
|
||||
return Collector(editor)
|
||||
}
|
||||
|
||||
private class Collector(editor: Editor) : FactoryInlayHintsCollector(editor) {
|
||||
override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
|
||||
if (element.text == "continue") {
|
||||
// val presentation = HorizontalBarPresentation.create(factory, editor, element)
|
||||
// sink.addInlineElement(element.textOffset, true, presentation)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun createConfigurable(settings: NoSettings): ImmediateConfigurable {
|
||||
return object : ImmediateConfigurable {
|
||||
override fun createComponent(listener: ChangeListener): JComponent {
|
||||
return JTextArea("Hello World!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,589 @@
|
|||
import com.github.continuedev.continueintellijextension.*
|
||||
import com.github.continuedev.continueintellijextension.constants.getContinueGlobalPath
|
||||
import com.github.continuedev.continueintellijextension.`continue`.DiffManager
|
||||
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.github.continuedev.continueintellijextension.utils.OS
|
||||
import com.github.continuedev.continueintellijextension.utils.getMachineUniqueID
|
||||
import com.github.continuedev.continueintellijextension.utils.getOS
|
||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.execution.util.ExecUtil
|
||||
import com.intellij.ide.plugins.PluginManager
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.lang.annotation.HighlightSeverity
|
||||
import com.intellij.notification.NotificationAction
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.impl.DocumentMarkupModel
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.testFramework.LightVirtualFile
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.Desktop
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.URI
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.file.Paths
|
||||
|
||||
class IntelliJIDE(
|
||||
private val project: Project,
|
||||
private val workspacePath: String?,
|
||||
private val continuePluginService: ContinuePluginService,
|
||||
|
||||
) : IDE {
|
||||
private val ripgrep: String
|
||||
|
||||
init {
|
||||
val myPluginId = "com.github.continuedev.continueintellijextension"
|
||||
val pluginDescriptor =
|
||||
PluginManager.getPlugin(PluginId.getId(myPluginId)) ?: throw Exception("Plugin not found")
|
||||
|
||||
val pluginPath = pluginDescriptor.pluginPath
|
||||
val os = getOS()
|
||||
ripgrep =
|
||||
Paths.get(pluginPath.toString(), "ripgrep", "bin", "rg" + if (os == OS.WINDOWS) ".exe" else "").toString()
|
||||
}
|
||||
|
||||
override suspend fun getIdeInfo(): IdeInfo {
|
||||
val applicationInfo = ApplicationInfo.getInstance()
|
||||
val ideName: String = applicationInfo.fullApplicationName
|
||||
val ideVersion = applicationInfo.fullVersion
|
||||
val sshClient = System.getenv("SSH_CLIENT")
|
||||
val sshTty = System.getenv("SSH_TTY")
|
||||
|
||||
var remoteName = "local"
|
||||
if (sshClient != null || sshTty != null) {
|
||||
remoteName = "ssh"
|
||||
}
|
||||
|
||||
val pluginId = "com.github.continuedev.continueintellijextension"
|
||||
val plugin = PluginManagerCore.getPlugin(PluginId.getId(pluginId))
|
||||
val extensionVersion = plugin?.version ?: "Unknown"
|
||||
|
||||
return IdeInfo(
|
||||
ideType = IdeType.JETBRAINS,
|
||||
name = ideName,
|
||||
version = ideVersion,
|
||||
remoteName = remoteName,
|
||||
extensionVersion = extensionVersion
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getIdeSettings(): IdeSettings {
|
||||
val settings = service<ContinueExtensionSettings>()
|
||||
|
||||
return IdeSettings(
|
||||
remoteConfigServerUrl = settings.continueState.remoteConfigServerUrl,
|
||||
remoteConfigSyncPeriod = settings.continueState.remoteConfigSyncPeriod,
|
||||
userToken = settings.continueState.userToken ?: "",
|
||||
enableControlServerBeta = settings.continueState.enableContinueTeamsBeta,
|
||||
pauseCodebaseIndexOnStart = false, // TODO: Needs to be implemented
|
||||
enableDebugLogs = false // TODO: Needs to be implemented
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getDiff(includeUnstaged: Boolean): List<String> {
|
||||
val workspaceDirs = workspaceDirectories()
|
||||
val diffs = mutableListOf<String>()
|
||||
|
||||
for (workspaceDir in workspaceDirs) {
|
||||
val output = StringBuilder()
|
||||
val builder = if (includeUnstaged) {
|
||||
ProcessBuilder("git", "diff")
|
||||
} else {
|
||||
ProcessBuilder("git", "diff", "--cached")
|
||||
}
|
||||
builder.directory(File(workspaceDir))
|
||||
val process = withContext(Dispatchers.IO) {
|
||||
builder.start()
|
||||
}
|
||||
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
var line: String? = withContext(Dispatchers.IO) {
|
||||
reader.readLine()
|
||||
}
|
||||
while (line != null) {
|
||||
output.append(line)
|
||||
output.append("\n")
|
||||
line = withContext(Dispatchers.IO) {
|
||||
reader.readLine()
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
process.waitFor()
|
||||
}
|
||||
|
||||
diffs.add(output.toString())
|
||||
}
|
||||
|
||||
return diffs
|
||||
}
|
||||
|
||||
override suspend fun getClipboardContent(): Map<String, String> {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val data = withContext(Dispatchers.IO) {
|
||||
clipboard.getData(DataFlavor.stringFlavor)
|
||||
}
|
||||
val text = data as? String ?: ""
|
||||
return mapOf("text" to text)
|
||||
}
|
||||
|
||||
override suspend fun isTelemetryEnabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getUniqueId(): String {
|
||||
return getMachineUniqueID()
|
||||
}
|
||||
|
||||
override suspend fun getTerminalContents(): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override suspend fun getDebugLocals(threadIndex: Int): String {
|
||||
throw NotImplementedError("getDebugLocals not implemented yet")
|
||||
}
|
||||
|
||||
override suspend fun getTopLevelCallStackSources(threadIndex: Int, stackDepth: Int): List<String> {
|
||||
throw NotImplementedError("getTopLevelCallStackSources not implemented")
|
||||
}
|
||||
|
||||
override suspend fun getAvailableThreads(): List<Thread> {
|
||||
throw NotImplementedError("getAvailableThreads not implemented yet")
|
||||
}
|
||||
|
||||
override suspend fun listFolders(): List<String> {
|
||||
val workspacePath = this.workspacePath ?: return emptyList()
|
||||
val folders = mutableListOf<String>()
|
||||
fun findNestedFolders(dirPath: String) {
|
||||
val dir = File(dirPath)
|
||||
val nestedFolders = dir.listFiles { file -> file.isDirectory }?.map { it.absolutePath } ?: emptyList()
|
||||
folders.addAll(nestedFolders)
|
||||
nestedFolders.forEach { folder -> findNestedFolders(folder) }
|
||||
}
|
||||
findNestedFolders(workspacePath)
|
||||
return folders
|
||||
}
|
||||
|
||||
override suspend fun getWorkspaceDirs(): List<String> {
|
||||
return workspaceDirectories().toList()
|
||||
}
|
||||
|
||||
override suspend fun getWorkspaceConfigs(): List<ContinueRcJson> {
|
||||
val workspaceDirs = workspaceDirectories()
|
||||
|
||||
val configs = mutableListOf<String>()
|
||||
|
||||
for (workspaceDir in workspaceDirs) {
|
||||
val workspacePath = File(workspaceDir)
|
||||
val dir = VirtualFileManager.getInstance().findFileByUrl("file://$workspacePath")
|
||||
if (dir != null) {
|
||||
val contents = dir.children.map { it.name }
|
||||
|
||||
// Find any .continuerc.json files
|
||||
for (file in contents) {
|
||||
if (file.endsWith(".continuerc.json")) {
|
||||
val filePath = workspacePath.resolve(file)
|
||||
val fileContent = File(filePath.toString()).readText()
|
||||
configs.add(fileContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs as List<ContinueRcJson>
|
||||
}
|
||||
|
||||
override suspend fun fileExists(filepath: String): Boolean {
|
||||
val file = File(filepath)
|
||||
return file.exists()
|
||||
}
|
||||
|
||||
override suspend fun writeFile(path: String, contents: String) {
|
||||
val file = File(path)
|
||||
file.writeText(contents)
|
||||
}
|
||||
|
||||
override suspend fun showVirtualFile(title: String, contents: String) {
|
||||
val virtualFile = LightVirtualFile(title, contents)
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
FileEditorManager.getInstance(project).openFile(virtualFile, true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getContinueDir(): String {
|
||||
return getContinueGlobalPath()
|
||||
}
|
||||
|
||||
override suspend fun openFile(path: String) {
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(path)
|
||||
file?.let {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
FileEditorManager.getInstance(project).openFile(it, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openUrl(url: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Desktop.getDesktop().browse(URI(url))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun runCommand(command: String) {
|
||||
throw NotImplementedError("runCommand not implemented in IntelliJ")
|
||||
}
|
||||
|
||||
override suspend fun saveFile(filepath: String) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(filepath) ?: return@invokeLater
|
||||
val fileDocumentManager = FileDocumentManager.getInstance()
|
||||
val document = fileDocumentManager.getDocument(file)
|
||||
|
||||
document?.let {
|
||||
fileDocumentManager.saveDocument(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readFile(filepath: String): String {
|
||||
return try {
|
||||
val content = ApplicationManager.getApplication().runReadAction<String?> {
|
||||
val virtualFile = LocalFileSystem.getInstance().findFileByPath(filepath)
|
||||
if (virtualFile != null && FileDocumentManager.getInstance().isFileModified(virtualFile)) {
|
||||
return@runReadAction FileDocumentManager.getInstance().getDocument(virtualFile)?.text
|
||||
}
|
||||
return@runReadAction null
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
content
|
||||
} else {
|
||||
val file = File(filepath)
|
||||
if (!file.exists()) return ""
|
||||
withContext(Dispatchers.IO) {
|
||||
FileInputStream(file).use { fis ->
|
||||
val sizeToRead = minOf(100000, file.length()).toInt()
|
||||
val buffer = ByteArray(sizeToRead)
|
||||
val bytesRead = fis.read(buffer, 0, sizeToRead)
|
||||
if (bytesRead <= 0) return@use ""
|
||||
String(buffer, 0, bytesRead, Charset.forName("UTF-8"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readRangeInFile(filepath: String, range: Range): String {
|
||||
val fullContents = readFile(filepath)
|
||||
val lines = fullContents.lines()
|
||||
val startLine = range.start.line
|
||||
val startCharacter = range.start.character
|
||||
val endLine = range.end.line
|
||||
val endCharacter = range.end.character
|
||||
|
||||
val firstLine = lines.getOrNull(startLine)?.substring(startCharacter) ?: ""
|
||||
val lastLine = lines.getOrNull(endLine)?.substring(0, endCharacter) ?: ""
|
||||
val betweenLines = if (endLine - startLine > 1) {
|
||||
lines.subList(startLine + 1, endLine).joinToString("\n")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return listOf(firstLine, betweenLines, lastLine).filter { it.isNotEmpty() }.joinToString("\n")
|
||||
}
|
||||
|
||||
override suspend fun showLines(filepath: String, startLine: Int, endLine: Int) {
|
||||
setFileOpen(filepath, true)
|
||||
}
|
||||
|
||||
override suspend fun showDiff(filepath: String, newContents: String, stepIndex: Int) {
|
||||
continuePluginService.diffManager?.showDiff(filepath, newContents, stepIndex)
|
||||
}
|
||||
|
||||
override suspend fun getOpenFiles(): List<String> {
|
||||
val fileEditorManager = FileEditorManager.getInstance(project)
|
||||
return fileEditorManager.openFiles.map { it.path }.toList()
|
||||
}
|
||||
|
||||
override suspend fun getCurrentFile(): Map<String, Any>? {
|
||||
val fileEditorManager = FileEditorManager.getInstance(project)
|
||||
val editor = fileEditorManager.selectedTextEditor
|
||||
val virtualFile = editor?.document?.let { FileDocumentManager.getInstance().getFile(it) }
|
||||
return virtualFile?.let {
|
||||
mapOf(
|
||||
"path" to it.path,
|
||||
"contents" to editor.document.text,
|
||||
"isUntitled" to false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPinnedFiles(): List<String> {
|
||||
// Returning open files for now as per existing code
|
||||
return getOpenFiles()
|
||||
}
|
||||
|
||||
override suspend fun getSearchResults(query: String): String {
|
||||
val command = GeneralCommandLine(ripgrep, "-i", "-C", "2", "--", query, ".")
|
||||
command.setWorkDirectory(project.basePath)
|
||||
return ExecUtil.execAndGetOutput(command).stdout
|
||||
}
|
||||
|
||||
override suspend fun subprocess(command: String, cwd: String?): List<Any> {
|
||||
val commandList = command.split(" ")
|
||||
val builder = ProcessBuilder(commandList)
|
||||
|
||||
if (cwd != null) {
|
||||
builder.directory(File(cwd))
|
||||
}
|
||||
|
||||
val process = withContext(Dispatchers.IO) {
|
||||
builder.start()
|
||||
}
|
||||
|
||||
val stdout = process.inputStream.bufferedReader().readText()
|
||||
val stderr = process.errorStream.bufferedReader().readText()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
process.waitFor()
|
||||
}
|
||||
|
||||
return listOf(stdout, stderr)
|
||||
}
|
||||
|
||||
override suspend fun getProblems(filepath: String?): List<Problem> {
|
||||
val problems = mutableListOf<Problem>()
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return@invokeAndWait
|
||||
|
||||
val document = editor.document
|
||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@invokeAndWait
|
||||
val highlightInfos = DocumentMarkupModel.forDocument(document, project, true)
|
||||
.allHighlighters
|
||||
.mapNotNull(HighlightInfo::fromRangeHighlighter)
|
||||
|
||||
for (highlightInfo in highlightInfos) {
|
||||
if (highlightInfo.severity === HighlightSeverity.ERROR ||
|
||||
highlightInfo.severity === HighlightSeverity.WARNING
|
||||
) {
|
||||
val startOffset = highlightInfo.startOffset
|
||||
val endOffset = highlightInfo.endOffset
|
||||
|
||||
val startLineNumber = document.getLineNumber(startOffset)
|
||||
val endLineNumber = document.getLineNumber(endOffset)
|
||||
val startCharacter = startOffset - document.getLineStartOffset(startLineNumber)
|
||||
val endCharacter = endOffset - document.getLineStartOffset(endLineNumber)
|
||||
|
||||
problems.add(
|
||||
Problem(
|
||||
filepath = psiFile.virtualFile?.path ?: "",
|
||||
range = Range(
|
||||
start = Position(
|
||||
line = startLineNumber,
|
||||
character = startCharacter
|
||||
),
|
||||
end = Position(
|
||||
line = endLineNumber,
|
||||
character = endCharacter
|
||||
)
|
||||
),
|
||||
message = highlightInfo.description
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
override suspend fun getBranch(dir: String): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val builder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
builder.directory(File(dir))
|
||||
val process = builder.start()
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val output = reader.readLine()
|
||||
process.waitFor()
|
||||
output ?: "NONE"
|
||||
} catch (e: Exception) {
|
||||
"NONE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTags(artifactId: String): List<IndexTag> {
|
||||
val workspaceDirs = workspaceDirectories()
|
||||
|
||||
// Collect branches concurrently using Kotlin coroutines
|
||||
val branches = withContext(Dispatchers.IO) {
|
||||
workspaceDirs.map { dir ->
|
||||
async { getBranch(dir) }
|
||||
}.map { it.await() }
|
||||
}
|
||||
|
||||
// Create the list of IndexTag objects
|
||||
return workspaceDirs.mapIndexed { index, directory ->
|
||||
IndexTag(directory, branches[index], artifactId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRepoName(dir: String): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val directory = File(dir)
|
||||
val targetDir = if (directory.isFile) directory.parentFile else directory
|
||||
val builder = ProcessBuilder("git", "config", "--get", "remote.origin.url")
|
||||
builder.directory(targetDir)
|
||||
var output: String?
|
||||
|
||||
try {
|
||||
val process = builder.start()
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
output = reader.readLine()
|
||||
process.waitFor()
|
||||
} catch (e: Exception) {
|
||||
output = null
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun showToast(type: ToastType, message: String, vararg otherParams: Any): Any {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val notificationType = when (type) {
|
||||
ToastType.ERROR -> NotificationType.ERROR
|
||||
ToastType.WARNING -> NotificationType.WARNING
|
||||
else -> NotificationType.INFORMATION
|
||||
}
|
||||
|
||||
val deferred = CompletableDeferred<String?>()
|
||||
val icon = IconLoader.getIcon("/icons/continue.svg", javaClass)
|
||||
|
||||
val notification = NotificationGroupManager.getInstance().getNotificationGroup("Continue")
|
||||
.createNotification(message, notificationType).setIcon(icon)
|
||||
|
||||
val buttonTexts = otherParams.filterIsInstance<String>().toTypedArray()
|
||||
buttonTexts.forEach { buttonText ->
|
||||
notification.addAction(NotificationAction.create(buttonText) { _, _ ->
|
||||
deferred.complete(buttonText)
|
||||
notification.expire()
|
||||
})
|
||||
}
|
||||
|
||||
launch {
|
||||
delay(15000)
|
||||
if (!deferred.isCompleted) {
|
||||
deferred.complete(null)
|
||||
notification.expire()
|
||||
}
|
||||
}
|
||||
|
||||
notification.whenExpired {
|
||||
if (!deferred.isCompleted) {
|
||||
deferred.complete(null)
|
||||
}
|
||||
}
|
||||
|
||||
notification.notify(project)
|
||||
|
||||
deferred.await() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getGitRootPath(dir: String): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val builder = ProcessBuilder("git", "rev-parse", "--show-toplevel")
|
||||
builder.directory(File(dir))
|
||||
val process = builder.start()
|
||||
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val output = reader.readLine()
|
||||
process.waitFor()
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listDir(dir: String): List<List<Any>> {
|
||||
val files = File(dir).listFiles()?.map {
|
||||
listOf(it.name, if (it.isDirectory) FileType.DIRECTORY else FileType.FILE)
|
||||
} ?: emptyList()
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
override suspend fun getLastModified(files: List<String>): Map<String, Long> {
|
||||
return files.associateWith { file ->
|
||||
File(file).lastModified()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getGitHubAuthToken(args: GetGhTokenArgs): String? {
|
||||
val continueSettingsService = service<ContinueExtensionSettings>()
|
||||
return continueSettingsService.continueState.ghAuthToken
|
||||
}
|
||||
|
||||
override suspend fun gotoDefinition(location: Location): List<RangeInFile> {
|
||||
throw NotImplementedError("gotoDefinition not implemented yet")
|
||||
}
|
||||
|
||||
override fun onDidChangeActiveTextEditor(callback: (filepath: String) -> Unit) {
|
||||
throw NotImplementedError("onDidChangeActiveTextEditor not implemented yet")
|
||||
}
|
||||
|
||||
override suspend fun pathSep(): String {
|
||||
return File.separator
|
||||
}
|
||||
|
||||
private fun setFileOpen(filepath: String, open: Boolean = true) {
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(filepath)
|
||||
|
||||
file?.let {
|
||||
if (open) {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
FileEditorManager.getInstance(project).openFile(it, true)
|
||||
}
|
||||
} else {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
FileEditorManager.getInstance(project).closeFile(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun workspaceDirectories(): Array<String> {
|
||||
val dirs = this.continuePluginService.workspacePaths
|
||||
|
||||
if (dirs?.isNotEmpty() == true) {
|
||||
return dirs
|
||||
}
|
||||
|
||||
if (this.workspacePath != null) {
|
||||
return arrayOf(this.workspacePath)
|
||||
}
|
||||
|
||||
return arrayOf()
|
||||
}
|
||||
}
|
|
@ -42,12 +42,12 @@ class ContinuePluginSelectionListener(
|
|||
|
||||
|
||||
private fun handleSelection(e: SelectionEvent) {
|
||||
ApplicationManager.getApplication().runReadAction {
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
val editor = e.editor
|
||||
|
||||
if (!isFileEditor(editor)) {
|
||||
removeAllTooltips()
|
||||
return@runReadAction
|
||||
return@invokeLater
|
||||
}
|
||||
|
||||
// Fixes a bug where the tooltip isn't being disposed of when opening new files
|
||||
|
@ -61,7 +61,7 @@ class ContinuePluginSelectionListener(
|
|||
|
||||
if (shouldRemoveTooltip(selectedText, editor)) {
|
||||
removeExistingTooltips(editor)
|
||||
return@runReadAction
|
||||
return@invokeLater
|
||||
}
|
||||
|
||||
updateTooltip(editor, model)
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package com.github.continuedev.continueintellijextension.protocol
|
||||
|
||||
import com.github.continuedev.continueintellijextension.*
|
||||
|
||||
data class GetControlPlaneSessionInfoParams(val silent: Boolean)
|
||||
|
||||
data class WriteFileParams(
|
||||
val path: String,
|
||||
val contents: String
|
||||
)
|
||||
|
||||
data class ShowVirtualFileParams(
|
||||
val name: String,
|
||||
val content: String
|
||||
)
|
||||
|
||||
|
||||
data class OpenFileParams(val path: String)
|
||||
|
||||
typealias OpenUrlParam = String
|
||||
|
||||
typealias getTagsParams = String
|
||||
|
||||
data class GetSearchResultsParams(val query: String)
|
||||
|
||||
|
||||
data class SaveFileParams(val filepath: String)
|
||||
|
||||
data class FileExistsParams(val filepath: String)
|
||||
|
||||
data class ReadFileParams(val filepath: String)
|
||||
|
||||
data class ShowDiffParams(
|
||||
val filepath: String,
|
||||
val newContents: String,
|
||||
val stepIndex: Int
|
||||
)
|
||||
|
||||
data class ShowLinesParams(
|
||||
val filepath: String,
|
||||
val startLine: Int,
|
||||
val endLine: Int
|
||||
)
|
||||
|
||||
data class ReadRangeInFileParams(
|
||||
val filepath: String,
|
||||
val range: Range
|
||||
)
|
||||
|
||||
|
||||
data class GetDiffParams(val includeUnstaged: Boolean)
|
||||
|
||||
data class GetBranchParams(val dir: String)
|
||||
|
||||
data class GetRepoNameParams(val dir: String)
|
||||
|
||||
data class GetGitRootPathParams(val dir: String)
|
||||
|
||||
data class ListDirParams(val dir: String)
|
||||
|
||||
data class GetLastModifiedParams(val files: List<String>)
|
|
@ -0,0 +1,21 @@
|
|||
package com.github.continuedev.continueintellijextension.protocol
|
||||
|
||||
data class CopyTextParams(
|
||||
val text: String
|
||||
)
|
||||
|
||||
data class SetGitHubAuthTokenParams(
|
||||
val token: String
|
||||
)
|
||||
|
||||
data class ApplyToFileParams(
|
||||
val text: String,
|
||||
val streamId: String,
|
||||
val curSelectedModelTitle: String,
|
||||
val filepath: String?
|
||||
)
|
||||
|
||||
data class InsertAtCursorParams(
|
||||
val text: String
|
||||
)
|
||||
|
|
@ -1,36 +1,30 @@
|
|||
package com.github.continuedev.continueintellijextension.services
|
||||
|
||||
import IntelliJIDE
|
||||
import com.github.continuedev.continueintellijextension.`continue`.CoreMessenger
|
||||
import com.github.continuedev.continueintellijextension.`continue`.CoreMessengerManager
|
||||
import com.github.continuedev.continueintellijextension.`continue`.DiffManager
|
||||
import com.github.continuedev.continueintellijextension.`continue`.IdeProtocolClient
|
||||
import com.github.continuedev.continueintellijextension.`continue`.uuid
|
||||
import com.github.continuedev.continueintellijextension.toolWindow.ContinueBrowser
|
||||
import com.github.continuedev.continueintellijextension.toolWindow.ContinuePluginToolWindowFactory
|
||||
import com.google.gson.Gson
|
||||
import com.github.continuedev.continueintellijextension.utils.uuid
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.jcef.executeJavaScriptAsync
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
class ContinuePluginService(project: Project) : Disposable, DumbAware {
|
||||
val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
class ContinuePluginService : Disposable, DumbAware {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
var continuePluginWindow: ContinuePluginToolWindowFactory.ContinuePluginWindow? = null
|
||||
|
||||
var ideProtocolClient: IdeProtocolClient? = null
|
||||
|
||||
var coreMessengerManager: CoreMessengerManager? = null
|
||||
val coreMessenger: CoreMessenger?
|
||||
get() = coreMessengerManager?.coreMessenger
|
||||
|
||||
var workspacePaths: Array<String>? = null
|
||||
var windowId: String = UUID.randomUUID().toString()
|
||||
var windowId: String = uuid()
|
||||
var diffManager: DiffManager? = null
|
||||
|
||||
override fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package com.github.continuedev.continueintellijextension.toolWindow
|
||||
|
||||
import com.github.continuedev.continueintellijextension.activities.ContinuePluginDisposable
|
||||
import com.github.continuedev.continueintellijextension.activities.showTutorial
|
||||
import com.github.continuedev.continueintellijextension.constants.MessageTypes
|
||||
import com.github.continuedev.continueintellijextension.constants.getConfigJsonPath
|
||||
import com.github.continuedev.continueintellijextension.`continue`.*
|
||||
import com.github.continuedev.continueintellijextension.factories.CustomSchemeHandlerFactory
|
||||
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.github.continuedev.continueintellijextension.utils.uuid
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
|
@ -15,57 +13,61 @@ import com.intellij.openapi.components.ServiceManager
|
|||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.jcef.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.cef.CefApp
|
||||
import org.cef.browser.CefBrowser
|
||||
import org.cef.handler.CefLoadHandlerAdapter
|
||||
|
||||
class ContinueBrowser(val project: Project, url: String) {
|
||||
private val coroutineScope = CoroutineScope(
|
||||
SupervisorJob() + Dispatchers.Default
|
||||
)
|
||||
|
||||
private val heightChangeListeners = mutableListOf<(Int) -> Unit>()
|
||||
|
||||
private val PASS_THROUGH_TO_CORE = listOf(
|
||||
"ping",
|
||||
"abort",
|
||||
"history/list",
|
||||
"history/delete",
|
||||
"history/load",
|
||||
"history/save",
|
||||
"devdata/log",
|
||||
"config/addOpenAiKey",
|
||||
"config/addModel",
|
||||
"config/newPromptFile",
|
||||
"config/ideSettingsUpdate",
|
||||
"config/getSerializedProfileInfo",
|
||||
"config/deleteModel",
|
||||
"config/newPromptFile",
|
||||
"config/reload",
|
||||
"config/listProfiles",
|
||||
"config/openProfile",
|
||||
"context/getContextItems",
|
||||
"context/getSymbolsForFiles",
|
||||
"context/loadSubmenuItems",
|
||||
"context/addDocs",
|
||||
"context/removeDocs",
|
||||
"context/indexDocs",
|
||||
"autocomplete/complete",
|
||||
"autocomplete/cancel",
|
||||
"autocomplete/accept",
|
||||
"command/run",
|
||||
"tts/kill",
|
||||
"llm/complete",
|
||||
"llm/streamComplete",
|
||||
"llm/streamChat",
|
||||
"llm/listModels",
|
||||
"streamDiffLines",
|
||||
"chatDescriber/describe",
|
||||
"stats/getTokensPerDay",
|
||||
"stats/getTokensPerModel",
|
||||
// Codebase
|
||||
"index/setPaused",
|
||||
"index/forceReIndex",
|
||||
"index/forceReIndexFiles",
|
||||
"index/indexingProgressBarInitialized",
|
||||
// Docs, etc.
|
||||
"indexing/reindex",
|
||||
"indexing/abort",
|
||||
"indexing/setPaused",
|
||||
"docs/getSuggestedDocs",
|
||||
"docs/initStatuses",
|
||||
//
|
||||
"completeOnboarding",
|
||||
"addAutocompleteModel",
|
||||
"config/listProfiles",
|
||||
"profiles/switch",
|
||||
"didChangeSelectedProfile",
|
||||
"context/getSymbolsForFiles",
|
||||
"tools/call",
|
||||
)
|
||||
|
||||
private fun registerAppSchemeHandler() {
|
||||
|
@ -94,6 +96,7 @@ class ContinueBrowser(val project: Project, url: String) {
|
|||
|
||||
// Listen for events sent from browser
|
||||
val myJSQueryOpenInBrowser = JBCefJSQuery.create((browser as JBCefBrowserBase?)!!)
|
||||
|
||||
myJSQueryOpenInBrowser.addHandler { msg: String? ->
|
||||
val parser = JsonParser()
|
||||
val json: JsonObject = parser.parse(msg).asJsonObject
|
||||
|
@ -106,16 +109,14 @@ class ContinueBrowser(val project: Project, url: String) {
|
|||
ContinuePluginService::class.java
|
||||
)
|
||||
|
||||
val ide = continuePluginService.ideProtocolClient;
|
||||
|
||||
val respond = fun(data: Any?) {
|
||||
// This matches the way that we expect receive messages in IdeMessenger.ts (gui)
|
||||
// and the way they are sent in VS Code (webviewProtocol.ts)
|
||||
var result: Map<String, Any?>? = null
|
||||
if (MessageTypes.generatorTypes.contains(messageType)) {
|
||||
result = data as? Map<String, Any?>
|
||||
var result: Map<String, Any?>? = if (MessageTypes.generatorTypes.contains(messageType)) {
|
||||
data as? Map<String, Any?>
|
||||
} else {
|
||||
result = mutableMapOf(
|
||||
mutableMapOf(
|
||||
"status" to "success",
|
||||
"done" to false,
|
||||
"content" to data
|
||||
|
@ -130,98 +131,10 @@ class ContinueBrowser(val project: Project, url: String) {
|
|||
return@addHandler null
|
||||
}
|
||||
|
||||
when (messageType) {
|
||||
"jetbrains/editorInsetHeight" -> {
|
||||
val height = data.asJsonObject.get("height").asInt
|
||||
heightChangeListeners.forEach { it(height) }
|
||||
}
|
||||
|
||||
"jetbrains/isOSREnabled" -> {
|
||||
respond(isOSREnabled)
|
||||
}
|
||||
|
||||
"onLoad" -> {
|
||||
coroutineScope.launch {
|
||||
// Set the colors to match Intellij theme
|
||||
val colors = GetTheme().getTheme();
|
||||
sendToWebview("setColors", colors)
|
||||
|
||||
val jsonData = mutableMapOf(
|
||||
"windowId" to continuePluginService.windowId,
|
||||
"workspacePaths" to continuePluginService.workspacePaths,
|
||||
"vscMachineId" to getMachineUniqueID(),
|
||||
"vscMediaUrl" to "http://continue",
|
||||
)
|
||||
respond(jsonData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"showLines" -> {
|
||||
val data = data.asJsonObject
|
||||
ide?.setFileOpen(data.get("filepath").asString)
|
||||
ide?.highlightCode(
|
||||
RangeInFile(
|
||||
data.get("filepath").asString,
|
||||
Range(
|
||||
Position(
|
||||
data.get("start").asInt,
|
||||
0
|
||||
), Position(
|
||||
data.get("end").asInt,
|
||||
0
|
||||
)
|
||||
),
|
||||
|
||||
), "#00ff0022"
|
||||
)
|
||||
}
|
||||
|
||||
"showTutorial" -> {
|
||||
showTutorial(project)
|
||||
}
|
||||
|
||||
"showVirtualFile" -> {
|
||||
val data = data.asJsonObject
|
||||
ide?.showVirtualFile(data.get("name").asString, data.get("content").asString)
|
||||
}
|
||||
|
||||
"showFile" -> {
|
||||
val data = data.asJsonObject
|
||||
ide?.setFileOpen(data.get("filepath").asString)
|
||||
}
|
||||
|
||||
"reloadWindow" -> {}
|
||||
|
||||
"readRangeInFile" -> {
|
||||
val data = data.asJsonObject
|
||||
ide?.readRangeInFile(
|
||||
RangeInFile(
|
||||
data.get("filepath").asString,
|
||||
Range(
|
||||
Position(
|
||||
data.get("start").asInt,
|
||||
0
|
||||
), Position(
|
||||
data.get("end").asInt + 1,
|
||||
0
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"focusEditor" -> {}
|
||||
|
||||
// IDE //
|
||||
else -> {
|
||||
if (msg != null) {
|
||||
ide?.handleMessage(msg, respond)
|
||||
}
|
||||
}
|
||||
if (msg != null) {
|
||||
continuePluginService.ideProtocolClient?.handleMessage(msg, respond)
|
||||
}
|
||||
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
package com.github.continuedev.continueintellijextension
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
enum class IdeType(val value: String) {
|
||||
JETBRAINS("jetbrains"),
|
||||
VSCODE("vscode"),
|
||||
}
|
||||
|
||||
enum class ToastType(val value: String) {
|
||||
INFO("info"),
|
||||
ERROR("error"),
|
||||
WARNING("warning"),
|
||||
}
|
||||
|
||||
enum class FileType(val value: Int) {
|
||||
UNKNOWN(0),
|
||||
FILE(1),
|
||||
DIRECTORY(2),
|
||||
SYMBOLIC_LINK(64)
|
||||
}
|
||||
|
||||
enum class ConfigMergeType {
|
||||
MERGE,
|
||||
OVERWRITE
|
||||
}
|
||||
|
||||
data class Position(val line: Int, val character: Int)
|
||||
|
||||
data class Range(val start: Position, val end: Position)
|
||||
|
||||
data class IdeInfo(
|
||||
val ideType: IdeType,
|
||||
val name: String,
|
||||
val version: String,
|
||||
val remoteName: String,
|
||||
val extensionVersion: String
|
||||
)
|
||||
|
||||
data class Problem(
|
||||
val filepath: String,
|
||||
val range: Range,
|
||||
val message: String
|
||||
)
|
||||
|
||||
data class Thread(val name: String, val id: Int)
|
||||
|
||||
data class IndexTag(
|
||||
val artifactId: String,
|
||||
val branch: String,
|
||||
val directory: String
|
||||
)
|
||||
|
||||
data class Location(
|
||||
val filepath: String,
|
||||
val position: Position
|
||||
)
|
||||
|
||||
data class RangeInFile(
|
||||
val filepath: String,
|
||||
val range: Range
|
||||
)
|
||||
|
||||
data class RangeInFileWithContents(
|
||||
val filepath: String,
|
||||
val range: Range,
|
||||
val contents: String
|
||||
)
|
||||
|
||||
data class ControlPlaneSessionInfo(
|
||||
val accessToken: String,
|
||||
val account: Account
|
||||
)
|
||||
|
||||
data class Account(
|
||||
val label: String,
|
||||
val id: String
|
||||
)
|
||||
|
||||
data class IdeSettings(
|
||||
val remoteConfigServerUrl: String?,
|
||||
val remoteConfigSyncPeriod: Int,
|
||||
val userToken: String,
|
||||
val enableControlServerBeta: Boolean,
|
||||
val pauseCodebaseIndexOnStart: Boolean,
|
||||
val enableDebugLogs: Boolean
|
||||
)
|
||||
|
||||
data class ContinueRcJson(
|
||||
val mergeBehavior: ConfigMergeType
|
||||
)
|
||||
|
||||
|
||||
interface IDE {
|
||||
suspend fun getIdeInfo(): IdeInfo
|
||||
|
||||
suspend fun getIdeSettings(): IdeSettings
|
||||
|
||||
suspend fun getDiff(includeUnstaged: Boolean): List<String>
|
||||
|
||||
suspend fun getClipboardContent(): Map<String, String>
|
||||
|
||||
suspend fun isTelemetryEnabled(): Boolean
|
||||
|
||||
suspend fun getUniqueId(): String
|
||||
|
||||
suspend fun getTerminalContents(): String
|
||||
|
||||
suspend fun getDebugLocals(threadIndex: Int): String
|
||||
|
||||
suspend fun getTopLevelCallStackSources(
|
||||
threadIndex: Int,
|
||||
stackDepth: Int
|
||||
): List<String>
|
||||
|
||||
suspend fun getAvailableThreads(): List<Thread>
|
||||
|
||||
suspend fun listFolders(): List<String>
|
||||
|
||||
suspend fun getWorkspaceDirs(): List<String>
|
||||
|
||||
suspend fun getWorkspaceConfigs(): List<ContinueRcJson>
|
||||
|
||||
suspend fun fileExists(filepath: String): Boolean
|
||||
|
||||
suspend fun writeFile(path: String, contents: String)
|
||||
|
||||
suspend fun showVirtualFile(title: String, contents: String)
|
||||
|
||||
suspend fun getContinueDir(): String
|
||||
|
||||
suspend fun openFile(path: String)
|
||||
|
||||
suspend fun openUrl(url: String)
|
||||
|
||||
suspend fun runCommand(command: String)
|
||||
|
||||
suspend fun saveFile(filepath: String)
|
||||
|
||||
suspend fun readFile(filepath: String): String
|
||||
|
||||
suspend fun readRangeInFile(filepath: String, range: Range): String
|
||||
|
||||
suspend fun showLines(
|
||||
filepath: String,
|
||||
startLine: Int,
|
||||
endLine: Int
|
||||
)
|
||||
|
||||
suspend fun showDiff(
|
||||
filepath: String,
|
||||
newContents: String,
|
||||
stepIndex: Int
|
||||
)
|
||||
|
||||
suspend fun getOpenFiles(): List<String>
|
||||
|
||||
suspend fun getCurrentFile(): Map<String, Any>?
|
||||
|
||||
suspend fun getPinnedFiles(): List<String>
|
||||
|
||||
suspend fun getSearchResults(query: String): String
|
||||
|
||||
// Note: This should be a `Pair<String, String>` but we use `List<Any>` because the keys of `Pair`
|
||||
// will serialize to `first and `second` rather than `0` and `1` like in JavaScript
|
||||
suspend fun subprocess(command: String, cwd: String? = null): List<Any>
|
||||
|
||||
suspend fun getProblems(filepath: String? = null): List<Problem>
|
||||
|
||||
suspend fun getBranch(dir: String): String
|
||||
|
||||
suspend fun getTags(artifactId: String): List<IndexTag>
|
||||
|
||||
suspend fun getRepoName(dir: String): String?
|
||||
|
||||
suspend fun showToast(
|
||||
type: ToastType,
|
||||
message: String,
|
||||
vararg otherParams: Any
|
||||
): Any
|
||||
|
||||
suspend fun getGitRootPath(dir: String): String?
|
||||
|
||||
// Note: This should be a `List<Pair<String, FileType>>` but we use `List<Any>` because the keys of `Pair`
|
||||
// will serialize to `first and `second` rather than `0` and `1` like in JavaScript
|
||||
suspend fun listDir(dir: String): List<List<Any>>
|
||||
|
||||
suspend fun getLastModified(files: List<String>): Map<String, Long>
|
||||
|
||||
suspend fun getGitHubAuthToken(args: GetGhTokenArgs): String?
|
||||
|
||||
// LSP
|
||||
suspend fun gotoDefinition(location: Location): List<RangeInFile>
|
||||
|
||||
// Callbacks
|
||||
fun onDidChangeActiveTextEditor(callback: (filepath: String) -> Unit)
|
||||
|
||||
suspend fun pathSep(): String
|
||||
}
|
||||
|
||||
data class GetGhTokenArgs(
|
||||
val force: String?
|
||||
)
|
||||
|
||||
data class Message(
|
||||
val messageType: String,
|
||||
val messageId: String,
|
||||
val data: JsonElement
|
||||
)
|
||||
|
||||
// TODO: Needs to be updated to handle new "apply" logic
|
||||
data class AcceptRejectDiff(val accepted: Boolean, val stepIndex: Int)
|
||||
|
||||
data class DeleteAtIndex(val index: Int)
|
|
@ -1,56 +0,0 @@
|
|||
package com.github.continuedev.continueintellijextension.utils
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.intellij.ui.jcef.JBCefBrowser
|
||||
import com.intellij.ui.jcef.executeJavaScriptAsync
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun CoroutineScope.dispatchEventToWebview(
|
||||
type: String,
|
||||
data: Map<String, Any>,
|
||||
webView: JBCefBrowser?
|
||||
) {
|
||||
if (webView == null) return
|
||||
val gson = Gson()
|
||||
val jsonData = gson.toJson(data)
|
||||
val jsCode = buildJavaScript(type, jsonData)
|
||||
|
||||
launch(CoroutineExceptionHandler { _, exception ->
|
||||
println("Failed to dispatch custom event: ${exception.message}")
|
||||
}) {
|
||||
while (true) {
|
||||
try {
|
||||
webView.executeJavaScriptAsync(jsCode)
|
||||
break // If the JS execution is successful, break the loop
|
||||
} catch (e: IllegalStateException) {
|
||||
delay(1000) // If an error occurs, wait for 1 second and then retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.runJsInWebview(
|
||||
jsCode: String,
|
||||
webView: JBCefBrowser?
|
||||
) {
|
||||
if (webView == null) return
|
||||
launch(CoroutineExceptionHandler { _, exception ->
|
||||
println("Failed to dispatch custom event: ${exception.message}")
|
||||
}) {
|
||||
while (true) {
|
||||
try {
|
||||
webView.executeJavaScriptAsync(jsCode)
|
||||
break // If the JS execution is successful, break the loop
|
||||
} catch (e: IllegalStateException) {
|
||||
delay(1000) // If an error occurs, wait for 1 second and then retry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildJavaScript(type: String, jsonData: String): String {
|
||||
return """window.postMessage($jsonData, "*");"""
|
||||
}
|
|
@ -1,39 +1,80 @@
|
|||
package com.github.continuedev.continueintellijextension.utils
|
||||
|
||||
enum class Os {
|
||||
import java.net.NetworkInterface
|
||||
import java.util.*
|
||||
import java.awt.event.KeyEvent.*
|
||||
|
||||
enum class OS {
|
||||
MAC, WINDOWS, LINUX
|
||||
}
|
||||
|
||||
fun getOs(): Os {
|
||||
val osName = System.getProperty("os.name").toLowerCase()
|
||||
fun getMetaKey(): Int {
|
||||
return when (getOS()) {
|
||||
OS.MAC -> VK_META
|
||||
OS.WINDOWS -> VK_CONTROL
|
||||
OS.LINUX -> VK_CONTROL
|
||||
}
|
||||
}
|
||||
|
||||
fun getOS(): OS {
|
||||
val osName = System.getProperty("os.name").lowercase()
|
||||
val os = when {
|
||||
osName.contains("mac") || osName.contains("darwin") -> Os.MAC
|
||||
osName.contains("win") -> Os.WINDOWS
|
||||
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> Os.LINUX
|
||||
else -> Os.LINUX
|
||||
osName.contains("mac") || osName.contains("darwin") -> OS.MAC
|
||||
osName.contains("win") -> OS.WINDOWS
|
||||
osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> OS.LINUX
|
||||
else -> OS.LINUX
|
||||
}
|
||||
return os
|
||||
}
|
||||
|
||||
fun getMetaKeyLabel(): String {
|
||||
return when (getOs()) {
|
||||
Os.MAC -> "⌘"
|
||||
Os.WINDOWS -> "^"
|
||||
Os.LINUX -> "^"
|
||||
return when (getOS()) {
|
||||
OS.MAC -> "⌘"
|
||||
OS.WINDOWS -> "^"
|
||||
OS.LINUX -> "^"
|
||||
}
|
||||
}
|
||||
|
||||
fun getAltKeyLabel(): String {
|
||||
return when (getOs()) {
|
||||
Os.MAC -> "⌥"
|
||||
Os.WINDOWS -> "Alt"
|
||||
Os.LINUX -> "Alt"
|
||||
return when (getOS()) {
|
||||
OS.MAC -> "⌥"
|
||||
OS.WINDOWS -> "Alt"
|
||||
OS.LINUX -> "Alt"
|
||||
}
|
||||
}
|
||||
|
||||
fun getShiftKeyLabel(): String {
|
||||
return when (getOs()) {
|
||||
Os.MAC -> "⇧"
|
||||
Os.WINDOWS, Os.LINUX -> "↑"
|
||||
return when (getOS()) {
|
||||
OS.MAC -> "⇧"
|
||||
OS.WINDOWS, OS.LINUX -> "↑"
|
||||
}
|
||||
}
|
||||
|
||||
fun getMachineUniqueID(): String {
|
||||
val sb = StringBuilder()
|
||||
val networkInterfaces = NetworkInterface.getNetworkInterfaces()
|
||||
|
||||
while (networkInterfaces.hasMoreElements()) {
|
||||
val networkInterface = networkInterfaces.nextElement()
|
||||
val mac = networkInterface.hardwareAddress
|
||||
|
||||
if (mac != null) {
|
||||
for (i in mac.indices) {
|
||||
sb.append(
|
||||
String.format(
|
||||
"%02X%s",
|
||||
mac[i],
|
||||
if (i < mac.size - 1) "-" else ""
|
||||
)
|
||||
)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
return "No MAC Address Found"
|
||||
}
|
||||
|
||||
fun uuid(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package com.github.continuedev.continueintellijextension
|
||||
|
||||
import com.github.continuedev.continueintellijextension.editor.DiffStreamHandler
|
||||
import com.github.continuedev.continueintellijextension.editor.DiffStreamService
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.impl.ImaginaryEditor
|
||||
import com.intellij.testFramework.TestDataPath
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
|
||||
@TestDataPath("\$CONTENT_ROOT/src/test/testData")
|
||||
class DiffStreamTest : BasePlatformTestCase() {
|
||||
fun `test getAllInlaysForEditor`() {
|
||||
myFixture.configureByText("index.ts", "console.log('Hello World!');")
|
||||
val editor = ImaginaryEditor(myFixture.project, myFixture.editor.document)
|
||||
|
||||
val diffStreamService = project.service<DiffStreamService>()
|
||||
val diffStreamHandler = DiffStreamHandler(project, editor, 0, 1, { -> }, { -> })
|
||||
|
||||
diffStreamService.register(diffStreamHandler, editor)
|
||||
|
||||
diffStreamHandler.streamDiffLinesToEditor()
|
||||
|
||||
assertEquals(editor.document.text, myFixture.file.text)
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package com.github.continuedev.continueintellijextension
|
||||
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
import com.intellij.ide.highlighter.XmlFileType
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.psi.xml.XmlFile
|
||||
import com.intellij.testFramework.TestDataPath
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.util.PsiErrorElementUtil
|
||||
|
||||
@TestDataPath("\$CONTENT_ROOT/src/test/testData")
|
||||
class MyPluginTest : BasePlatformTestCase() {
|
||||
|
||||
fun testXMLFile() {
|
||||
val psiFile =
|
||||
myFixture.configureByText(XmlFileType.INSTANCE, "<foo>bar</foo>")
|
||||
val xmlFile = assertInstanceOf(psiFile, XmlFile::class.java)
|
||||
|
||||
assertFalse(PsiErrorElementUtil.hasErrors(project, xmlFile.virtualFile))
|
||||
|
||||
assertNotNull(xmlFile.rootTag)
|
||||
|
||||
xmlFile.rootTag?.let {
|
||||
assertEquals("foo", it.name)
|
||||
assertEquals("bar", it.value.text)
|
||||
}
|
||||
}
|
||||
|
||||
fun testProjectService() {
|
||||
val projectService = project.service<ContinuePluginService>()
|
||||
|
||||
// assertNotSame(
|
||||
// projectService.getRandomNumber(),
|
||||
// projectService.getRandomNumber()
|
||||
// )
|
||||
}
|
||||
|
||||
override fun getTestDataPath() = "src/test/testData/rename"
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.github.continuedev.continueintellijextension
|
||||
|
||||
import com.github.continuedev.continueintellijextension.editor.VerticalDiffBlock
|
||||
import com.intellij.openapi.command.WriteCommandAction
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.colors.TextAttributesKey
|
||||
import com.intellij.testFramework.LightVirtualFile
|
||||
import com.intellij.openapi.fileTypes.PlainTextFileType
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
|
||||
class VerticalDiffBlockTest : BasePlatformTestCase() {
|
||||
|
||||
private lateinit var editor: Editor
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
// Create a document and an editor with a virtual file
|
||||
val document = EditorFactory.getInstance().createDocument("")
|
||||
val virtualFile = LightVirtualFile("TestFile.kt", PlainTextFileType.INSTANCE, "")
|
||||
editor = EditorFactory.getInstance().createEditor(document, project, virtualFile, false)
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
// Release the editor to avoid memory leaks
|
||||
EditorFactory.getInstance().releaseEditor(editor)
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun testAddNewLine() {
|
||||
// Arrange
|
||||
val textToAdd = "This is a new line"
|
||||
val startLine = 0
|
||||
|
||||
// Create an instance of VerticalDiffBlock
|
||||
val verticalDiffBlock = VerticalDiffBlock(
|
||||
editor = editor,
|
||||
project = project,
|
||||
startLine = startLine,
|
||||
onAcceptReject = { _, _ -> }
|
||||
)
|
||||
|
||||
// Act
|
||||
WriteCommandAction.runWriteCommandAction(project) {
|
||||
verticalDiffBlock.addNewLine(textToAdd, startLine)
|
||||
}
|
||||
|
||||
// Assert
|
||||
val documentText = editor.document.text
|
||||
|
||||
// We expect a newline to be inserted in addition to our line
|
||||
val expectedText = "$textToAdd\n\n"
|
||||
assertEquals(expectedText, documentText)
|
||||
|
||||
// Check if the new line is highlighted
|
||||
val highlighters = editor.markupModel.allHighlighters.filter { highlighter ->
|
||||
val line = editor.document.getLineNumber(highlighter.startOffset)
|
||||
line == startLine
|
||||
}
|
||||
|
||||
assertTrue("Expected at least one highlighter on the new line", highlighters.isNotEmpty())
|
||||
|
||||
// Check the attributes of the highlighter
|
||||
val expectedTextAttributesKey = TextAttributesKey.find("CONTINUE_DIFF_NEW_LINE")
|
||||
val expectedAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(expectedTextAttributesKey)
|
||||
val highlighterAttributes = highlighters.first().textAttributes
|
||||
|
||||
assertEquals(expectedAttributes.foregroundColor, highlighterAttributes?.foregroundColor)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package com.github.continuedev.continueintellijextension.e2e
|
||||
|
||||
import com.automation.remarks.junit5.Video
|
||||
import com.github.continuedev.continueintellijextension.pages.dialog
|
||||
import com.github.continuedev.continueintellijextension.pages.idea
|
||||
import com.github.continuedev.continueintellijextension.pages.welcomeFrame
|
||||
import com.github.continuedev.continueintellijextension.utils.RemoteRobotExtension
|
||||
import com.github.continuedev.continueintellijextension.utils.StepsLogger
|
||||
import com.github.continuedev.continueintellijextension.utils.getMetaKey
|
||||
import com.intellij.remoterobot.RemoteRobot
|
||||
import com.intellij.remoterobot.fixtures.ComponentFixture
|
||||
import com.intellij.remoterobot.fixtures.JCefBrowserFixture
|
||||
import com.intellij.remoterobot.search.locators.Locator
|
||||
import com.intellij.remoterobot.search.locators.byXpath
|
||||
import com.intellij.remoterobot.steps.CommonSteps
|
||||
import com.intellij.remoterobot.utils.keyboard
|
||||
import com.intellij.remoterobot.utils.waitFor
|
||||
import com.intellij.remoterobot.utils.waitForIgnoringError
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import java.awt.event.KeyEvent.*
|
||||
import java.time.Duration.ofMinutes
|
||||
import java.time.Duration.ofSeconds
|
||||
|
||||
@ExtendWith(RemoteRobotExtension::class)
|
||||
class TutorialTest {
|
||||
init {
|
||||
StepsLogger.init()
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun waitForIde(remoteRobot: RemoteRobot) {
|
||||
waitForIgnoringError(ofMinutes(3)) { remoteRobot.callJs("true") }
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun closeProject(remoteRobot: RemoteRobot) = CommonSteps(remoteRobot).closeProject()
|
||||
|
||||
@Test
|
||||
@Video
|
||||
fun completeTutorial(remoteRobot: RemoteRobot) = with(remoteRobot) {
|
||||
welcomeFrame {
|
||||
createNewProjectLink.click()
|
||||
dialog("New Project") {
|
||||
findText("Java").click()
|
||||
checkBox("Add sample code").select()
|
||||
button("Create").click()
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the default "Main.java" tab to load
|
||||
// Our "continue_tutorial.java" tab loads first, but then "Main.java" takes focus.
|
||||
// So we need to wait for that to occur, and then focus on "continue_tutorial.java"
|
||||
waitFor(ofSeconds(20)) {
|
||||
findAll<ComponentFixture>(
|
||||
byXpath("//div[@accessiblename='Main.java' and @class='EditorTabLabel']")
|
||||
).isNotEmpty()
|
||||
}
|
||||
|
||||
val tutorialEditorTabLocator: Locator =
|
||||
byXpath("//div[@accessiblename='continue_tutorial.java' and @class='EditorTabLabel']")
|
||||
val tutorialEditorTab: ComponentFixture =
|
||||
remoteRobot.find(ComponentFixture::class.java, tutorialEditorTabLocator)
|
||||
tutorialEditorTab.click()
|
||||
|
||||
// Manually open the webview
|
||||
find<ComponentFixture>(byXpath("//div[@text='Continue']")).click()
|
||||
|
||||
// Arbitrary sleep while we wait for the webview to load
|
||||
Thread.sleep(10000)
|
||||
|
||||
val textToInsert = "Hello world!"
|
||||
|
||||
idea {
|
||||
with(textEditor()) {
|
||||
editor.insertTextAtLine(0, 0, textToInsert)
|
||||
editor.selectText(textToInsert)
|
||||
keyboard {
|
||||
hotKey(getMetaKey(), VK_J)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: locator needs to be OS aware
|
||||
// https://github.com/JetBrains/intellij-ui-test-robot/blob/139a05eb99e9a49f13605626b81ad9864be23c96/remote-fixtures/src/main/kotlin/com/intellij/remoterobot/fixtures/CommonContainerFixture.kt#L203
|
||||
val jcefBrowser = find<JCefBrowserFixture>(JCefBrowserFixture.macLocator)
|
||||
assert(jcefBrowser.getDom().isNotEmpty()) { "JCEF browser not found or empty" }
|
||||
|
||||
val codeSnippetText = jcefBrowser.findElementByContainsText(textToInsert)
|
||||
assert(codeSnippetText.html.isNotEmpty()) { "Failed to find code snippet in webview" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.github.continuedev.continueintellijextension.pages
|
||||
|
||||
/*
|
||||
* Source: https://github.com/JetBrains/intellij-ui-test-robot/blob/139a05eb99e9a49f13605626b81ad9864be23c96/ui-test-example/src/test/kotlin/org/intellij/examples/simple/plugin/pages/DialogFixture.kt
|
||||
*/
|
||||
import com.intellij.remoterobot.RemoteRobot
|
||||
import com.intellij.remoterobot.data.RemoteComponent
|
||||
import com.intellij.remoterobot.fixtures.CommonContainerFixture
|
||||
import com.intellij.remoterobot.fixtures.ContainerFixture
|
||||
import com.intellij.remoterobot.fixtures.FixtureName
|
||||
import com.intellij.remoterobot.search.locators.byXpath
|
||||
import com.intellij.remoterobot.stepsProcessing.step
|
||||
import java.time.Duration
|
||||
|
||||
fun ContainerFixture.dialog(
|
||||
title: String,
|
||||
timeout: Duration = Duration.ofSeconds(30),
|
||||
function: DialogFixture.() -> Unit = {}
|
||||
): DialogFixture = step("Search for dialog with title $title") {
|
||||
find<DialogFixture>(DialogFixture.byTitle(title), timeout).apply(function)
|
||||
}
|
||||
|
||||
@FixtureName("Dialog")
|
||||
class DialogFixture(
|
||||
remoteRobot: RemoteRobot,
|
||||
remoteComponent: RemoteComponent
|
||||
) : CommonContainerFixture(remoteRobot, remoteComponent) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']")
|
||||
}
|
||||
|
||||
val title: String
|
||||
get() = callJs("component.getTitle();")
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package com.github.continuedev.continueintellijextension.pages
|
||||
|
||||
/*
|
||||
* Source: https://github.com/JetBrains/intellij-ui-test-robot/blob/139a05eb99e9a49f13605626b81ad9864be23c96/ui-test-example/src/test/kotlin/org/intellij/examples/simple/plugin/pages/IdeaFrame.kt
|
||||
*/
|
||||
import com.intellij.remoterobot.RemoteRobot
|
||||
import com.intellij.remoterobot.data.RemoteComponent
|
||||
import com.intellij.remoterobot.fixtures.*
|
||||
import com.intellij.remoterobot.search.locators.byXpath
|
||||
import com.intellij.remoterobot.stepsProcessing.step
|
||||
import com.intellij.remoterobot.utils.waitFor
|
||||
import java.time.Duration
|
||||
|
||||
fun RemoteRobot.idea(function: IdeaFrame.() -> Unit) {
|
||||
find<IdeaFrame>(timeout = Duration.ofSeconds(10)).apply(function)
|
||||
}
|
||||
|
||||
@FixtureName("Idea frame")
|
||||
@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']")
|
||||
class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) :
|
||||
CommonContainerFixture(remoteRobot, remoteComponent) {
|
||||
|
||||
val projectViewTree
|
||||
get() = find<ContainerFixture>(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']"))
|
||||
|
||||
val projectName
|
||||
get() = step("Get project name") { return@step callJs<String>("component.getProject().getName()") }
|
||||
|
||||
val menuBar: JMenuBarFixture
|
||||
get() = step("Menu...") {
|
||||
return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType())
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) {
|
||||
step("Wait for smart mode") {
|
||||
waitFor(duration = timeout, interval = Duration.ofSeconds(5)) {
|
||||
runCatching { isDumbMode().not() }.getOrDefault(false)
|
||||
}
|
||||
function()
|
||||
step("..wait for smart mode again") {
|
||||
waitFor(duration = timeout, interval = Duration.ofSeconds(5)) {
|
||||
isDumbMode().not()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDumbMode(): Boolean {
|
||||
return callJs(
|
||||
"""
|
||||
const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component)
|
||||
if (frameHelper) {
|
||||
const project = frameHelper.getProject()
|
||||
project ? com.intellij.openapi.project.DumbService.isDumb(project) : true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
""", true
|
||||
)
|
||||
}
|
||||
|
||||
fun openFile(path: String) {
|
||||
runJs(
|
||||
"""
|
||||
importPackage(com.intellij.openapi.fileEditor)
|
||||
importPackage(com.intellij.openapi.vfs)
|
||||
importPackage(com.intellij.openapi.wm.impl)
|
||||
importClass(com.intellij.openapi.application.ApplicationManager)
|
||||
|
||||
const path = '$path'
|
||||
const frameHelper = ProjectFrameHelper.getFrameHelper(component)
|
||||
if (frameHelper) {
|
||||
const project = frameHelper.getProject()
|
||||
const projectPath = project.getBasePath()
|
||||
const file = LocalFileSystem.getInstance().findFileByPath(projectPath + '/' + path)
|
||||
const openFileFunction = new Runnable({
|
||||
run: function() {
|
||||
FileEditorManager.getInstance(project).openTextEditor(
|
||||
new OpenFileDescriptor(
|
||||
project,
|
||||
file
|
||||
), true
|
||||
)
|
||||
}
|
||||
})
|
||||
ApplicationManager.getApplication().invokeLater(openFileFunction)
|
||||
}
|
||||
""", true
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.github.continuedev.continueintellijextension.pages
|
||||
|
||||
/*
|
||||
* Source: https://github.com/JetBrains/intellij-ui-test-robot/blob/139a05eb99e9a49f13605626b81ad9864be23c96/ui-test-example/src/test/kotlin/org/intellij/examples/simple/plugin/pages/WelcomeFrame.kt
|
||||
*/
|
||||
import com.intellij.remoterobot.RemoteRobot
|
||||
import com.intellij.remoterobot.data.RemoteComponent
|
||||
import com.intellij.remoterobot.fixtures.*
|
||||
import com.intellij.remoterobot.search.locators.byXpath
|
||||
import java.time.Duration
|
||||
|
||||
fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) {
|
||||
find(WelcomeFrame::class.java, Duration.ofSeconds(20)).apply(function)
|
||||
}
|
||||
|
||||
@FixtureName("Welcome Frame")
|
||||
@DefaultXpath("type", "//div[@class='FlatWelcomeFrame']")
|
||||
class WelcomeFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) :
|
||||
CommonContainerFixture(remoteRobot, remoteComponent) {
|
||||
val createNewProjectLink
|
||||
get() = actionLink(
|
||||
byXpath(
|
||||
"New Project",
|
||||
"//div[(@class='MainButton' and @text='New Project') or (@accessiblename='New Project' and @class='JButton')]"
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package com.github.continuedev.continueintellijextension.utils
|
||||
|
||||
/**
|
||||
* Source: https://github.com/JetBrains/intellij-ui-test-robot/blob/139a05eb99e9a49f13605626b81ad9864be23c96/ui-test-example/src/test/kotlin/org/intellij/examples/simple/plugin/utils/RemoteRobotExtension.kt
|
||||
*/
|
||||
import com.intellij.remoterobot.RemoteRobot
|
||||
import com.intellij.remoterobot.fixtures.ContainerFixture
|
||||
import com.intellij.remoterobot.search.locators.byXpath
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
import org.junit.jupiter.api.extension.ParameterContext
|
||||
import org.junit.jupiter.api.extension.ParameterResolver
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.reflect.Method
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
class RemoteRobotExtension : AfterTestExecutionCallback, ParameterResolver {
|
||||
private val url: String = System.getProperty("remote-robot-url") ?: "http://127.0.0.1:8082"
|
||||
private val remoteRobot: RemoteRobot = if (System.getProperty("debug-retrofit")?.equals("enable") == true) {
|
||||
val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
this.level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
this.addInterceptor(interceptor)
|
||||
}.build()
|
||||
RemoteRobot(url, client)
|
||||
} else {
|
||||
RemoteRobot(url)
|
||||
}
|
||||
private val client = OkHttpClient()
|
||||
|
||||
override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean {
|
||||
return parameterContext?.parameter?.type?.equals(RemoteRobot::class.java) ?: false
|
||||
}
|
||||
|
||||
override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any {
|
||||
return remoteRobot
|
||||
}
|
||||
|
||||
override fun afterTestExecution(context: ExtensionContext?) {
|
||||
val testMethod: Method = context?.requiredTestMethod ?: throw IllegalStateException("test method is null")
|
||||
val testMethodName = testMethod.name
|
||||
val testFailed: Boolean = context.executionException?.isPresent ?: false
|
||||
if (testFailed) {
|
||||
// saveScreenshot(testMethodName)
|
||||
saveIdeaFrames(testMethodName)
|
||||
saveHierarchy(testMethodName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveScreenshot(testName: String) {
|
||||
fetchScreenShot().save(testName)
|
||||
}
|
||||
|
||||
private fun saveHierarchy(testName: String) {
|
||||
val hierarchySnapshot =
|
||||
saveFile(url, "build/reports", "hierarchy-$testName.html")
|
||||
if (File("build/reports/styles.css").exists().not()) {
|
||||
saveFile("$url/styles.css", "build/reports", "styles.css")
|
||||
}
|
||||
println("Hierarchy snapshot: ${hierarchySnapshot.absolutePath}")
|
||||
}
|
||||
|
||||
private fun saveFile(url: String, folder: String, name: String): File {
|
||||
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||
return File(folder).apply {
|
||||
mkdirs()
|
||||
}.resolve(name).apply {
|
||||
writeText(response.body?.string() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun BufferedImage.save(name: String) {
|
||||
val bytes = ByteArrayOutputStream().use { b ->
|
||||
ImageIO.write(this, "png", b)
|
||||
b.toByteArray()
|
||||
}
|
||||
File("build/reports").apply { mkdirs() }.resolve("$name.png").writeBytes(bytes)
|
||||
}
|
||||
|
||||
private fun saveIdeaFrames(testName: String) {
|
||||
remoteRobot.findAll<ContainerFixture>(byXpath("//div[@class='IdeFrameImpl']")).forEachIndexed { n, frame ->
|
||||
val pic = try {
|
||||
frame.callJs<ByteArray>(
|
||||
"""
|
||||
importPackage(java.io)
|
||||
importPackage(javax.imageio)
|
||||
importPackage(java.awt.image)
|
||||
const screenShot = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
component.paint(screenShot.getGraphics())
|
||||
let pictureBytes;
|
||||
const baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
ImageIO.write(screenShot, "png", baos);
|
||||
pictureBytes = baos.toByteArray();
|
||||
} finally {
|
||||
baos.close();
|
||||
}
|
||||
pictureBytes;
|
||||
""", true
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
pic.inputStream().use {
|
||||
ImageIO.read(it)
|
||||
}.save(testName + "_" + n)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchScreenShot(): BufferedImage {
|
||||
return remoteRobot.callJs<ByteArray>(
|
||||
"""
|
||||
importPackage(java.io)
|
||||
importPackage(javax.imageio)
|
||||
const screenShot = new java.awt.Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
|
||||
let pictureBytes;
|
||||
const baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
ImageIO.write(screenShot, "png", baos);
|
||||
pictureBytes = baos.toByteArray();
|
||||
} finally {
|
||||
baos.close();
|
||||
}
|
||||
pictureBytes;
|
||||
"""
|
||||
).inputStream().use {
|
||||
ImageIO.read(it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.github.continuedev.continueintellijextension.utils
|
||||
|
||||
/**
|
||||
* Source: https://github.com/JetBrains/intellij-ui-test-robot/blob/139a05eb99e9a49f13605626b81ad9864be23c96/ui-test-example/src/test/kotlin/org/intellij/examples/simple/plugin/utils/StepsLogger.kt
|
||||
*/
|
||||
import com.intellij.remoterobot.stepsProcessing.StepLogger
|
||||
import com.intellij.remoterobot.stepsProcessing.StepWorker
|
||||
|
||||
object StepsLogger {
|
||||
private var initialized = false
|
||||
|
||||
@JvmStatic
|
||||
fun init() {
|
||||
if (initialized.not()) {
|
||||
StepWorker.registerProcessor(StepLogger())
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
import * as path from "node:path";
|
||||
|
||||
import * as vscode from "vscode";
|
||||
|
||||
import { uriFromFilePath } from "./util/vscode";
|
||||
|
||||
export function showAnswerInTextEditor(
|
||||
filename: string,
|
||||
range: vscode.Range,
|
||||
answer: string,
|
||||
) {
|
||||
vscode.workspace.openTextDocument(uriFromFilePath(filename)).then((doc) => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open file, reveal range, show decoration
|
||||
vscode.window.showTextDocument(doc).then((new_editor) => {
|
||||
new_editor.revealRange(
|
||||
new vscode.Range(range.end, range.end),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
);
|
||||
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
after: {
|
||||
contentText: `${answer}\n`,
|
||||
color: "rgb(0, 255, 0, 0.8)",
|
||||
},
|
||||
backgroundColor: "rgb(0, 255, 0, 0.2)",
|
||||
});
|
||||
new_editor.setDecorations(decorationType, [range]);
|
||||
vscode.window.showInformationMessage("Answer found!");
|
||||
|
||||
// Remove decoration when user moves cursor
|
||||
vscode.window.onDidChangeTextEditorSelection((e) => {
|
||||
if (
|
||||
e.textEditor === new_editor &&
|
||||
e.selections[0].active.line !== range.end.line
|
||||
) {
|
||||
new_editor.setDecorations(decorationType, []);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type DecorationKey = {
|
||||
editorUri: string;
|
||||
options: vscode.DecorationOptions;
|
||||
decorationType: vscode.TextEditorDecorationType;
|
||||
};
|
||||
|
||||
class DecorationManager {
|
||||
private editorToDecorations = new Map<
|
||||
string,
|
||||
Map<vscode.TextEditorDecorationType, vscode.DecorationOptions[]>
|
||||
>();
|
||||
|
||||
constructor() {
|
||||
vscode.window.onDidChangeVisibleTextEditors((editors) => {
|
||||
for (const editor of editors) {
|
||||
if (editor.document.isClosed) {
|
||||
this.editorToDecorations.delete(editor.document.uri.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private rerenderDecorations(
|
||||
editorUri: string,
|
||||
decorationType: vscode.TextEditorDecorationType,
|
||||
) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decorationTypes = this.editorToDecorations.get(editorUri);
|
||||
if (!decorationTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decorations = decorationTypes.get(decorationType);
|
||||
if (!decorations) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.setDecorations(decorationType, decorations);
|
||||
}
|
||||
|
||||
addDecoration(key: DecorationKey) {
|
||||
let decorationTypes = this.editorToDecorations.get(key.editorUri);
|
||||
if (!decorationTypes) {
|
||||
decorationTypes = new Map();
|
||||
decorationTypes.set(key.decorationType, [key.options]);
|
||||
this.editorToDecorations.set(key.editorUri, decorationTypes);
|
||||
} else {
|
||||
const decorations = decorationTypes.get(key.decorationType);
|
||||
if (!decorations) {
|
||||
decorationTypes.set(key.decorationType, [key.options]);
|
||||
} else {
|
||||
decorations.push(key.options);
|
||||
}
|
||||
}
|
||||
|
||||
this.rerenderDecorations(key.editorUri, key.decorationType);
|
||||
|
||||
vscode.window.onDidChangeTextEditorSelection((event) => {
|
||||
if (event.textEditor.document.fileName === key.editorUri) {
|
||||
this.deleteAllDecorations(key.editorUri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDecoration(key: DecorationKey) {
|
||||
const decorationTypes = this.editorToDecorations.get(key.editorUri);
|
||||
if (!decorationTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
let decorations = decorationTypes?.get(key.decorationType);
|
||||
if (!decorations) {
|
||||
return;
|
||||
}
|
||||
|
||||
decorations = decorations.filter((decOpts) => decOpts !== key.options);
|
||||
decorationTypes.set(key.decorationType, decorations);
|
||||
this.rerenderDecorations(key.editorUri, key.decorationType);
|
||||
}
|
||||
|
||||
deleteAllDecorations(editorUri: string) {
|
||||
const decorationTypes = this.editorToDecorations.get(editorUri)?.keys();
|
||||
if (!decorationTypes) {
|
||||
return;
|
||||
}
|
||||
this.editorToDecorations.delete(editorUri);
|
||||
for (const decorationType of decorationTypes) {
|
||||
this.rerenderDecorations(editorUri, decorationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const decorationManager = new DecorationManager();
|
||||
|
||||
function constructBaseKey(
|
||||
editor: vscode.TextEditor,
|
||||
lineno: number,
|
||||
decorationType?: vscode.TextEditorDecorationType,
|
||||
): DecorationKey {
|
||||
return {
|
||||
editorUri: editor.document.uri.toString(),
|
||||
options: {
|
||||
range: new vscode.Range(lineno, 0, lineno, 0),
|
||||
},
|
||||
decorationType:
|
||||
decorationType || vscode.window.createTextEditorDecorationType({}),
|
||||
};
|
||||
}
|
||||
|
||||
export function showLintMessage(
|
||||
editor: vscode.TextEditor,
|
||||
lineno: number,
|
||||
msg: string,
|
||||
): DecorationKey {
|
||||
const key = constructBaseKey(editor, lineno);
|
||||
key.decorationType = vscode.window.createTextEditorDecorationType({
|
||||
after: {
|
||||
contentText: "Linting error",
|
||||
color: "rgb(255, 0, 0, 0.6)",
|
||||
},
|
||||
gutterIconPath: vscode.Uri.file(
|
||||
path.join(__dirname, "..", "media", "error.png"),
|
||||
),
|
||||
gutterIconSize: "contain",
|
||||
});
|
||||
key.options.hoverMessage = msg;
|
||||
decorationManager.addDecoration(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
export function highlightCode(
|
||||
editor: vscode.TextEditor,
|
||||
range: vscode.Range,
|
||||
removeOnClick = true,
|
||||
): DecorationKey {
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: "rgb(255, 255, 0, 0.1)",
|
||||
});
|
||||
const key = {
|
||||
editorUri: editor.document.uri.toString(),
|
||||
options: {
|
||||
range,
|
||||
},
|
||||
decorationType,
|
||||
};
|
||||
decorationManager.addDecoration(key);
|
||||
|
||||
if (removeOnClick) {
|
||||
vscode.window.onDidChangeTextEditorSelection((e) => {
|
||||
if (e.textEditor === editor) {
|
||||
decorationManager.deleteDecoration(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
|
@ -84,47 +84,6 @@ export class VsCodeIdeUtils {
|
|||
// ------------------------------------ //
|
||||
// On message handlers
|
||||
|
||||
private _lastDecorationType: vscode.TextEditorDecorationType | null = null;
|
||||
async highlightCode(rangeInFile: RangeInFile, color: string) {
|
||||
const range = new vscode.Range(
|
||||
rangeInFile.range.start.line,
|
||||
rangeInFile.range.start.character,
|
||||
rangeInFile.range.end.line,
|
||||
rangeInFile.range.end.character,
|
||||
);
|
||||
const editor = await openEditorAndRevealRange(
|
||||
rangeInFile.filepath,
|
||||
range,
|
||||
vscode.ViewColumn.One,
|
||||
);
|
||||
if (editor) {
|
||||
const decorationType = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: color,
|
||||
isWholeLine: true,
|
||||
});
|
||||
editor.setDecorations(decorationType, [range]);
|
||||
|
||||
const cursorDisposable = vscode.window.onDidChangeTextEditorSelection(
|
||||
(event) => {
|
||||
if (event.textEditor.document.uri.fsPath === rangeInFile.filepath) {
|
||||
cursorDisposable.dispose();
|
||||
editor.setDecorations(decorationType, []);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
cursorDisposable.dispose();
|
||||
editor.setDecorations(decorationType, []);
|
||||
}, 2500);
|
||||
|
||||
if (this._lastDecorationType) {
|
||||
editor.setDecorations(this._lastDecorationType, []);
|
||||
}
|
||||
this._lastDecorationType = decorationType;
|
||||
}
|
||||
}
|
||||
|
||||
showSuggestion(edit: FileEdit) {
|
||||
// showSuggestion already exists
|
||||
showSuggestionInEditor(
|
||||
|
@ -250,14 +209,6 @@ export class VsCodeIdeUtils {
|
|||
.map((uri) => uri.fsPath);
|
||||
}
|
||||
|
||||
getVisibleFiles(): string[] {
|
||||
return vscode.window.visibleTextEditors
|
||||
.filter((editor) => this.documentIsCode(editor.document.uri))
|
||||
.map((editor) => {
|
||||
return editor.document.uri.fsPath;
|
||||
});
|
||||
}
|
||||
|
||||
saveFile(filepath: string) {
|
||||
vscode.window.visibleTextEditors
|
||||
.filter((editor) => this.documentIsCode(editor.document.uri))
|
||||
|
|
|
@ -133,11 +133,15 @@ function InputToolbar(props: InputToolbarProps) {
|
|||
/>
|
||||
<HoverItem>
|
||||
<PhotoIcon
|
||||
className="h-4 w-4"
|
||||
className="h-4 w-4 hover:brightness-125"
|
||||
data-tooltip-id="image-tooltip"
|
||||
onClick={(e) => {
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
/>
|
||||
<ToolTip id="image-tooltip" place="top-middle">
|
||||
Attach an image
|
||||
</ToolTip>
|
||||
</HoverItem>
|
||||
</>
|
||||
))}
|
||||
|
@ -145,10 +149,10 @@ function InputToolbar(props: InputToolbarProps) {
|
|||
<HoverItem onClick={props.onAddContextItem}>
|
||||
<AtSymbolIcon
|
||||
data-tooltip-id="add-context-item-tooltip"
|
||||
className="h-4 w-4"
|
||||
className="h-4 w-4 hover:brightness-125"
|
||||
/>
|
||||
|
||||
<ToolTip id="add-context-item-tooltip" place="top-start">
|
||||
<ToolTip id="add-context-item-tooltip" place="top-middle">
|
||||
Add context (files, docs, urls, etc.)
|
||||
</ToolTip>
|
||||
</HoverItem>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
EllipsisHorizontalCircleIcon as EllipsisHorizontalIcon,
|
||||
WrenchScrewdriverIcon as WrenchScrewdriverIconOutline,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { modelSupportsTools } from "core/llm/autodetect";
|
||||
import { WrenchScrewdriverIcon as WrenchScrewdriverIconSolid } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
|
@ -15,33 +14,22 @@ import InfoHover from "../../InfoHover";
|
|||
import HoverItem from "./HoverItem";
|
||||
import ToolDropdownItem from "./ToolDropdownItem";
|
||||
import { useAppSelector } from "../../../redux/hooks";
|
||||
import { selectDefaultModel } from "../../../redux/slices/configSlice";
|
||||
|
||||
const BackgroundDiv = styled.div<{ useTools: boolean }>`
|
||||
background-color: ${(props) =>
|
||||
props.useTools ? `${lightGray}33` : "transparent"};
|
||||
border-radius: ${defaultBorderRadius};
|
||||
padding: 1px;
|
||||
|
||||
font-size: ${getFontSize() - 4}px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
transition: background-color 200ms;
|
||||
`;
|
||||
import { ToolTip } from "../../gui/Tooltip";
|
||||
|
||||
export default function ToolDropdown() {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const dispatch = useDispatch();
|
||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const useTools = useAppSelector((state) => state.ui.useTools);
|
||||
const availableTools = useAppSelector((state) => state.config.config.tools);
|
||||
const [showAbove, setShowAbove] = useState(false);
|
||||
|
||||
const ToolsIcon = useTools
|
||||
? WrenchScrewdriverIconSolid
|
||||
: WrenchScrewdriverIconOutline;
|
||||
|
||||
useEffect(() => {
|
||||
const checkPosition = () => {
|
||||
if (buttonRef.current) {
|
||||
|
@ -59,95 +47,120 @@ export default function ToolDropdown() {
|
|||
|
||||
return (
|
||||
<HoverItem onClick={() => dispatch(toggleUseTools())}>
|
||||
<BackgroundDiv useTools={useTools}>
|
||||
<WrenchScrewdriverIcon
|
||||
<div
|
||||
data-tooltip-id="tools-tooltip"
|
||||
className={`-ml-1 -mt-1 flex flex-row items-center gap-1.5 rounded-md px-1 py-0.5 text-xs ${
|
||||
useTools || isHovered ? "bg-lightgray/30" : ""
|
||||
}`}
|
||||
>
|
||||
<ToolsIcon
|
||||
className="h-4 w-4 text-gray-400"
|
||||
style={{
|
||||
color: useTools && vscForeground,
|
||||
padding: "1px",
|
||||
transition: "background-color 200ms",
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
/>
|
||||
{!useTools && (
|
||||
<ToolTip id="tools-tooltip" place="top-middle">
|
||||
Enable tool usage
|
||||
</ToolTip>
|
||||
)}
|
||||
|
||||
{useTools && (
|
||||
<>
|
||||
<span className="hidden md:flex">Tools</span>
|
||||
<span className="hidden align-top sm:flex">Tools</span>
|
||||
|
||||
<div className="relative">
|
||||
<Listbox onChange={() => {}}>
|
||||
<Listbox.Button
|
||||
ref={buttonRef}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDropdownOpen((prev) => !prev);
|
||||
}}
|
||||
className="text-lightgray flex cursor-pointer items-center border-none bg-transparent outline-none"
|
||||
>
|
||||
{isDropdownOpen ? (
|
||||
<ChevronUpIcon className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
)}
|
||||
</Listbox.Button>
|
||||
<Transition show={isDropdownOpen}>
|
||||
<Listbox.Options
|
||||
className={`bg-vsc-editor-background border-lightgray/50 absolute -left-32 z-50 mb-1 min-w-fit whitespace-nowrap rounded-md border border-solid px-1 py-0 shadow-lg ${showAbove ? "bottom-full" : ""}`}
|
||||
>
|
||||
<div className="sticky">
|
||||
<div
|
||||
className="mb-1 flex items-center gap-2 px-2 py-1"
|
||||
style={{
|
||||
color: vscForeground,
|
||||
borderBottom: `1px solid ${lightGray}`,
|
||||
}}
|
||||
<Listbox
|
||||
value={null}
|
||||
onChange={() => {}}
|
||||
as="div"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Button
|
||||
ref={buttonRef}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDropdownOpen(!isDropdownOpen);
|
||||
}}
|
||||
className="text-lightgray flex cursor-pointer items-center border-none bg-transparent px-0 outline-none"
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-3 w-3 cursor-pointer hover:brightness-125" />
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
afterLeave={() => setDropdownOpen(false)}
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`bg-vsc-editor-background border-lightgray/50 absolute -left-32 z-50 mb-1 min-w-fit whitespace-nowrap rounded-md border border-solid px-1 py-0 shadow-lg ${showAbove ? "bottom-full" : ""}`}
|
||||
static
|
||||
>
|
||||
Tool policies{" "}
|
||||
<InfoHover
|
||||
id={"tool-policies"}
|
||||
size={"3"}
|
||||
msg={
|
||||
<div
|
||||
className="gap-0 *:m-1 *:text-left"
|
||||
style={{ fontSize: "10px" }}
|
||||
<div className="sticky">
|
||||
<div
|
||||
className="mb-1 flex items-center gap-2 px-2 py-1 text-xs"
|
||||
style={{
|
||||
color: vscForeground,
|
||||
borderBottom: `1px solid ${lightGray}`,
|
||||
}}
|
||||
>
|
||||
Tool policies{" "}
|
||||
<InfoHover
|
||||
id={"tool-policies"}
|
||||
size={"3"}
|
||||
msg={
|
||||
<div
|
||||
className="gap-0 *:m-1 *:text-left"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
<p>
|
||||
<span className="text-green-500">
|
||||
Automatic:
|
||||
</span>{" "}
|
||||
Can be used without asking
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-yellow-500">
|
||||
Allowed:
|
||||
</span>{" "}
|
||||
Will ask before using
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-red-500">
|
||||
Disabled:
|
||||
</span>{" "}
|
||||
Cannot be used
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-48 overflow-y-auto overflow-x-hidden">
|
||||
{availableTools.map((tool) => (
|
||||
<Listbox.Option
|
||||
key={tool.function.name}
|
||||
value="addAllFiles"
|
||||
className="text-vsc-foreground block w-full cursor-pointer text-left text-xs brightness-75 hover:brightness-125"
|
||||
>
|
||||
<p>
|
||||
<span className="text-green-500">
|
||||
Automatic:
|
||||
</span>{" "}
|
||||
Can be used without asking
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-yellow-500">
|
||||
Allowed:
|
||||
</span>{" "}
|
||||
Will ask before using
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-red-500">Disabled:</span>{" "}
|
||||
Cannot be used
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-48 overflow-y-auto overflow-x-hidden">
|
||||
{availableTools.map((tool) => (
|
||||
<Listbox.Option
|
||||
key={tool.function.name}
|
||||
value="addAllFiles"
|
||||
className="text-vsc-foreground block w-full cursor-pointer text-left text-[10px] brightness-75 hover:brightness-125"
|
||||
>
|
||||
<ToolDropdownItem tool={tool} />
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
<ToolDropdownItem tool={tool} />
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</BackgroundDiv>
|
||||
</div>
|
||||
</HoverItem>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ function ToolDropdownItem(props: ToolDropdownItemProps) {
|
|||
msg={props.tool.function.description}
|
||||
/> */}
|
||||
</span>
|
||||
<div className="flex gap-2 pr-4">
|
||||
<div className="flex cursor-pointer gap-2 pr-4">
|
||||
{(settings === "allowedWithPermission" || settings === undefined) && (
|
||||
<span className="text-yellow-500">Allowed</span>
|
||||
)}
|
||||
|
|
|
@ -167,7 +167,7 @@ export default function StepContainerPreToolbar(
|
|||
<div className="flex min-w-0 max-w-[45%] items-center">
|
||||
<ChevronDownIcon
|
||||
onClick={onClickExpand}
|
||||
className={`h-3.5 w-3.5 shrink-0 cursor-pointer text-gray-400 transition-colors hover:bg-white/10 ${
|
||||
className={`h-3.5 w-3.5 shrink-0 cursor-pointer text-gray-400 hover:brightness-125 ${
|
||||
isExpanded ? "rotate-0" : "-rotate-90"
|
||||
}`}
|
||||
/>
|
||||
|
|
|
@ -65,7 +65,11 @@ export class IdeMessenger implements IIdeMessenger {
|
|||
);
|
||||
}
|
||||
|
||||
private _postToIde(messageType: string, data: any, messageId?: string) {
|
||||
private _postToIde(
|
||||
messageType: string,
|
||||
data: any,
|
||||
messageId: string = uuidv4(),
|
||||
) {
|
||||
if (typeof vscode === "undefined") {
|
||||
if (isJetBrains()) {
|
||||
if (window.postIntellijMessage === undefined) {
|
||||
|
@ -76,22 +80,24 @@ export class IdeMessenger implements IIdeMessenger {
|
|||
);
|
||||
throw new Error("postIntellijMessage is undefined");
|
||||
}
|
||||
window.postIntellijMessage?.(messageType, data, messageId ?? uuidv4());
|
||||
window.postIntellijMessage?.(messageType, data, messageId);
|
||||
return;
|
||||
} else {
|
||||
console.log(
|
||||
"Unable to send message: vscode is undefined. ",
|
||||
"Unable to send message: vscode is undefined",
|
||||
messageType,
|
||||
data,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const msg: Message = {
|
||||
messageId: messageId ?? uuidv4(),
|
||||
messageId,
|
||||
messageType,
|
||||
data,
|
||||
};
|
||||
|
||||
vscode.postMessage(msg);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { defaultBorderRadius } from "../components";
|
||||
import TipTapEditor from "../components/mainInput/TipTapEditor";
|
||||
import { IdeMessengerContext } from "../context/IdeMessenger";
|
||||
import useSetup from "../hooks/useSetup";
|
||||
import { selectSlashCommandComboBoxInputs } from "../redux/selectors";
|
||||
import { useAppSelector } from "../redux/hooks";
|
||||
|
@ -28,22 +26,6 @@ function EditorInset() {
|
|||
|
||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const ideMessenger = useContext(IdeMessengerContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elementRef.current) return;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (!elementRef.current) return;
|
||||
|
||||
console.log("Height: ", elementRef.current.clientHeight);
|
||||
ideMessenger.post("jetbrains/editorInsetHeight", {
|
||||
height: elementRef.current.clientHeight,
|
||||
});
|
||||
});
|
||||
resizeObserver.observe(elementRef.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditorInsetDiv ref={elementRef}>
|
||||
<TipTapEditor
|
||||
|
@ -54,7 +36,7 @@ function EditorInset() {
|
|||
console.log("Enter: ", e, modifiers);
|
||||
}}
|
||||
historyKey="chat"
|
||||
></TipTapEditor>
|
||||
/>
|
||||
</EditorInsetDiv>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
addContextItemsAtIndex,
|
||||
} from "../redux/slices/sessionSlice";
|
||||
import { setTTSActive } from "../redux/slices/uiSlice";
|
||||
import useUpdatingRef from "./useUpdatingRef";
|
||||
import { updateFileSymbolsFromHistory } from "../redux/thunks/updateFileSymbols";
|
||||
import { refreshSessionMetadata } from "../redux/thunks/session";
|
||||
|
||||
|
@ -64,7 +63,7 @@ function useSetup() {
|
|||
// Init to run on initial config load
|
||||
ideMessenger.post("docs/getSuggestedDocs", undefined);
|
||||
ideMessenger.post("docs/initStatuses", undefined);
|
||||
dispatch(updateFileSymbolsFromHistory())
|
||||
dispatch(updateFileSymbolsFromHistory());
|
||||
dispatch(refreshSessionMetadata({}));
|
||||
|
||||
// This triggers sending pending status to the GUI for relevant docs indexes
|
||||
|
@ -98,21 +97,29 @@ function useSetup() {
|
|||
// Override persisted state
|
||||
dispatch(setInactive());
|
||||
|
||||
// Tell JetBrains the webview is ready
|
||||
ideMessenger.request("onLoad", undefined).then((result) => {
|
||||
if (result.status === "error") {
|
||||
return;
|
||||
}
|
||||
const msg = result.content;
|
||||
(window as any).windowId = msg.windowId;
|
||||
(window as any).serverUrl = msg.serverUrl;
|
||||
(window as any).workspacePaths = msg.workspacePaths;
|
||||
(window as any).vscMachineId = msg.vscMachineId;
|
||||
(window as any).vscMediaUrl = msg.vscMediaUrl;
|
||||
});
|
||||
|
||||
// Save theme colors to local storage for immediate loading in JetBrains
|
||||
if (isJetBrains()) {
|
||||
// Save theme colors to local storage for immediate loading in JetBrains
|
||||
ideMessenger.request("jetbrains/getColors", undefined).then((result) => {
|
||||
Object.keys(result).forEach((key) => {
|
||||
document.body.style.setProperty(key, result[key]);
|
||||
document.documentElement.style.setProperty(key, result[key]);
|
||||
});
|
||||
});
|
||||
|
||||
// Tell JetBrains the webview is ready
|
||||
ideMessenger.request("jetbrains/onLoad", undefined).then((result) => {
|
||||
if (result.status === "error") {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = result.content;
|
||||
(window as any).windowId = msg.windowId;
|
||||
(window as any).serverUrl = msg.serverUrl;
|
||||
(window as any).workspacePaths = msg.workspacePaths;
|
||||
(window as any).vscMachineId = msg.vscMachineId;
|
||||
(window as any).vscMediaUrl = msg.vscMediaUrl;
|
||||
});
|
||||
|
||||
for (const colorVar of VSC_THEME_COLOR_VARS) {
|
||||
if (document.body.style.getPropertyValue(colorVar)) {
|
||||
localStorage.setItem(
|
||||
|
@ -153,13 +160,6 @@ function useSetup() {
|
|||
dispatch(setTTSActive(status));
|
||||
});
|
||||
|
||||
useWebviewListener("setColors", async (colors) => {
|
||||
Object.keys(colors).forEach((key) => {
|
||||
document.body.style.setProperty(key, colors[key]);
|
||||
document.documentElement.style.setProperty(key, colors[key]);
|
||||
});
|
||||
});
|
||||
|
||||
useWebviewListener("configError", async (error) => {
|
||||
dispatch(setConfigError(error));
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue