Squash commits for JB integration testing

This commit is contained in:
Test 2024-12-17 09:12:33 -08:00
parent d2d3c3065a
commit b76af6a3da
58 changed files with 2483 additions and 1964 deletions

View File

@ -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

View File

@ -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>

View File

@ -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"]

View File

@ -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" : "";

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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);

View File

@ -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();

View File

@ -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];

View File

@ -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",

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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").
![run-extension-screenshot](../../media/run-continue-intellij.png)
@ -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/

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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"
}

View File

@ -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)
}
}

View File

@ -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!")
}
}
}
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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>)

View File

@ -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
)

View File

@ -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()

View File

@ -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
}

View File

@ -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)

View File

@ -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, "*");"""
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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"
}

View File

@ -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)
}
}

View File

@ -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" }
}
}

View File

@ -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();")
}

View File

@ -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
)
}
}

View File

@ -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')]"
)
)
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}

View File

@ -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))

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
)}

View File

@ -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"
}`}
/>

View File

@ -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);
}

View File

@ -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>
);
}

View File

@ -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));
});