mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
WIP: JetBrains integration
This commit is contained in:
parent
8fee3127ff
commit
b91425889f
210 changed files with 2076707 additions and 73 deletions
192
.github/workflows/jetbrains-build.yml
vendored
Normal file
192
.github/workflows/jetbrains-build.yml
vendored
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
name: JetBrains Integration Build
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'jetbrains/**'
|
||||
- 'deps/patches/vscode/jetbrains.patch'
|
||||
- '.github/workflows/jetbrains-build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'jetbrains/**'
|
||||
- 'deps/patches/vscode/jetbrains.patch'
|
||||
- '.github/workflows/jetbrains-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.19.2'
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Check JetBrains dependencies
|
||||
run: node jetbrains/scripts/check-dependencies.js
|
||||
|
||||
build-host:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-dependencies
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.19.2'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.8.1
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-jetbrains-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-jetbrains-
|
||||
|
||||
- name: Install host dependencies
|
||||
working-directory: jetbrains/host
|
||||
run: npm install
|
||||
|
||||
- name: Apply VSCode patches
|
||||
working-directory: jetbrains/host
|
||||
run: npm run deps:patch
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build host
|
||||
working-directory: jetbrains/host
|
||||
run: npm run build
|
||||
|
||||
- name: Upload host artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jetbrains-host-build
|
||||
path: jetbrains/host/dist/
|
||||
|
||||
build-plugin:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-dependencies
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Build plugin
|
||||
working-directory: jetbrains/plugin
|
||||
run: ./gradlew buildPlugin
|
||||
|
||||
- name: Run plugin tests
|
||||
working-directory: jetbrains/plugin
|
||||
run: ./gradlew test
|
||||
|
||||
- name: Upload plugin artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jetbrains-plugin-build
|
||||
path: jetbrains/plugin/build/distributions/*.zip
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-host, build-plugin]
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download host artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jetbrains-host-build
|
||||
path: jetbrains/host/dist/
|
||||
|
||||
- name: Download plugin artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jetbrains-plugin-build
|
||||
path: jetbrains/plugin/build/distributions/
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.19.2'
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
echo "Integration tests would run here"
|
||||
# Add actual integration test commands when available
|
||||
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-host, build-plugin]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download host artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jetbrains-host-build
|
||||
path: jetbrains/host/dist/
|
||||
|
||||
- name: Download plugin artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: jetbrains-plugin-build
|
||||
path: jetbrains/plugin/build/distributions/
|
||||
|
||||
- name: Create platform.zip
|
||||
run: |
|
||||
cd jetbrains
|
||||
zip -r platform.zip host/dist plugin/build/distributions
|
||||
echo "Platform package created"
|
||||
|
||||
- name: Upload platform package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jetbrains-platform-package
|
||||
path: jetbrains/platform.zip
|
||||
retention-days: 30
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[submodule "deps/vscode"]
|
||||
path = deps/vscode
|
||||
url = https://github.com/microsoft/vscode.git
|
||||
ignore = dirty
|
||||
1832
deps/patches/vscode/jetbrains.patch
vendored
Normal file
1832
deps/patches/vscode/jetbrains.patch
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
deps/vscode
vendored
Submodule
1
deps/vscode
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 174af221c9ea2ccdb64abe4aab8e1a805e77beae
|
||||
43
jetbrains/.gitignore
vendored
Normal file
43
jetbrains/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# JetBrains integration specific ignores
|
||||
|
||||
# Host build outputs
|
||||
host/dist/
|
||||
host/deps/vscode/
|
||||
host/*.log
|
||||
host/logs/
|
||||
host/.turbo/
|
||||
host/node_modules/
|
||||
|
||||
# Plugin build outputs
|
||||
plugin/build/
|
||||
plugin/.gradle/
|
||||
plugin/out/
|
||||
plugin/.idea/
|
||||
plugin/*.iml
|
||||
plugin/*.ipr
|
||||
plugin/*.iws
|
||||
plugin/local.properties
|
||||
|
||||
# Platform files
|
||||
platform.zip
|
||||
*.vsix
|
||||
|
||||
# Debug and temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# IDE specific
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
1
jetbrains/.pr/kilocode-2129/branch.txt
Normal file
1
jetbrains/.pr/kilocode-2129/branch.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
catrielmuller/jetbrains
|
||||
0
jetbrains/.pr/kilocode-2129/changed_files.txt
Normal file
0
jetbrains/.pr/kilocode-2129/changed_files.txt
Normal file
197
jetbrains/README.md
Normal file
197
jetbrains/README.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# JetBrains Integration for Roo-Code
|
||||
|
||||
This directory contains the JetBrains integration layer that allows Roo-Code to run as a plugin within JetBrains IDEs (IntelliJ IDEA, WebStorm, PyCharm, etc.).
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The JetBrains integration consists of two main components:
|
||||
|
||||
### 1. Host Bridge (`/jetbrains/host/`)
|
||||
|
||||
A Node.js application that:
|
||||
|
||||
- Starts the VSCode extension host process
|
||||
- Provides socket-based IPC communication between JetBrains and the extension
|
||||
- Translates between JetBrains and VSCode APIs via RPC
|
||||
- Manages the extension lifecycle
|
||||
|
||||
### 2. JetBrains Plugin (`/jetbrains/plugin/`)
|
||||
|
||||
A Kotlin/Java plugin that:
|
||||
|
||||
- Integrates with JetBrains IDE APIs
|
||||
- Spawns the Node.js host process
|
||||
- Handles UI integration within the IDE
|
||||
- Maps IDE actions to extension commands
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18.x or higher
|
||||
- Java 17 or higher
|
||||
- JetBrains IDE (IntelliJ IDEA, WebStorm, etc.)
|
||||
- Git with submodule support
|
||||
|
||||
### Building the Host Bridge
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd jetbrains/host
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Apply VSCode patches:
|
||||
|
||||
```bash
|
||||
npm run deps:patch
|
||||
```
|
||||
|
||||
3. Build the host:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Building the JetBrains Plugin
|
||||
|
||||
1. Navigate to the plugin directory:
|
||||
|
||||
```bash
|
||||
cd jetbrains/plugin
|
||||
```
|
||||
|
||||
2. Build the plugin:
|
||||
|
||||
```bash
|
||||
./gradlew buildPlugin
|
||||
```
|
||||
|
||||
3. The plugin will be available in `build/distributions/`
|
||||
|
||||
## Development
|
||||
|
||||
### Running in Development Mode
|
||||
|
||||
1. Start the host in development mode:
|
||||
|
||||
```bash
|
||||
cd jetbrains/host
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. Open the plugin project in IntelliJ IDEA:
|
||||
|
||||
```bash
|
||||
cd jetbrains/plugin
|
||||
idea .
|
||||
```
|
||||
|
||||
3. Run the plugin using the "Run Plugin" configuration
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable debug logging by setting environment variables:
|
||||
|
||||
- `JETBRAINS_DEBUG_IPC=true` - Logs IPC messages
|
||||
- `JETBRAINS_RPC_DEBUG=true` - Logs RPC protocol messages
|
||||
|
||||
### Testing
|
||||
|
||||
Run tests for the host:
|
||||
|
||||
```bash
|
||||
cd jetbrains/host
|
||||
npm test
|
||||
```
|
||||
|
||||
Run tests for the plugin:
|
||||
|
||||
```bash
|
||||
cd jetbrains/plugin
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Host Configuration
|
||||
|
||||
The host can be configured via `jetbrains/host/src/config.ts`:
|
||||
|
||||
- `DEFAULT_PORT`: Default socket port (51234)
|
||||
- `SOCKET_TIMEOUT`: Connection timeout in milliseconds
|
||||
- `MAX_RECONNECT_ATTEMPTS`: Maximum reconnection attempts
|
||||
|
||||
### Plugin Configuration
|
||||
|
||||
Plugin settings are in `jetbrains/plugin/gradle.properties`:
|
||||
|
||||
- `pluginVersion`: Plugin version
|
||||
- `platformVersion`: Target IDE version
|
||||
- `platformType`: IDE type (IC for IntelliJ Community)
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### Communication Flow
|
||||
|
||||
```
|
||||
JetBrains IDE <-> Kotlin Plugin <-> Socket (TCP) <-> Node.js Host <-> VSCode Extension API <-> Roo-Code Extension
|
||||
```
|
||||
|
||||
### RPC Protocol
|
||||
|
||||
The RPC manager (`host/src/rpcManager.ts`) handles bidirectional communication:
|
||||
|
||||
- Incoming calls from JetBrains to VSCode APIs
|
||||
- Outgoing calls from VSCode to JetBrains APIs
|
||||
- Event subscriptions and notifications
|
||||
|
||||
### API Translation
|
||||
|
||||
Main thread actors in the plugin map JetBrains APIs to VSCode equivalents:
|
||||
|
||||
- `MainThreadCommandsShape`: Command execution
|
||||
- `MainThreadDocumentsShape`: Document management
|
||||
- `MainThreadTextEditorsShape`: Editor operations
|
||||
- `MainThreadTerminalServiceShape`: Terminal integration
|
||||
- And many more...
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Port already in use**: Change the socket port in the configuration
|
||||
2. **VSCode patch fails**: Ensure the VSCode submodule is at the correct version
|
||||
3. **Plugin doesn't load**: Check IDE compatibility in gradle.properties
|
||||
4. **Extension not found**: Verify the extension is built and in the correct location
|
||||
|
||||
### Logs Location
|
||||
|
||||
- Host logs: `jetbrains/host/logs/`
|
||||
- Plugin logs: Check IDE's log directory
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to the JetBrains integration:
|
||||
|
||||
1. Follow the existing code style
|
||||
2. Add tests for new functionality
|
||||
3. Update documentation as needed
|
||||
4. Test in multiple JetBrains IDEs if possible
|
||||
|
||||
## License
|
||||
|
||||
This JetBrains integration follows the same license as Roo-Code.
|
||||
|
||||
## Support
|
||||
|
||||
For issues specific to JetBrains integration:
|
||||
|
||||
1. Check this README's troubleshooting section
|
||||
2. Search existing issues on GitHub
|
||||
3. Create a new issue with the "jetbrains" label
|
||||
|
||||
## Credits
|
||||
|
||||
This integration is adapted from similar VSCode-to-JetBrains bridge implementations and modified for Roo-Code's specific requirements.
|
||||
5
jetbrains/host/.gitignore
vendored
Normal file
5
jetbrains/host/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
### Dependencies
|
||||
deps/*
|
||||
|
||||
### Build
|
||||
dist
|
||||
11
jetbrains/host/bootstrap-cli.ts
Normal file
11
jetbrains/host/bootstrap-cli.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Delete `VSCODE_CWD` very early. We have seen
|
||||
// reports where `code .` would use the wrong
|
||||
// current working directory due to our variable
|
||||
// somehow escaping to the parent shell
|
||||
// (https://github.com/microsoft/vscode/issues/126399)
|
||||
delete process.env["VSCODE_CWD"]
|
||||
126
jetbrains/host/bootstrap-esm.ts
Normal file
126
jetbrains/host/bootstrap-esm.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import { fileURLToPath } from "url"
|
||||
import { createRequire, register } from "node:module"
|
||||
import { product, pkg } from "./bootstrap-meta.js"
|
||||
import "./bootstrap-node.js"
|
||||
import * as performance from "./deps/vscode/vs/base/common/performance.js"
|
||||
import { INLSConfiguration } from "./deps/vscode/vs/nls.js"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// Install a hook to module resolution to map 'fs' to 'original-fs'
|
||||
if (process.env["ELECTRON_RUN_AS_NODE"] || process.versions["electron"]) {
|
||||
const jsCode = `
|
||||
export async function resolve(specifier, context, nextResolve) {
|
||||
if (specifier === 'fs') {
|
||||
return {
|
||||
format: 'builtin',
|
||||
shortCircuit: true,
|
||||
url: 'node:original-fs'
|
||||
};
|
||||
}
|
||||
|
||||
// Defer to the next hook in the chain, which would be the
|
||||
// Node.js default resolve if this is the last user-specified loader.
|
||||
return nextResolve(specifier, context);
|
||||
}`
|
||||
register(`data:text/javascript;base64,${Buffer.from(jsCode).toString("base64")}`, import.meta.url)
|
||||
}
|
||||
|
||||
// Prepare globals that are needed for running
|
||||
globalThis._VSCODE_PRODUCT_JSON = { ...product }
|
||||
if (process.env["VSCODE_DEV"]) {
|
||||
try {
|
||||
const overrides: unknown = require("../product.overrides.json")
|
||||
globalThis._VSCODE_PRODUCT_JSON = Object.assign(globalThis._VSCODE_PRODUCT_JSON, overrides)
|
||||
} catch (error) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
globalThis._VSCODE_PACKAGE_JSON = { ...pkg }
|
||||
globalThis._VSCODE_FILE_ROOT = __dirname
|
||||
|
||||
//#region NLS helpers
|
||||
|
||||
let setupNLSResult: Promise<INLSConfiguration | undefined> | undefined = undefined
|
||||
|
||||
function setupNLS(): Promise<INLSConfiguration | undefined> {
|
||||
if (!setupNLSResult) {
|
||||
setupNLSResult = doSetupNLS()
|
||||
}
|
||||
|
||||
return setupNLSResult
|
||||
}
|
||||
|
||||
async function doSetupNLS(): Promise<INLSConfiguration | undefined> {
|
||||
performance.mark("code/willLoadNls")
|
||||
|
||||
let nlsConfig: INLSConfiguration | undefined = undefined
|
||||
|
||||
let messagesFile: string | undefined
|
||||
if (process.env["VSCODE_NLS_CONFIG"]) {
|
||||
try {
|
||||
nlsConfig = JSON.parse(process.env["VSCODE_NLS_CONFIG"])
|
||||
if (nlsConfig?.languagePack?.messagesFile) {
|
||||
messagesFile = nlsConfig.languagePack.messagesFile
|
||||
} else if (nlsConfig?.defaultMessagesFile) {
|
||||
messagesFile = nlsConfig.defaultMessagesFile
|
||||
}
|
||||
|
||||
globalThis._VSCODE_NLS_LANGUAGE = nlsConfig?.resolvedLanguage
|
||||
} catch (e) {
|
||||
console.error(`Error reading VSCODE_NLS_CONFIG from environment: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env["VSCODE_DEV"] || // no NLS support in dev mode
|
||||
!messagesFile // no NLS messages file
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(messagesFile)).toString())
|
||||
} catch (error) {
|
||||
console.error(`Error reading NLS messages file ${messagesFile}: ${error}`)
|
||||
|
||||
// Mark as corrupt: this will re-create the language pack cache next startup
|
||||
if (nlsConfig?.languagePack?.corruptMarkerFile) {
|
||||
try {
|
||||
await fs.promises.writeFile(nlsConfig.languagePack.corruptMarkerFile, "corrupted")
|
||||
} catch (error) {
|
||||
console.error(`Error writing corrupted NLS marker file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the default message file to ensure english translation at least
|
||||
if (nlsConfig?.defaultMessagesFile && nlsConfig.defaultMessagesFile !== messagesFile) {
|
||||
try {
|
||||
globalThis._VSCODE_NLS_MESSAGES = JSON.parse(
|
||||
(await fs.promises.readFile(nlsConfig.defaultMessagesFile)).toString(),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error reading default NLS messages file ${nlsConfig.defaultMessagesFile}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
performance.mark("code/didLoadNls")
|
||||
|
||||
return nlsConfig
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export async function bootstrapESM(): Promise<void> {
|
||||
// NLS
|
||||
await setupNLS()
|
||||
}
|
||||
251
jetbrains/host/bootstrap-fork.ts
Normal file
251
jetbrains/host/bootstrap-fork.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as performance from "./deps/vscode/vs/base/common/performance.js"
|
||||
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from "./bootstrap-node.js"
|
||||
import { bootstrapESM } from "./bootstrap-esm.js"
|
||||
|
||||
performance.mark("code/fork/start")
|
||||
|
||||
//#region Helpers
|
||||
|
||||
function pipeLoggingToParent(): void {
|
||||
const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024
|
||||
const MAX_LENGTH = 100000
|
||||
|
||||
/**
|
||||
* Prevent circular stringify and convert arguments to real array
|
||||
*/
|
||||
function safeToString(args: ArrayLike<unknown>): string {
|
||||
const seen: unknown[] = []
|
||||
const argsArray: unknown[] = []
|
||||
|
||||
// Massage some arguments with special treatment
|
||||
if (args.length) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
let arg = args[i]
|
||||
|
||||
// Any argument of type 'undefined' needs to be specially treated because
|
||||
// JSON.stringify will simply ignore those. We replace them with the string
|
||||
// 'undefined' which is not 100% right, but good enough to be logged to console
|
||||
if (typeof arg === "undefined") {
|
||||
arg = "undefined"
|
||||
}
|
||||
|
||||
// Any argument that is an Error will be changed to be just the error stack/message
|
||||
// itself because currently cannot serialize the error over entirely.
|
||||
else if (arg instanceof Error) {
|
||||
const errorObj = arg
|
||||
if (errorObj.stack) {
|
||||
arg = errorObj.stack
|
||||
} else {
|
||||
arg = errorObj.toString()
|
||||
}
|
||||
}
|
||||
|
||||
argsArray.push(arg)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = JSON.stringify(argsArray, function (key, value: unknown) {
|
||||
// Objects get special treatment to prevent circles
|
||||
if (isObject(value) || Array.isArray(value)) {
|
||||
if (seen.indexOf(value) !== -1) {
|
||||
return "[Circular]"
|
||||
}
|
||||
|
||||
seen.push(value)
|
||||
}
|
||||
|
||||
return value
|
||||
})
|
||||
|
||||
if (res.length > MAX_LENGTH) {
|
||||
return "Output omitted for a large object that exceeds the limits"
|
||||
}
|
||||
|
||||
return res
|
||||
} catch (error) {
|
||||
return `Output omitted for an object that cannot be inspected ('${error.toString()}')`
|
||||
}
|
||||
}
|
||||
|
||||
function safeSend(arg: { type: string; severity: string; arguments: string }): void {
|
||||
try {
|
||||
if (process.send) {
|
||||
process.send(arg)
|
||||
}
|
||||
} catch (error) {
|
||||
// Can happen if the parent channel is closed meanwhile
|
||||
}
|
||||
}
|
||||
|
||||
function isObject(obj: unknown): boolean {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
!Array.isArray(obj) &&
|
||||
!(obj instanceof RegExp) &&
|
||||
!(obj instanceof Date)
|
||||
)
|
||||
}
|
||||
|
||||
function safeSendConsoleMessage(severity: "log" | "warn" | "error", args: string): void {
|
||||
safeSend({ type: "__$console", severity, arguments: args })
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a console message so that it is transmitted to the renderer.
|
||||
*
|
||||
* The wrapped property is not defined with `writable: false` to avoid
|
||||
* throwing errors, but rather a no-op setting. See https://github.com/microsoft/vscode-extension-telemetry/issues/88
|
||||
*/
|
||||
function wrapConsoleMethod(method: "log" | "info" | "warn" | "error", severity: "log" | "warn" | "error"): void {
|
||||
Object.defineProperty(console, method, {
|
||||
set: () => {},
|
||||
get: () =>
|
||||
function () {
|
||||
safeSendConsoleMessage(severity, safeToString(arguments))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps process.stderr/stdout.write() so that it is transmitted to the
|
||||
* renderer or CLI. It both calls through to the original method as well
|
||||
* as to console.log with complete lines so that they're made available
|
||||
* to the debugger/CLI.
|
||||
*/
|
||||
function wrapStream(streamName: "stdout" | "stderr", severity: "log" | "warn" | "error"): void {
|
||||
const stream = process[streamName]
|
||||
const original = stream.write
|
||||
|
||||
let buf = ""
|
||||
|
||||
Object.defineProperty(stream, "write", {
|
||||
set: () => {},
|
||||
get:
|
||||
() =>
|
||||
(
|
||||
chunk: string | Buffer | Uint8Array,
|
||||
encoding: BufferEncoding | undefined,
|
||||
callback: ((err?: Error | undefined) => void) | undefined,
|
||||
) => {
|
||||
buf += chunk.toString(encoding)
|
||||
const eol = buf.length > MAX_STREAM_BUFFER_LENGTH ? buf.length : buf.lastIndexOf("\n")
|
||||
if (eol !== -1) {
|
||||
console[severity](buf.slice(0, eol))
|
||||
buf = buf.slice(eol + 1)
|
||||
}
|
||||
|
||||
original.call(stream, chunk, encoding, callback)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Pass console logging to the outside so that we have it in the main side if told so
|
||||
if (process.env["VSCODE_VERBOSE_LOGGING"] === "true") {
|
||||
wrapConsoleMethod("info", "log")
|
||||
wrapConsoleMethod("log", "log")
|
||||
wrapConsoleMethod("warn", "warn")
|
||||
wrapConsoleMethod("error", "error")
|
||||
} else {
|
||||
console.log = function () {
|
||||
/* ignore */
|
||||
}
|
||||
console.warn = function () {
|
||||
/* ignore */
|
||||
}
|
||||
console.info = function () {
|
||||
/* ignore */
|
||||
}
|
||||
wrapConsoleMethod("error", "error")
|
||||
}
|
||||
|
||||
wrapStream("stderr", "error")
|
||||
wrapStream("stdout", "log")
|
||||
}
|
||||
|
||||
function handleExceptions(): void {
|
||||
// Handle uncaught exceptions
|
||||
process.on("uncaughtException", function (err) {
|
||||
console.error("Uncaught Exception: ", err)
|
||||
})
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on("unhandledRejection", function (reason) {
|
||||
console.error("Unhandled Promise Rejection: ", reason)
|
||||
})
|
||||
}
|
||||
|
||||
function terminateWhenParentTerminates(): void {
|
||||
const parentPid = Number(process.env["VSCODE_PARENT_PID"])
|
||||
|
||||
if (typeof parentPid === "number" && !isNaN(parentPid)) {
|
||||
setInterval(function () {
|
||||
try {
|
||||
process.kill(parentPid, 0) // throws an exception if the main process doesn't exist anymore.
|
||||
} catch (e) {
|
||||
process.exit()
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
function configureCrashReporter(): void {
|
||||
const crashReporterProcessType = process.env["VSCODE_CRASH_REPORTER_PROCESS_TYPE"]
|
||||
if (crashReporterProcessType) {
|
||||
try {
|
||||
//@ts-ignore
|
||||
if (
|
||||
process["crashReporter"] &&
|
||||
typeof process["crashReporter"].addExtraParameter === "function" /* Electron only */
|
||||
) {
|
||||
//@ts-ignore
|
||||
process["crashReporter"].addExtraParameter("processType", crashReporterProcessType)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// Crash reporter
|
||||
configureCrashReporter()
|
||||
|
||||
// Remove global paths from the node module lookup (node.js only)
|
||||
removeGlobalNodeJsModuleLookupPaths()
|
||||
|
||||
if (process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]) {
|
||||
devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
|
||||
}
|
||||
|
||||
// Configure: pipe logging to parent process
|
||||
if (!!process.send && process.env["VSCODE_PIPE_LOGGING"] === "true") {
|
||||
pipeLoggingToParent()
|
||||
}
|
||||
|
||||
// Handle Exceptions
|
||||
if (!process.env["VSCODE_HANDLES_UNCAUGHT_ERRORS"]) {
|
||||
handleExceptions()
|
||||
}
|
||||
|
||||
// Terminate when parent terminates
|
||||
if (process.env["VSCODE_PARENT_PID"]) {
|
||||
terminateWhenParentTerminates()
|
||||
}
|
||||
|
||||
// Bootstrap ESM
|
||||
await bootstrapESM()
|
||||
|
||||
// Load ESM entry point
|
||||
await import(
|
||||
[`./${process.env["VSCODE_ESM_ENTRYPOINT"]}.js`].join(
|
||||
"/",
|
||||
) /* workaround: esbuild prints some strange warnings when trying to inline? */
|
||||
)
|
||||
62
jetbrains/host/bootstrap-import.ts
Normal file
62
jetbrains/host/bootstrap-import.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// *********************************************************************
|
||||
// * *
|
||||
// * We need this to redirect to node_modules from the remote-folder. *
|
||||
// * This ONLY applies when running out of source. *
|
||||
// * *
|
||||
// *********************************************************************
|
||||
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
import { promises } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
// SEE https://nodejs.org/docs/latest/api/module.html#initialize
|
||||
|
||||
const _specifierToUrl: Record<string, string> = {}
|
||||
|
||||
export async function initialize(injectPath: string): Promise<void> {
|
||||
// populate mappings
|
||||
|
||||
const injectPackageJSONPath = fileURLToPath(new URL("../package.json", pathToFileURL(injectPath)))
|
||||
const packageJSON = JSON.parse(String(await promises.readFile(injectPackageJSONPath)))
|
||||
|
||||
for (const [name] of Object.entries(packageJSON.dependencies)) {
|
||||
try {
|
||||
const path = join(injectPackageJSONPath, `../node_modules/${name}/package.json`)
|
||||
let { main } = JSON.parse(String(await promises.readFile(path)))
|
||||
|
||||
if (!main) {
|
||||
main = "index.js"
|
||||
}
|
||||
if (!main.endsWith(".js")) {
|
||||
main += ".js"
|
||||
}
|
||||
const mainPath = join(injectPackageJSONPath, `../node_modules/${name}/${main}`)
|
||||
_specifierToUrl[name] = pathToFileURL(mainPath).href
|
||||
} catch (err) {
|
||||
console.error(name)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[bootstrap-import] Initialized node_modules redirector for: ${injectPath}`)
|
||||
}
|
||||
|
||||
export async function resolve(specifier: string | number, context: any, nextResolve: (arg0: any, arg1: any) => any) {
|
||||
const newSpecifier = _specifierToUrl[specifier]
|
||||
if (newSpecifier !== undefined) {
|
||||
return {
|
||||
format: "commonjs",
|
||||
shortCircuit: true,
|
||||
url: newSpecifier,
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the next hook in the chain, which would be the
|
||||
// Node.js default resolve if this is the last user-specified loader.
|
||||
return nextResolve(specifier, context)
|
||||
}
|
||||
24
jetbrains/host/bootstrap-meta.ts
Normal file
24
jetbrains/host/bootstrap-meta.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createRequire } from "node:module"
|
||||
import type { IProductConfiguration } from "./deps/vscode/vs/base/common/product.js"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
let productObj: Partial<IProductConfiguration> & { BUILD_INSERT_PRODUCT_CONFIGURATION?: string } = {
|
||||
BUILD_INSERT_PRODUCT_CONFIGURATION: "BUILD_INSERT_PRODUCT_CONFIGURATION",
|
||||
} // DO NOT MODIFY, PATCHED DURING BUILD
|
||||
if (productObj["BUILD_INSERT_PRODUCT_CONFIGURATION"]) {
|
||||
productObj = require("../product.json") // Running out of sources
|
||||
}
|
||||
|
||||
let pkgObj = { BUILD_INSERT_PACKAGE_CONFIGURATION: "BUILD_INSERT_PACKAGE_CONFIGURATION" } // DO NOT MODIFY, PATCHED DURING BUILD
|
||||
if (pkgObj["BUILD_INSERT_PACKAGE_CONFIGURATION"]) {
|
||||
pkgObj = require("../package.json") // Running out of sources
|
||||
}
|
||||
|
||||
export const product = productObj
|
||||
export const pkg = pkgObj
|
||||
193
jetbrains/host/bootstrap-node.ts
Normal file
193
jetbrains/host/bootstrap-node.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import { fileURLToPath } from "url"
|
||||
import { createRequire } from "node:module"
|
||||
import type { IProductConfiguration } from "./deps/vscode/vs/base/common/product.js"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const isWindows = process.platform === "win32"
|
||||
|
||||
// increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
|
||||
Error.stackTraceLimit = 100
|
||||
|
||||
if (!process.env["VSCODE_HANDLES_SIGPIPE"]) {
|
||||
// Workaround for Electron not installing a handler to ignore SIGPIPE
|
||||
// (https://github.com/electron/electron/issues/13254)
|
||||
let didLogAboutSIGPIPE = false
|
||||
process.on("SIGPIPE", () => {
|
||||
// See https://github.com/microsoft/vscode-remote-release/issues/6543
|
||||
// In certain situations, the console itself can be in a broken pipe state
|
||||
// so logging SIGPIPE to the console will cause an infinite async loop
|
||||
if (!didLogAboutSIGPIPE) {
|
||||
didLogAboutSIGPIPE = true
|
||||
console.error(new Error(`Unexpected SIGPIPE`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Setup current working directory in all our node & electron processes
|
||||
// - Windows: call `process.chdir()` to always set application folder as cwd
|
||||
// - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups
|
||||
function setupCurrentWorkingDirectory(): void {
|
||||
try {
|
||||
// Store the `process.cwd()` inside `VSCODE_CWD`
|
||||
// for consistent lookups, but make sure to only
|
||||
// do this once unless defined already from e.g.
|
||||
// a parent process.
|
||||
if (typeof process.env["VSCODE_CWD"] !== "string") {
|
||||
process.env["VSCODE_CWD"] = process.cwd()
|
||||
}
|
||||
|
||||
// Windows: always set application folder as current working dir
|
||||
if (process.platform === "win32") {
|
||||
process.chdir(path.dirname(process.execPath))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
setupCurrentWorkingDirectory()
|
||||
|
||||
/**
|
||||
* Add support for redirecting the loading of node modules
|
||||
*
|
||||
* Note: only applies when running out of sources.
|
||||
*/
|
||||
export function devInjectNodeModuleLookupPath(injectPath: string): void {
|
||||
if (!process.env["VSCODE_DEV"]) {
|
||||
return // only applies running out of sources
|
||||
}
|
||||
|
||||
if (!injectPath) {
|
||||
throw new Error("Missing injectPath")
|
||||
}
|
||||
|
||||
// register a loader hook
|
||||
const Module = require("node:module")
|
||||
Module.register("./bootstrap-import.js", { parentURL: import.meta.url, data: injectPath })
|
||||
}
|
||||
|
||||
export function removeGlobalNodeJsModuleLookupPaths(): void {
|
||||
if (typeof process?.versions?.electron === "string") {
|
||||
return // Electron disables global search paths in https://github.com/electron/electron/blob/3186c2f0efa92d275dc3d57b5a14a60ed3846b0e/shell/common/node_bindings.cc#L653
|
||||
}
|
||||
|
||||
const Module = require("module")
|
||||
const globalPaths = Module.globalPaths
|
||||
|
||||
const originalResolveLookupPaths = Module._resolveLookupPaths
|
||||
|
||||
Module._resolveLookupPaths = function (moduleName: string, parent: any): string[] {
|
||||
const paths = originalResolveLookupPaths(moduleName, parent)
|
||||
if (Array.isArray(paths)) {
|
||||
let commonSuffixLength = 0
|
||||
while (
|
||||
commonSuffixLength < paths.length &&
|
||||
paths[paths.length - 1 - commonSuffixLength] ===
|
||||
globalPaths[globalPaths.length - 1 - commonSuffixLength]
|
||||
) {
|
||||
commonSuffixLength++
|
||||
}
|
||||
|
||||
return paths.slice(0, paths.length - commonSuffixLength)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
const originalNodeModulePaths = Module._nodeModulePaths
|
||||
Module._nodeModulePaths = function (from: string): string[] {
|
||||
let paths: string[] = originalNodeModulePaths(from)
|
||||
if (!isWindows) {
|
||||
return paths
|
||||
}
|
||||
|
||||
// On Windows, remove drive(s) and users' home directory from search paths,
|
||||
// UNLESS 'from' is explicitly set to one of those.
|
||||
const isDrive = (p: string) => p.length >= 3 && p.endsWith(":\\")
|
||||
|
||||
if (!isDrive(from)) {
|
||||
paths = paths.filter((p) => !isDrive(path.dirname(p)))
|
||||
}
|
||||
|
||||
if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
|
||||
const userDir = path.dirname(path.join(process.env.HOMEDRIVE, process.env.HOMEPATH))
|
||||
|
||||
const isUsersDir = (p: string) => path.relative(p, userDir).length === 0
|
||||
|
||||
// Check if 'from' is the same as 'userDir'
|
||||
if (!isUsersDir(from)) {
|
||||
paths = paths.filter((p) => !isUsersDir(path.dirname(p)))
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to enable portable mode.
|
||||
*/
|
||||
export function configurePortable(product: Partial<IProductConfiguration>): {
|
||||
portableDataPath: string
|
||||
isPortable: boolean
|
||||
} {
|
||||
const appRoot = path.dirname(__dirname)
|
||||
|
||||
function getApplicationPath(): string {
|
||||
if (process.env["VSCODE_DEV"]) {
|
||||
return appRoot
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return path.dirname(path.dirname(path.dirname(appRoot)))
|
||||
}
|
||||
|
||||
return path.dirname(path.dirname(appRoot))
|
||||
}
|
||||
|
||||
function getPortableDataPath(): string {
|
||||
if (process.env["VSCODE_PORTABLE"]) {
|
||||
return process.env["VSCODE_PORTABLE"]
|
||||
}
|
||||
|
||||
if (process.platform === "win32" || process.platform === "linux") {
|
||||
return path.join(getApplicationPath(), "data")
|
||||
}
|
||||
|
||||
const portableDataName = product.portable || `${product.applicationName}-portable-data`
|
||||
return path.join(path.dirname(getApplicationPath()), portableDataName)
|
||||
}
|
||||
|
||||
const portableDataPath = getPortableDataPath()
|
||||
const isPortable = !("target" in product) && fs.existsSync(portableDataPath)
|
||||
const portableTempPath = path.join(portableDataPath, "tmp")
|
||||
const isTempPortable = isPortable && fs.existsSync(portableTempPath)
|
||||
|
||||
if (isPortable) {
|
||||
process.env["VSCODE_PORTABLE"] = portableDataPath
|
||||
} else {
|
||||
delete process.env["VSCODE_PORTABLE"]
|
||||
}
|
||||
|
||||
if (isTempPortable) {
|
||||
if (process.platform === "win32") {
|
||||
process.env["TMP"] = portableTempPath
|
||||
process.env["TEMP"] = portableTempPath
|
||||
} else {
|
||||
process.env["TMPDIR"] = portableTempPath
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
portableDataPath,
|
||||
isPortable,
|
||||
}
|
||||
}
|
||||
7
jetbrains/host/bootstrap-server.ts
Normal file
7
jetbrains/host/bootstrap-server.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Keep bootstrap-esm.js from redefining 'fs'.
|
||||
delete process.env["ELECTRON_RUN_AS_NODE"]
|
||||
252
jetbrains/host/bootstrap-window.ts
Normal file
252
jetbrains/host/bootstrap-window.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
;(function () {
|
||||
type ISandboxConfiguration =
|
||||
import("./deps/vscode/vs/base/parts/sandbox/common/sandboxTypes.js").ISandboxConfiguration
|
||||
type ILoadResult<
|
||||
M,
|
||||
T extends ISandboxConfiguration,
|
||||
> = import("./deps/vscode/vs/platform/window/electron-sandbox/window.js").ILoadResult<M, T>
|
||||
type ILoadOptions<T extends ISandboxConfiguration> =
|
||||
import("./deps/vscode/vs/platform/window/electron-sandbox/window.js").ILoadOptions<T>
|
||||
type IMainWindowSandboxGlobals =
|
||||
import("./deps/vscode/vs/base/parts/sandbox/electron-sandbox/globals.js").IMainWindowSandboxGlobals
|
||||
|
||||
const preloadGlobals: IMainWindowSandboxGlobals = (window as any).vscode // defined by preload.ts
|
||||
const safeProcess = preloadGlobals.process
|
||||
|
||||
async function load<M, T extends ISandboxConfiguration>(
|
||||
esModule: string,
|
||||
options: ILoadOptions<T>,
|
||||
): Promise<ILoadResult<M, T>> {
|
||||
// Window Configuration from Preload Script
|
||||
const configuration = await resolveWindowConfiguration<T>()
|
||||
|
||||
// Signal before import()
|
||||
options?.beforeImport?.(configuration)
|
||||
|
||||
// Developer settings
|
||||
const {
|
||||
enableDeveloperKeybindings,
|
||||
removeDeveloperKeybindingsAfterLoad,
|
||||
developerDeveloperKeybindingsDisposable,
|
||||
forceDisableShowDevtoolsOnError,
|
||||
} = setupDeveloperKeybindings(configuration, options)
|
||||
|
||||
// NLS
|
||||
setupNLS<T>(configuration)
|
||||
|
||||
// Compute base URL and set as global
|
||||
const baseUrl = new URL(
|
||||
`${fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === "win32", scheme: "vscode-file", fallbackAuthority: "vscode-app" })}/out/`,
|
||||
)
|
||||
globalThis._VSCODE_FILE_ROOT = baseUrl.toString()
|
||||
|
||||
// Dev only: CSS import map tricks
|
||||
setupCSSImportMaps<T>(configuration, baseUrl)
|
||||
|
||||
// ESM Import
|
||||
try {
|
||||
const result = await import(new URL(`${esModule}.js`, baseUrl).href)
|
||||
|
||||
if (developerDeveloperKeybindingsDisposable && removeDeveloperKeybindingsAfterLoad) {
|
||||
developerDeveloperKeybindingsDisposable()
|
||||
}
|
||||
|
||||
return { result, configuration }
|
||||
} catch (error) {
|
||||
onUnexpectedError(error, enableDeveloperKeybindings && !forceDisableShowDevtoolsOnError)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveWindowConfiguration<T extends ISandboxConfiguration>() {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error(
|
||||
`[resolve window config] Could not resolve window configuration within 10 seconds, but will continue to wait...`,
|
||||
)
|
||||
}, 10000)
|
||||
performance.mark("code/willWaitForWindowConfig")
|
||||
|
||||
const configuration = (await preloadGlobals.context.resolveConfiguration()) as T
|
||||
performance.mark("code/didWaitForWindowConfig")
|
||||
|
||||
clearTimeout(timeout)
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
function setupDeveloperKeybindings<T extends ISandboxConfiguration>(configuration: T, options: ILoadOptions<T>) {
|
||||
const {
|
||||
forceEnableDeveloperKeybindings,
|
||||
disallowReloadKeybinding,
|
||||
removeDeveloperKeybindingsAfterLoad,
|
||||
forceDisableShowDevtoolsOnError,
|
||||
} =
|
||||
typeof options?.configureDeveloperSettings === "function"
|
||||
? options.configureDeveloperSettings(configuration)
|
||||
: {
|
||||
forceEnableDeveloperKeybindings: false,
|
||||
disallowReloadKeybinding: false,
|
||||
removeDeveloperKeybindingsAfterLoad: false,
|
||||
forceDisableShowDevtoolsOnError: false,
|
||||
}
|
||||
|
||||
const isDev = !!safeProcess.env["VSCODE_DEV"]
|
||||
const enableDeveloperKeybindings = Boolean(isDev || forceEnableDeveloperKeybindings)
|
||||
let developerDeveloperKeybindingsDisposable: Function | undefined = undefined
|
||||
if (enableDeveloperKeybindings) {
|
||||
developerDeveloperKeybindingsDisposable = registerDeveloperKeybindings(disallowReloadKeybinding)
|
||||
}
|
||||
|
||||
return {
|
||||
enableDeveloperKeybindings,
|
||||
removeDeveloperKeybindingsAfterLoad,
|
||||
developerDeveloperKeybindingsDisposable,
|
||||
forceDisableShowDevtoolsOnError,
|
||||
}
|
||||
}
|
||||
|
||||
function registerDeveloperKeybindings(disallowReloadKeybinding: boolean | undefined): Function {
|
||||
const ipcRenderer = preloadGlobals.ipcRenderer
|
||||
|
||||
const extractKey = function (e: KeyboardEvent) {
|
||||
return [
|
||||
e.ctrlKey ? "ctrl-" : "",
|
||||
e.metaKey ? "meta-" : "",
|
||||
e.altKey ? "alt-" : "",
|
||||
e.shiftKey ? "shift-" : "",
|
||||
e.keyCode,
|
||||
].join("")
|
||||
}
|
||||
|
||||
// Devtools & reload support
|
||||
const TOGGLE_DEV_TOOLS_KB = safeProcess.platform === "darwin" ? "meta-alt-73" : "ctrl-shift-73" // mac: Cmd-Alt-I, rest: Ctrl-Shift-I
|
||||
const TOGGLE_DEV_TOOLS_KB_ALT = "123" // F12
|
||||
const RELOAD_KB = safeProcess.platform === "darwin" ? "meta-82" : "ctrl-82" // mac: Cmd-R, rest: Ctrl-R
|
||||
|
||||
let listener: ((e: KeyboardEvent) => void) | undefined = function (e) {
|
||||
const key = extractKey(e)
|
||||
if (key === TOGGLE_DEV_TOOLS_KB || key === TOGGLE_DEV_TOOLS_KB_ALT) {
|
||||
ipcRenderer.send("vscode:toggleDevTools")
|
||||
} else if (key === RELOAD_KB && !disallowReloadKeybinding) {
|
||||
ipcRenderer.send("vscode:reloadWindow")
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", listener)
|
||||
|
||||
return function () {
|
||||
if (listener) {
|
||||
window.removeEventListener("keydown", listener)
|
||||
listener = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupNLS<T extends ISandboxConfiguration>(configuration: T): void {
|
||||
globalThis._VSCODE_NLS_MESSAGES = configuration.nls.messages
|
||||
globalThis._VSCODE_NLS_LANGUAGE = configuration.nls.language
|
||||
|
||||
let language = configuration.nls.language || "en"
|
||||
if (language === "zh-tw") {
|
||||
language = "zh-Hant"
|
||||
} else if (language === "zh-cn") {
|
||||
language = "zh-Hans"
|
||||
}
|
||||
|
||||
window.document.documentElement.setAttribute("lang", language)
|
||||
}
|
||||
|
||||
function onUnexpectedError(error: string | Error, showDevtoolsOnError: boolean): void {
|
||||
if (showDevtoolsOnError) {
|
||||
const ipcRenderer = preloadGlobals.ipcRenderer
|
||||
ipcRenderer.send("vscode:openDevTools")
|
||||
}
|
||||
|
||||
console.error(`[uncaught exception]: ${error}`)
|
||||
|
||||
if (error && typeof error !== "string" && error.stack) {
|
||||
console.error(error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
function fileUriFromPath(
|
||||
path: string,
|
||||
config: { isWindows?: boolean; scheme?: string; fallbackAuthority?: string },
|
||||
): string {
|
||||
// Since we are building a URI, we normalize any backslash
|
||||
// to slashes and we ensure that the path begins with a '/'.
|
||||
let pathName = path.replace(/\\/g, "/")
|
||||
if (pathName.length > 0 && pathName.charAt(0) !== "/") {
|
||||
pathName = `/${pathName}`
|
||||
}
|
||||
|
||||
let uri: string
|
||||
|
||||
// Windows: in order to support UNC paths (which start with '//')
|
||||
// that have their own authority, we do not use the provided authority
|
||||
// but rather preserve it.
|
||||
if (config.isWindows && pathName.startsWith("//")) {
|
||||
uri = encodeURI(`${config.scheme || "file"}:${pathName}`)
|
||||
}
|
||||
|
||||
// Otherwise we optionally add the provided authority if specified
|
||||
else {
|
||||
uri = encodeURI(`${config.scheme || "file"}://${config.fallbackAuthority || ""}${pathName}`)
|
||||
}
|
||||
|
||||
return uri.replace(/#/g, "%23")
|
||||
}
|
||||
|
||||
function setupCSSImportMaps<T extends ISandboxConfiguration>(configuration: T, baseUrl: URL) {
|
||||
// DEV ---------------------------------------------------------------------------------------
|
||||
// DEV: This is for development and enables loading CSS via import-statements via import-maps.
|
||||
// DEV: For each CSS modules that we have we defined an entry in the import map that maps to
|
||||
// DEV: a blob URL that loads the CSS via a dynamic @import-rule.
|
||||
// DEV ---------------------------------------------------------------------------------------
|
||||
|
||||
if (Array.isArray(configuration.cssModules) && configuration.cssModules.length > 0) {
|
||||
performance.mark("code/willAddCssLoader")
|
||||
|
||||
const style = document.createElement("style")
|
||||
style.type = "text/css"
|
||||
style.media = "screen"
|
||||
style.id = "vscode-css-loading"
|
||||
document.head.appendChild(style)
|
||||
|
||||
globalThis._VSCODE_CSS_LOAD = function (url) {
|
||||
style.textContent += `@import url(${url});\n`
|
||||
}
|
||||
|
||||
const importMap: { imports: Record<string, string> } = { imports: {} }
|
||||
for (const cssModule of configuration.cssModules) {
|
||||
const cssUrl = new URL(cssModule, baseUrl).href
|
||||
const jsSrc = `globalThis._VSCODE_CSS_LOAD('${cssUrl}');\n`
|
||||
const blob = new Blob([jsSrc], { type: "application/javascript" })
|
||||
importMap.imports[cssUrl] = URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
const ttp = window.trustedTypes?.createPolicy("vscode-bootstrapImportMap", {
|
||||
createScript(value) {
|
||||
return value
|
||||
},
|
||||
})
|
||||
const importMapSrc = JSON.stringify(importMap, undefined, 2)
|
||||
const importMapScript = document.createElement("script")
|
||||
importMapScript.type = "importmap"
|
||||
importMapScript.setAttribute("nonce", "0c6a828f1297")
|
||||
// @ts-ignore
|
||||
importMapScript.textContent = ttp?.createScript(importMapSrc) ?? importMapSrc
|
||||
document.head.appendChild(importMapScript)
|
||||
|
||||
performance.mark("code/didAddCssLoader")
|
||||
}
|
||||
}
|
||||
|
||||
;(globalThis as any).MonacoBootstrapWindow = { load }
|
||||
})()
|
||||
36
jetbrains/host/cli.ts
Normal file
36
jetbrains/host/cli.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import "./bootstrap-cli.js" // this MUST come before other imports as it changes global state
|
||||
import { dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { configurePortable } from "./bootstrap-node.js"
|
||||
import { bootstrapESM } from "./bootstrap-esm.js"
|
||||
import { resolveNLSConfiguration } from "./deps/vscode/vs/base/node/nls.js"
|
||||
import { product } from "./bootstrap-meta.js"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// NLS
|
||||
const nlsConfiguration = await resolveNLSConfiguration({
|
||||
userLocale: "en",
|
||||
osLocale: "en",
|
||||
commit: product.commit,
|
||||
userDataPath: "",
|
||||
nlsMetadataPath: __dirname,
|
||||
})
|
||||
process.env["VSCODE_NLS_CONFIG"] = JSON.stringify(nlsConfiguration) // required for `bootstrap-esm` to pick up NLS messages
|
||||
|
||||
// Enable portable support
|
||||
configurePortable(product)
|
||||
|
||||
// Signal processes that we got launched as CLI
|
||||
process.env["VSCODE_CLI"] = "1"
|
||||
|
||||
// Bootstrap ESM
|
||||
await bootstrapESM()
|
||||
|
||||
// Load Server
|
||||
await import("./deps/vscode/vs/code/node/cli.js")
|
||||
38
jetbrains/host/electron.d.ts
vendored
Normal file
38
jetbrains/host/electron.d.ts
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { MessageEvent } from "electron"
|
||||
|
||||
declare global {
|
||||
interface Process {
|
||||
/**
|
||||
* Electron's process.crash() method
|
||||
* This method is used to crash the process
|
||||
*/
|
||||
crash(): void
|
||||
|
||||
/**
|
||||
* Electron's IPC (Inter-Process Communication) port
|
||||
* Used for communication between renderer and main processes
|
||||
*/
|
||||
parentPort: {
|
||||
/**
|
||||
* Register a listener for a specific channel
|
||||
*/
|
||||
on(channel: string, listener: (event: MessageEvent) => void): void
|
||||
|
||||
/**
|
||||
* Register a one-time listener for a specific channel
|
||||
*/
|
||||
once(channel: string, listener: (event: MessageEvent) => void): void
|
||||
|
||||
/**
|
||||
* Send a message to the parent process
|
||||
*/
|
||||
postMessage(message: any): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
42
jetbrains/host/eslint.config.mjs
Normal file
42
jetbrains/host/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [
|
||||
...config,
|
||||
{
|
||||
rules: {
|
||||
// TODO: These should be fixed and the rules re-enabled.
|
||||
"no-regex-spaces": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-empty": "off",
|
||||
"prefer-const": "off",
|
||||
"prefer-rest-params": "off",
|
||||
"no-var": "off",
|
||||
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unsafe-function-type": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
||||
"@typescript-eslint/no-misused-new": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["__mocks__/**/*.js"],
|
||||
rules: {
|
||||
"no-undef": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/__test_cases__/**/*"],
|
||||
rules: {
|
||||
"no-undef": "off",
|
||||
"no-const-assign": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist", "deps"],
|
||||
},
|
||||
]
|
||||
142
jetbrains/host/extension.ts
Normal file
142
jetbrains/host/extension.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import net from "net"
|
||||
|
||||
// Save original process methods
|
||||
const originalProcessOn = process.on
|
||||
const originalProcessSend = process.send || (() => false)
|
||||
|
||||
// Store message event handlers
|
||||
const messageHandlers: ((message: any, socket?: net.Socket) => void)[] = []
|
||||
|
||||
// Reconnection related variables
|
||||
let isReconnecting = false
|
||||
let reconnectAttempts = 0
|
||||
const MAX_RECONNECT_ATTEMPTS = 5
|
||||
const RECONNECT_DELAY = 1000 // 1 second
|
||||
|
||||
// Override process.on
|
||||
process.on = function (event: string, listener: (...args: any[]) => void): any {
|
||||
if (event === "message") {
|
||||
messageHandlers.push((message: any, socket?: net.Socket) => {
|
||||
// Check the number of parameters for listener
|
||||
const paramCount = listener.length
|
||||
if (paramCount === 1) {
|
||||
// If only one parameter, pass only message
|
||||
listener(message)
|
||||
} else {
|
||||
// If multiple parameters, pass message and socket
|
||||
listener(message, socket)
|
||||
}
|
||||
})
|
||||
}
|
||||
return originalProcessOn.call(process, event, listener)
|
||||
}
|
||||
|
||||
// Override process.send
|
||||
process.send = function (message: any): boolean {
|
||||
if (message?.type === "VSCODE_EXTHOST_IPC_READY") {
|
||||
console.log("Extension host process is ready to receive socket")
|
||||
connect()
|
||||
}
|
||||
|
||||
// Call original process.send
|
||||
return originalProcessSend.call(process, message)
|
||||
}
|
||||
|
||||
// Establish socket connection
|
||||
function connect() {
|
||||
if (isReconnecting) {
|
||||
console.log("Already in reconnection process, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Get socket server information from environment variables
|
||||
const host = process.env.VSCODE_EXTHOST_SOCKET_HOST || "127.0.0.1"
|
||||
const port = parseInt(process.env.VSCODE_EXTHOST_SOCKET_PORT || "0", 10)
|
||||
|
||||
if (!port) {
|
||||
throw new Error("Invalid socket port")
|
||||
}
|
||||
|
||||
console.log(`Attempting to connect to ${host}:${port}`)
|
||||
|
||||
// Establish socket connection
|
||||
const socket = net.createConnection(port, host)
|
||||
// Set socket noDelay option
|
||||
socket.setNoDelay(true)
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected to main server")
|
||||
isReconnecting = false
|
||||
reconnectAttempts = 0
|
||||
|
||||
// Prepare message to send to VSCode module
|
||||
const socketMessage = {
|
||||
type: "VSCODE_EXTHOST_IPC_SOCKET",
|
||||
initialDataChunk: "",
|
||||
skipWebSocketFrames: true,
|
||||
permessageDeflate: false,
|
||||
inflateBytes: "",
|
||||
}
|
||||
|
||||
// Call all saved message handlers
|
||||
messageHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(socketMessage, socket)
|
||||
} catch (error) {
|
||||
console.error("Error in message handler:", error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
socket.on("error", (error: Error) => {
|
||||
console.error("Socket connection error:", error)
|
||||
handleDisconnect()
|
||||
})
|
||||
|
||||
socket.on("close", () => {
|
||||
console.log("Socket connection closed")
|
||||
handleDisconnect()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Connection error:", error)
|
||||
handleDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle disconnection
|
||||
async function handleDisconnect() {
|
||||
if (isReconnecting) {
|
||||
console.log("Already in reconnection process, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
isReconnecting = true
|
||||
reconnectAttempts++
|
||||
|
||||
if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
||||
console.error("Max reconnection attempts reached. Giving up.")
|
||||
isReconnecting = false
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Attempting to reconnect (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
|
||||
|
||||
// Wait for a while before retrying
|
||||
console.log(`Waiting ${RECONNECT_DELAY}ms before reconnecting...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY))
|
||||
console.log("Reconnection delay finished, attempting to connect...")
|
||||
connect()
|
||||
}
|
||||
|
||||
console.log("Starting extension host process...")
|
||||
|
||||
import start from "./deps/vscode/vs/workbench/api/node/extensionHostProcess.js"
|
||||
|
||||
// This line will trigger extension host related logic startup, actual logic is in extensionHostProcess,
|
||||
// Do not handle specific plugin business logic in subsequent content of this file
|
||||
start()
|
||||
178
jetbrains/host/package.json
Normal file
178
jetbrains/host/package.json
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"name": "@roo-code/jetbrains-host",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deps:check": "node ../../jetbrains/scripts/check-dependencies.js",
|
||||
"deps:patch": "npm run deps:check && cd ../../deps/vscode && git reset --hard HEAD && git clean -fd && git apply ../patches/vscode/jetbrains.patch",
|
||||
"deps:clean": "rm -rf ./deps/vscode/* || true",
|
||||
"deps:copy": "npm run deps:check && npx cpy '../../deps/vscode/src/**' './deps/vscode' --parents",
|
||||
"clean": "del-cli ./dist",
|
||||
"build": "tsc",
|
||||
"build:clean": "npm run clean && npm run build",
|
||||
"start": "node ./dist/src/main.js",
|
||||
"dev": "tsc && node ./dist/src/main.js",
|
||||
"watch:tsc": "tsc --watch",
|
||||
"watch": "tsc --watch",
|
||||
"bundle:package": "cp ./package.json ./dist/package.json",
|
||||
"bundle:build": "tsc --noEmit && tsup",
|
||||
"lint": "eslint . --ext=ts --max-warnings=0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@c4312/eventsource-umd": "^3.0.5",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.0",
|
||||
"@vscode/policy-watcher": "^1.3.2",
|
||||
"@vscode/proxy-agent": "^0.32.0",
|
||||
"@vscode/ripgrep": "^1.15.11",
|
||||
"@vscode/spdlog": "^0.15.0",
|
||||
"@vscode/sqlite3": "5.1.8-vscode",
|
||||
"@vscode/sudo-prompt": "9.3.1",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@vscode/windows-mutex": "^0.5.0",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
"@vscode/windows-registry": "^1.1.0",
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.82",
|
||||
"@xterm/addon-image": "^0.9.0-beta.99",
|
||||
"@xterm/addon-ligatures": "^0.10.0-beta.99",
|
||||
"@xterm/addon-progress": "^0.2.0-beta.5",
|
||||
"@xterm/addon-search": "^0.16.0-beta.99",
|
||||
"@xterm/addon-serialize": "^0.14.0-beta.99",
|
||||
"@xterm/addon-unicode11": "^0.9.0-beta.99",
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.99",
|
||||
"@xterm/headless": "^5.6.0-beta.99",
|
||||
"@xterm/xterm": "^5.6.0-beta.99",
|
||||
"all": "^0.0.0",
|
||||
"debug": "^4.4.1",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.4",
|
||||
"kerberos": "2.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"native-is-elevated": "0.7.0",
|
||||
"native-keymap": "^3.3.5",
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "^1.1.0-beta33",
|
||||
"open": "^8.4.2",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"undici": "^7.13.0",
|
||||
"undici-types": "^7.15.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
"vscode-regexpp": "^3.1.0",
|
||||
"vscode-textmate": "9.2.0",
|
||||
"yauzl": "^3.0.0",
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "file:../../packages/config-eslint",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "20.x",
|
||||
"@types/sinon": "^10.0.2",
|
||||
"@types/sinon-test": "^2.4.2",
|
||||
"@types/trusted-types": "^1.0.6",
|
||||
"@types/vscode-notebook-renderer": "^1.72.0",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@types/windows-foreground-love": "^0.3.0",
|
||||
"@types/winreg": "^1.2.30",
|
||||
"@types/yauzl": "^2.10.0",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@vscode/gulp-electron": "^1.36.0",
|
||||
"@vscode/l10n-dev": "0.0.35",
|
||||
"@vscode/telemetry-extractor": "^1.10.2",
|
||||
"@vscode/test-cli": "^0.0.6",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"@vscode/test-web": "^0.0.62",
|
||||
"@vscode/v8-heap-parser": "^0.1.0",
|
||||
"@vscode/vscode-perf": "^0.0.19",
|
||||
"@webgpu/types": "^0.1.44",
|
||||
"ansi-colors": "^3.2.3",
|
||||
"asar": "^3.0.3",
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
"cookie": "^0.7.2",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.9.1",
|
||||
"cssnano": "^6.0.3",
|
||||
"debounce": "^1.0.0",
|
||||
"deemon": "^1.11.0",
|
||||
"electron": "34.4.1",
|
||||
"event-stream": "3.3.4",
|
||||
"fancy-log": "^1.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"glob": "^7.2.3",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-azure-storage": "^0.12.1",
|
||||
"gulp-bom": "^3.0.0",
|
||||
"gulp-buffer": "0.0.2",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-flatmap": "^1.0.2",
|
||||
"gulp-gunzip": "^1.0.0",
|
||||
"gulp-gzip": "^1.4.2",
|
||||
"gulp-json-editor": "^2.5.0",
|
||||
"gulp-plumber": "^1.2.0",
|
||||
"gulp-rename": "^1.2.0",
|
||||
"gulp-replace": "^0.5.4",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-svgmin": "^4.1.0",
|
||||
"gulp-untar": "^0.0.7",
|
||||
"husky": "^0.13.1",
|
||||
"innosetup": "^6.4.1",
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"istanbul-lib-instrument": "^6.0.1",
|
||||
"istanbul-lib-report": "^3.0.0",
|
||||
"istanbul-lib-source-maps": "^4.0.1",
|
||||
"istanbul-reports": "^3.1.5",
|
||||
"lazy.js": "^0.4.2",
|
||||
"merge-options": "^1.0.1",
|
||||
"mime": "^1.4.1",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimist": "^1.2.6",
|
||||
"mocha": "^10.8.2",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"os-browserify": "^0.3.0",
|
||||
"p-all": "^1.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"del-cli": "^5.1.0",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
"source-map-support": "^0.3.2",
|
||||
"style-loader": "^3.3.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.6.3",
|
||||
"tsup": "^8.5.0",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-stream": "^7.0.0",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaserver": "^0.4.0"
|
||||
},
|
||||
"overrides": {
|
||||
"node-gyp-build": "4.8.1",
|
||||
"kerberos@2.1.1": {
|
||||
"node-addon-api": "7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
jetbrains/host/server-cli.ts
Normal file
40
jetbrains/host/server-cli.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import "./bootstrap-server.js" // this MUST come before other imports as it changes global state
|
||||
import { dirname, join } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { devInjectNodeModuleLookupPath } from "./bootstrap-node.js"
|
||||
import { bootstrapESM } from "./bootstrap-esm.js"
|
||||
import { resolveNLSConfiguration } from "./deps/vscode/vs/base/node/nls.js"
|
||||
import { product } from "./bootstrap-meta.js"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// NLS
|
||||
const nlsConfiguration = await resolveNLSConfiguration({
|
||||
userLocale: "en",
|
||||
osLocale: "en",
|
||||
commit: product.commit,
|
||||
userDataPath: "",
|
||||
nlsMetadataPath: __dirname,
|
||||
})
|
||||
process.env["VSCODE_NLS_CONFIG"] = JSON.stringify(nlsConfiguration) // required for `bootstrap-esm` to pick up NLS messages
|
||||
|
||||
if (process.env["VSCODE_DEV"]) {
|
||||
// When running out of sources, we need to load node modules from remote/node_modules,
|
||||
// which are compiled against nodejs, not electron
|
||||
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] =
|
||||
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] || join(__dirname, "..", "remote", "node_modules")
|
||||
devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
|
||||
} else {
|
||||
delete process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]
|
||||
}
|
||||
|
||||
// Bootstrap ESM
|
||||
await bootstrapESM()
|
||||
|
||||
// Load Server
|
||||
await import("./deps/vscode/vs/server/node/server.cli.js")
|
||||
328
jetbrains/host/server-main.ts
Normal file
328
jetbrains/host/server-main.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import "./bootstrap-server.js" // this MUST come before other imports as it changes global state
|
||||
import * as path from "path"
|
||||
import * as http from "http"
|
||||
import { AddressInfo } from "net"
|
||||
import * as os from "os"
|
||||
import * as readline from "readline"
|
||||
import { performance } from "perf_hooks"
|
||||
import { fileURLToPath } from "url"
|
||||
import minimist from "minimist"
|
||||
import { devInjectNodeModuleLookupPath, removeGlobalNodeJsModuleLookupPaths } from "./bootstrap-node.js"
|
||||
import { bootstrapESM } from "./bootstrap-esm.js"
|
||||
import { resolveNLSConfiguration } from "./deps/vscode/vs/base/node/nls.js"
|
||||
import { product } from "./bootstrap-meta.js"
|
||||
import * as perf from "./deps/vscode/vs/base/common/performance.js"
|
||||
import { INLSConfiguration } from "./deps/vscode/vs/nls.js"
|
||||
import { IServerAPI } from "./deps/vscode/vs/server/node/remoteExtensionHostAgentServer.js"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
perf.mark("code/server/start")
|
||||
;(globalThis as any).vscodeServerStartTime = performance.now()
|
||||
|
||||
// Do a quick parse to determine if a server or the cli needs to be started
|
||||
const parsedArgs = minimist(process.argv.slice(2), {
|
||||
boolean: [
|
||||
"start-server",
|
||||
"list-extensions",
|
||||
"print-ip-address",
|
||||
"help",
|
||||
"version",
|
||||
"accept-server-license-terms",
|
||||
"update-extensions",
|
||||
],
|
||||
string: [
|
||||
"install-extension",
|
||||
"install-builtin-extension",
|
||||
"uninstall-extension",
|
||||
"locate-extension",
|
||||
"socket-path",
|
||||
"host",
|
||||
"port",
|
||||
"compatibility",
|
||||
],
|
||||
alias: { help: "h", version: "v" },
|
||||
})
|
||||
;["host", "port", "accept-server-license-terms"].forEach((e) => {
|
||||
if (!parsedArgs[e]) {
|
||||
const envValue = process.env[`VSCODE_SERVER_${e.toUpperCase().replace("-", "_")}`]
|
||||
if (envValue) {
|
||||
parsedArgs[e] = envValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const extensionLookupArgs = ["list-extensions", "locate-extension"]
|
||||
const extensionInstallArgs = [
|
||||
"install-extension",
|
||||
"install-builtin-extension",
|
||||
"uninstall-extension",
|
||||
"update-extensions",
|
||||
]
|
||||
|
||||
const shouldSpawnCli =
|
||||
parsedArgs.help ||
|
||||
parsedArgs.version ||
|
||||
extensionLookupArgs.some((a) => !!parsedArgs[a]) ||
|
||||
(extensionInstallArgs.some((a) => !!parsedArgs[a]) && !parsedArgs["start-server"])
|
||||
|
||||
const nlsConfiguration = await resolveNLSConfiguration({
|
||||
userLocale: "en",
|
||||
osLocale: "en",
|
||||
commit: product.commit,
|
||||
userDataPath: "",
|
||||
nlsMetadataPath: __dirname,
|
||||
})
|
||||
|
||||
if (shouldSpawnCli) {
|
||||
loadCode(nlsConfiguration).then((mod) => {
|
||||
mod.spawnCli()
|
||||
})
|
||||
} else {
|
||||
let _remoteExtensionHostAgentServer: IServerAPI | null = null
|
||||
let _remoteExtensionHostAgentServerPromise: Promise<IServerAPI> | null = null
|
||||
const getRemoteExtensionHostAgentServer = () => {
|
||||
if (!_remoteExtensionHostAgentServerPromise) {
|
||||
_remoteExtensionHostAgentServerPromise = loadCode(nlsConfiguration).then(async (mod) => {
|
||||
const server = await mod.createServer(address)
|
||||
_remoteExtensionHostAgentServer = server
|
||||
return server
|
||||
})
|
||||
}
|
||||
return _remoteExtensionHostAgentServerPromise
|
||||
}
|
||||
|
||||
if (Array.isArray(product.serverLicense) && product.serverLicense.length) {
|
||||
console.log(product.serverLicense.join("\n"))
|
||||
if (product.serverLicensePrompt && parsedArgs["accept-server-license-terms"] !== true) {
|
||||
if (hasStdinWithoutTty()) {
|
||||
console.log("To accept the license terms, start the server with --accept-server-license-terms")
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
const accept = await prompt(product.serverLicensePrompt)
|
||||
if (!accept) {
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let firstRequest = true
|
||||
let firstWebSocket = true
|
||||
|
||||
let address: string | AddressInfo | null = null
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (firstRequest) {
|
||||
firstRequest = false
|
||||
perf.mark("code/server/firstRequest")
|
||||
}
|
||||
const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer()
|
||||
return remoteExtensionHostAgentServer.handleRequest(req, res)
|
||||
})
|
||||
server.on("upgrade", async (req, socket) => {
|
||||
if (firstWebSocket) {
|
||||
firstWebSocket = false
|
||||
perf.mark("code/server/firstWebSocket")
|
||||
}
|
||||
const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer()
|
||||
// @ts-ignore
|
||||
return remoteExtensionHostAgentServer.handleUpgrade(req, socket)
|
||||
})
|
||||
server.on("error", async (err) => {
|
||||
const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer()
|
||||
return remoteExtensionHostAgentServer.handleServerError(err)
|
||||
})
|
||||
|
||||
const host =
|
||||
sanitizeStringArg(parsedArgs["host"]) || (parsedArgs["compatibility"] !== "1.63" ? "localhost" : undefined)
|
||||
const nodeListenOptions = parsedArgs["socket-path"]
|
||||
? { path: sanitizeStringArg(parsedArgs["socket-path"]) }
|
||||
: { host, port: await parsePort(host, sanitizeStringArg(parsedArgs["port"])) }
|
||||
server.listen(nodeListenOptions, async () => {
|
||||
let output =
|
||||
Array.isArray(product.serverGreeting) && product.serverGreeting.length
|
||||
? `\n\n${product.serverGreeting.join("\n")}\n\n`
|
||||
: ``
|
||||
|
||||
if (typeof nodeListenOptions.port === "number" && parsedArgs["print-ip-address"]) {
|
||||
const ifaces = os.networkInterfaces()
|
||||
Object.keys(ifaces).forEach(function (ifname) {
|
||||
ifaces[ifname]?.forEach(function (iface) {
|
||||
if (!iface.internal && iface.family === "IPv4") {
|
||||
output += `IP Address: ${iface.address}\n`
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
address = server.address()
|
||||
if (address === null) {
|
||||
throw new Error("Unexpected server address")
|
||||
}
|
||||
|
||||
output += `Server bound to ${typeof address === "string" ? address : `${address.address}:${address.port} (${address.family})`}\n`
|
||||
// Do not change this line. VS Code looks for this in the output.
|
||||
output += `Extension host agent listening on ${typeof address === "string" ? address : address.port}\n`
|
||||
console.log(output)
|
||||
|
||||
perf.mark("code/server/started")
|
||||
;(globalThis as any).vscodeServerListenTime = performance.now()
|
||||
|
||||
await getRemoteExtensionHostAgentServer()
|
||||
})
|
||||
|
||||
process.on("exit", () => {
|
||||
server.close()
|
||||
if (_remoteExtensionHostAgentServer) {
|
||||
_remoteExtensionHostAgentServer.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sanitizeStringArg(val: any): string | undefined {
|
||||
if (Array.isArray(val)) {
|
||||
// if an argument is passed multiple times, minimist creates an array
|
||||
val = val.pop() // take the last item
|
||||
}
|
||||
return typeof val === "string" ? val : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* If `--port` is specified and describes a single port, connect to that port.
|
||||
*
|
||||
* If `--port`describes a port range
|
||||
* then find a free port in that range. Throw error if no
|
||||
* free port available in range.
|
||||
*
|
||||
* In absence of specified ports, connect to port 8000.
|
||||
*/
|
||||
async function parsePort(host: string | undefined, strPort: string | undefined): Promise<number> {
|
||||
if (strPort) {
|
||||
let range: { start: number; end: number } | undefined
|
||||
if (strPort.match(/^\d+$/)) {
|
||||
return parseInt(strPort, 10)
|
||||
} else if ((range = parseRange(strPort))) {
|
||||
const port = await findFreePort(host, range.start, range.end)
|
||||
if (port !== undefined) {
|
||||
return port
|
||||
}
|
||||
// Remote-SSH extension relies on this exact port error message, treat as an API
|
||||
console.warn(`--port: Could not find free port in range: ${range.start} - ${range.end} (inclusive).`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.warn(
|
||||
`--port "${strPort}" is not a valid number or range. Ranges must be in the form 'from-to' with 'from' an integer larger than 0 and not larger than 'end'.`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
return 8000
|
||||
}
|
||||
|
||||
function parseRange(strRange: string): { start: number; end: number } | undefined {
|
||||
const match = strRange.match(/^(\d+)-(\d+)$/)
|
||||
if (match) {
|
||||
const start = parseInt(match[1], 10),
|
||||
end = parseInt(match[2], 10)
|
||||
if (start > 0 && start <= end && end <= 65535) {
|
||||
return { start, end }
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting at the `start` port, look for a free port incrementing
|
||||
* by 1 until `end` inclusive. If no free port is found, undefined is returned.
|
||||
*/
|
||||
async function findFreePort(host: string | undefined, start: number, end: number): Promise<number | undefined> {
|
||||
const testPort = (port: number) => {
|
||||
return new Promise((resolve) => {
|
||||
const server = http.createServer()
|
||||
server
|
||||
.listen(port, host, () => {
|
||||
server.close()
|
||||
resolve(true)
|
||||
})
|
||||
.on("error", () => {
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
for (let port = start; port <= end; port++) {
|
||||
if (await testPort(port)) {
|
||||
return port
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function loadCode(nlsConfiguration: INLSConfiguration) {
|
||||
// required for `bootstrap-esm` to pick up NLS messages
|
||||
process.env["VSCODE_NLS_CONFIG"] = JSON.stringify(nlsConfiguration)
|
||||
|
||||
// See https://github.com/microsoft/vscode-remote-release/issues/6543
|
||||
// We would normally install a SIGPIPE listener in bootstrap-node.js
|
||||
// But in certain situations, the console itself can be in a broken pipe state
|
||||
// so logging SIGPIPE to the console will cause an infinite async loop
|
||||
process.env["VSCODE_HANDLES_SIGPIPE"] = "true"
|
||||
|
||||
if (process.env["VSCODE_DEV"]) {
|
||||
// When running out of sources, we need to load node modules from remote/node_modules,
|
||||
// which are compiled against nodejs, not electron
|
||||
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] =
|
||||
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] ||
|
||||
path.join(__dirname, "..", "remote", "node_modules")
|
||||
devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
|
||||
} else {
|
||||
delete process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]
|
||||
}
|
||||
|
||||
// Remove global paths from the node module lookup (node.js only)
|
||||
removeGlobalNodeJsModuleLookupPaths()
|
||||
|
||||
// Bootstrap ESM
|
||||
await bootstrapESM()
|
||||
|
||||
// Load Server
|
||||
return import("./deps/vscode/vs/server/node/server.main.js")
|
||||
}
|
||||
|
||||
function hasStdinWithoutTty(): boolean {
|
||||
try {
|
||||
return !process.stdin.isTTY // Via https://twitter.com/MylesBorins/status/782009479382626304
|
||||
} catch (error) {
|
||||
// Windows workaround for https://github.com/nodejs/node/issues/11656
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function prompt(question: string): Promise<boolean> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
rl.question(question + " ", async function (data) {
|
||||
rl.close()
|
||||
const str = data.toString().trim().toLowerCase()
|
||||
if (str === "" || str === "y" || str === "yes") {
|
||||
resolve(true)
|
||||
} else if (str === "n" || str === "no") {
|
||||
resolve(false)
|
||||
} else {
|
||||
process.stdout.write("\nInvalid Response. Answer either yes (y, yes) or no (n, no)\n")
|
||||
resolve(await prompt(question))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
6
jetbrains/host/src/config.ts
Normal file
6
jetbrains/host/src/config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const DEBUG_PORT = 9229
|
||||
284
jetbrains/host/src/extension.ts
Normal file
284
jetbrains/host/src/extension.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import net from "net"
|
||||
import start from "../deps/vscode/vs/workbench/api/node/extensionHostProcess.js"
|
||||
import { FileRPCProtocolLogger } from "../deps/vscode/vs/workbench/services/extensions/common/fileRPCProtocolLogger.js"
|
||||
import { RequestInitiator } from "../deps/vscode/vs/workbench/services/extensions/common/rpcProtocol.js"
|
||||
|
||||
// Create global logger instance and export for use by other modules
|
||||
export const fileLoggerGlobal = new FileRPCProtocolLogger("extension")
|
||||
|
||||
// Command line argument parsing
|
||||
const args = process.argv.slice(2)
|
||||
console.log("args:", args)
|
||||
const LISTEN_MODE = args.includes("--listen") || process.env.VSCODE_EXTHOST_LISTEN === "true"
|
||||
const PORT = parseInt(
|
||||
args.find((arg) => arg.startsWith("--vscode-socket-port="))?.substring(21) ||
|
||||
process.env.VSCODE_EXTHOST_DEBUG_PORT ||
|
||||
"51234",
|
||||
10,
|
||||
)
|
||||
const SOCKET_HOST =
|
||||
args.find((arg) => arg.startsWith("--vscode-socket-host="))?.substring(21) ||
|
||||
process.env.VSCODE_EXTHOST_SOCKET_HOST ||
|
||||
"127.0.0.1"
|
||||
const WILL_SEND_SOCKET =
|
||||
args.find((arg) => arg.startsWith("--vscode-will-send-socket="))?.substring(26) ||
|
||||
process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET ||
|
||||
"0"
|
||||
const pipeName = process.env.VSCODE_EXTHOST_IPC_HOOK
|
||||
|
||||
console.log(`Extension host starting in ${LISTEN_MODE ? "LISTEN" : "CONNECT"} mode`)
|
||||
console.log("PORT:", PORT)
|
||||
console.log("SOCKET_HOST:", SOCKET_HOST)
|
||||
console.log("WILL_SEND_SOCKET:", WILL_SEND_SOCKET)
|
||||
console.log("pipeName:", pipeName)
|
||||
|
||||
if (pipeName) {
|
||||
console.log("Using pipeName, connection will be handled by VSCode IPC")
|
||||
} else {
|
||||
// Reset parameter values back to environment variables
|
||||
process.env.VSCODE_EXTHOST_SOCKET_PORT = PORT.toString()
|
||||
process.env.VSCODE_EXTHOST_SOCKET_HOST = SOCKET_HOST
|
||||
process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET = WILL_SEND_SOCKET
|
||||
console.log("set send socket:", process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET)
|
||||
|
||||
// Save original process methods
|
||||
const originalProcessOn = process.on
|
||||
const originalProcessSend = process.send || (() => false)
|
||||
|
||||
// Store message event handlers
|
||||
const messageHandlers: ((message: any, socket?: net.Socket) => void)[] = []
|
||||
|
||||
// Reconnection related variables
|
||||
let isReconnecting = false
|
||||
let reconnectAttempts = 0
|
||||
const MAX_RECONNECT_ATTEMPTS = 5
|
||||
const RECONNECT_DELAY = 1000 // 1 second
|
||||
|
||||
// Override process.on
|
||||
process.on = function (event: string, listener: (...args: any[]) => void): any {
|
||||
if (event === "message") {
|
||||
messageHandlers.push((message: any, socket?: net.Socket) => {
|
||||
// Check listener parameter count
|
||||
const paramCount = listener.length
|
||||
if (paramCount === 1) {
|
||||
// If only one parameter, pass only message
|
||||
listener(message)
|
||||
} else {
|
||||
// If multiple parameters, pass message and socket
|
||||
listener(message, socket)
|
||||
}
|
||||
})
|
||||
}
|
||||
return originalProcessOn.call(process, event, listener)
|
||||
}
|
||||
|
||||
// Override process.send
|
||||
process.send = function (message: any): boolean {
|
||||
if (message?.type === "VSCODE_EXTHOST_IPC_READY") {
|
||||
console.log("Extension host process is ready to receive socket")
|
||||
if (LISTEN_MODE) {
|
||||
startServer()
|
||||
} else {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
// Call original process.send
|
||||
return originalProcessSend.call(process, message)
|
||||
}
|
||||
|
||||
// Start server mode (for debugging)
|
||||
function startServer() {
|
||||
const server = net.createServer((socket) => {
|
||||
console.log("Main process connected to extension host")
|
||||
socket.setNoDelay(true)
|
||||
|
||||
// Prepare message to send to VSCode module
|
||||
const socketMessage = {
|
||||
type: "VSCODE_EXTHOST_IPC_SOCKET",
|
||||
initialDataChunk: "",
|
||||
skipWebSocketFrames: true,
|
||||
permessageDeflate: false,
|
||||
inflateBytes: "",
|
||||
}
|
||||
|
||||
// Call all saved message handlers
|
||||
messageHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(socketMessage, socket)
|
||||
} catch (error) {
|
||||
console.error("Error in message handler:", error)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("error", (error) => {
|
||||
console.error("Socket error:", error)
|
||||
// Don't close server, wait for reconnection
|
||||
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Socket error:", error)
|
||||
})
|
||||
|
||||
socket.on("close", () => {
|
||||
console.log("Client connection closed, waiting for new connections...")
|
||||
fileLoggerGlobal.logOutgoing(
|
||||
0,
|
||||
0,
|
||||
RequestInitiator.LocalSide,
|
||||
"Client connection closed, waiting for new connections...",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Prevent server timeout closure, keep process active
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
if (server.listening) {
|
||||
console.log("Server still waiting for connections...")
|
||||
}
|
||||
}, 60000) // Print a log every minute to keep the process alive
|
||||
|
||||
// Ensure timer cleanup on process exit
|
||||
process.on("exit", () => {
|
||||
clearInterval(keepAliveInterval)
|
||||
})
|
||||
|
||||
server.listen(PORT, "127.0.0.1", () => {
|
||||
console.log(`Extension host server listening on 127.0.0.1:${PORT}`)
|
||||
console.log("Waiting for main process to connect...")
|
||||
})
|
||||
|
||||
server.on("error", (error) => {
|
||||
console.error("Server error:", error)
|
||||
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Server error:", error)
|
||||
// No longer exit process, only log error
|
||||
// Try to restart server
|
||||
setTimeout(() => {
|
||||
if (!server.listening) {
|
||||
console.log("Attempting to restart server after error...")
|
||||
try {
|
||||
server.listen(PORT, "127.0.0.1")
|
||||
} catch (e) {
|
||||
console.error("Failed to restart server:", e)
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
}
|
||||
|
||||
// Client mode (original behavior)
|
||||
function connect() {
|
||||
if (isReconnecting) {
|
||||
console.log("Already in reconnection process, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Check connection method
|
||||
// console.log("get send socket:", process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET);
|
||||
// const useSocket = process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET === "1";
|
||||
|
||||
// if (!useSocket) {
|
||||
// throw new Error('No connection method specified. Please set either VSCODE_EXTHOST_IPC_HOOK or VSCODE_EXTHOST_WILL_SEND_SOCKET');
|
||||
// }
|
||||
|
||||
// Use regular TCP Socket
|
||||
const host = process.env.VSCODE_EXTHOST_SOCKET_HOST || "127.0.0.1"
|
||||
const port = parseInt(process.env.VSCODE_EXTHOST_SOCKET_PORT || "0", 10)
|
||||
|
||||
if (!port) {
|
||||
throw new Error("Invalid socket port")
|
||||
}
|
||||
|
||||
console.log(`Attempting to connect to ${host}:${port}`)
|
||||
|
||||
// Establish socket connection
|
||||
const socket = net.createConnection(port, host)
|
||||
// Set the noDelay option for the socket
|
||||
socket.setNoDelay(true)
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected to main server")
|
||||
isReconnecting = false
|
||||
reconnectAttempts = 0
|
||||
|
||||
// Prepare the message to be sent to the VSCode module
|
||||
const socketMessage = {
|
||||
type: "VSCODE_EXTHOST_IPC_SOCKET",
|
||||
initialDataChunk: "",
|
||||
skipWebSocketFrames: true,
|
||||
permessageDeflate: false,
|
||||
inflateBytes: "",
|
||||
}
|
||||
|
||||
// Call all saved message handler functions
|
||||
messageHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(socketMessage, socket)
|
||||
} catch (error) {
|
||||
console.error("Error in message handler:", error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
socket.on("error", (error: Error) => {
|
||||
console.error("Socket connection error:", error)
|
||||
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Socket connection error:", error)
|
||||
handleDisconnect()
|
||||
})
|
||||
|
||||
socket.on("close", () => {
|
||||
console.log("Socket connection closed")
|
||||
handleDisconnect()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Connection error:", error)
|
||||
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Connection error:", error)
|
||||
handleDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle disconnection
|
||||
async function handleDisconnect() {
|
||||
if (isReconnecting) {
|
||||
console.log("Already in reconnection process, skipping")
|
||||
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Already in reconnection process, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
console.error("Max reconnection attempts reached. Giving up.")
|
||||
fileLoggerGlobal.logOutgoing(
|
||||
0,
|
||||
0,
|
||||
RequestInitiator.LocalSide,
|
||||
"Max reconnection attempts reached. Giving up.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
isReconnecting = true
|
||||
reconnectAttempts++
|
||||
|
||||
console.log(`Attempting to reconnect (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
|
||||
|
||||
// Retry after waiting for a period of time
|
||||
console.log(`Waiting ${RECONNECT_DELAY}ms before reconnecting...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY))
|
||||
console.log("Reconnection delay finished, attempting to connect...")
|
||||
|
||||
// Reset reconnection state to allow new reconnection attempts
|
||||
isReconnecting = false
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Starting extension host process...")
|
||||
|
||||
// Adjust logic: only start directly in non-LISTEN mode
|
||||
if (LISTEN_MODE) {
|
||||
process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET = "1"
|
||||
}
|
||||
start()
|
||||
101
jetbrains/host/src/extensionManager.ts
Normal file
101
jetbrains/host/src/extensionManager.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
ExtensionIdentifier,
|
||||
IExtensionDescription,
|
||||
TargetPlatform,
|
||||
} from "../deps/vscode/vs/platform/extensions/common/extensions.js"
|
||||
import { URI } from "../deps/vscode/vs/base/common/uri.js"
|
||||
import { ExtHostContext } from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
|
||||
import { IRPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
export class ExtensionManager {
|
||||
private extensionDescriptions: Map<string, IExtensionDescription> = new Map()
|
||||
|
||||
/**
|
||||
* Parse extension description information
|
||||
* @param extensionPath Extension path
|
||||
* @returns Extension description object
|
||||
*/
|
||||
private parseExtensionDescription(extensionPath: string): IExtensionDescription {
|
||||
const packageJsonPath = path.join(extensionPath, "extension.package.json")
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
||||
|
||||
return {
|
||||
identifier: new ExtensionIdentifier(packageJson.name),
|
||||
name: packageJson.name,
|
||||
displayName: packageJson.displayName,
|
||||
description: packageJson.description,
|
||||
version: packageJson.version,
|
||||
publisher: packageJson.publisher,
|
||||
main: "./extension.cjs",
|
||||
activationEvents: packageJson.activationEvents || ["onStartupFinished"],
|
||||
extensionLocation: URI.file(path.resolve(extensionPath)),
|
||||
targetPlatform: TargetPlatform.UNIVERSAL,
|
||||
isBuiltin: false,
|
||||
isUserBuiltin: false,
|
||||
isUnderDevelopment: false,
|
||||
engines: packageJson.engines || { vscode: "^1.0.0" },
|
||||
preRelease: false,
|
||||
capabilities: {},
|
||||
extensionDependencies: packageJson.extensionDependencies || [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parsed extension descriptions
|
||||
* @returns Extension description array
|
||||
*/
|
||||
public getAllExtensionDescriptions(): IExtensionDescription[] {
|
||||
return Array.from(this.extensionDescriptions.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description information for specified extension
|
||||
* @param extensionId Extension ID
|
||||
* @returns Extension description object, or undefined if not exists
|
||||
*/
|
||||
public getExtensionDescription(extensionId: string): IExtensionDescription | undefined {
|
||||
return this.extensionDescriptions.get(extensionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an extension
|
||||
* @param extensionPath Extension path
|
||||
* @returns Extension description object
|
||||
*/
|
||||
public registerExtension(extensionPath: string): IExtensionDescription {
|
||||
const extensionDescription = this.parseExtensionDescription(extensionPath)
|
||||
this.extensionDescriptions.set(extensionDescription.identifier.value, extensionDescription)
|
||||
return extensionDescription
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a registered extension
|
||||
* @param extensionId Extension ID
|
||||
* @param protocol RPC protocol
|
||||
*/
|
||||
public async activateExtension(extensionId: string, protocol: IRPCProtocol): Promise<void> {
|
||||
const extensionDescription = this.extensionDescriptions.get(extensionId)
|
||||
if (!extensionDescription) {
|
||||
throw new Error(`Extension ${extensionId} is not registered`)
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionService = protocol.getProxy(ExtHostContext.ExtHostExtensionService)
|
||||
await extensionService.$activate(extensionDescription.identifier, {
|
||||
startup: true,
|
||||
extensionId: extensionDescription.identifier,
|
||||
activationEvent: "api",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to activate extension ${extensionId}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
202
jetbrains/host/src/main.ts
Normal file
202
jetbrains/host/src/main.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fork } from "child_process"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import * as net from "net"
|
||||
import { VSBuffer } from "../deps/vscode/vs/base/common/buffer.js"
|
||||
import { NodeSocket } from "../deps/vscode/vs/base/parts/ipc/node/ipc.net.js"
|
||||
import { PersistentProtocol } from "../deps/vscode/vs/base/parts/ipc/common/ipc.net.js"
|
||||
import { DEBUG_PORT } from "./config.js"
|
||||
import {
|
||||
MessageType,
|
||||
createMessageOfType,
|
||||
isMessageOfType,
|
||||
UIKind,
|
||||
IExtensionHostInitData,
|
||||
} from "../deps/vscode/vs/workbench/services/extensions/common/extensionHostProtocol.js"
|
||||
import { SocketCloseEvent, SocketCloseEventType } from "../deps/vscode/vs/base/parts/ipc/common/ipc.net.js"
|
||||
import { IDisposable } from "../deps/vscode/vs/base/common/lifecycle.js"
|
||||
import { URI } from "../deps/vscode/vs/base/common/uri.js"
|
||||
import { RPCManager } from "./rpcManager.js"
|
||||
import { ExtensionManager } from "./extensionManager.js"
|
||||
|
||||
// Get current file directory path
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Create ExtensionManager instance and register extension
|
||||
const extensionManager = new ExtensionManager()
|
||||
const rooCodeIdentifier = extensionManager.registerExtension("roo-code").identifier
|
||||
|
||||
// Declare extension host process variables
|
||||
let extHostProcess: ReturnType<typeof fork>
|
||||
let protocol: PersistentProtocol | null = null
|
||||
let rpcManager: RPCManager | null = null
|
||||
|
||||
// Create socket server
|
||||
const server = net.createServer((socket) => {
|
||||
console.log("Someone connected to main server")
|
||||
|
||||
// Set socket noDelay option
|
||||
socket.setNoDelay(true)
|
||||
|
||||
// Wrap socket with NodeSocket
|
||||
const nodeSocket = new NodeSocket(socket)
|
||||
|
||||
// Listen for NodeSocket close events
|
||||
const closeDisposable: IDisposable = nodeSocket.onClose((event: SocketCloseEvent | undefined) => {
|
||||
console.log("NodeSocket close event received")
|
||||
if (event?.type === SocketCloseEventType.NodeSocketCloseEvent) {
|
||||
if (event.hadError) {
|
||||
console.error("Socket closed with error:", event.error)
|
||||
} else {
|
||||
console.log("Socket closed normally")
|
||||
}
|
||||
}
|
||||
closeDisposable.dispose()
|
||||
})
|
||||
|
||||
// Create PersistentProtocol instance
|
||||
protocol = new PersistentProtocol({
|
||||
socket: nodeSocket,
|
||||
initialChunk: null,
|
||||
})
|
||||
|
||||
// Set protocol message handler
|
||||
protocol.onMessage((message) => {
|
||||
if (isMessageOfType(message, MessageType.Ready)) {
|
||||
console.log("Extension host is ready")
|
||||
// Send initialization data
|
||||
const initData: IExtensionHostInitData = {
|
||||
commit: "development",
|
||||
version: "1.0.0",
|
||||
quality: undefined,
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: false,
|
||||
appName: "VSCodeAPIHook",
|
||||
appHost: "node",
|
||||
appLanguage: "en",
|
||||
appUriScheme: "vscode",
|
||||
appRoot: URI.file(__dirname),
|
||||
globalStorageHome: URI.file(path.join(__dirname, "globalStorage")),
|
||||
workspaceStorageHome: URI.file(path.join(__dirname, "workspaceStorage")),
|
||||
extensionDevelopmentLocationURI: undefined,
|
||||
extensionTestsLocationURI: undefined,
|
||||
useHostProxy: false,
|
||||
skipWorkspaceStorageLock: false,
|
||||
isExtensionTelemetryLoggingOnly: false,
|
||||
},
|
||||
workspace: {
|
||||
id: "development-workspace",
|
||||
name: "Development Workspace",
|
||||
transient: false,
|
||||
configuration: null,
|
||||
isUntitled: false,
|
||||
},
|
||||
remote: {
|
||||
authority: undefined,
|
||||
connectionData: null,
|
||||
isRemote: false,
|
||||
},
|
||||
extensions: {
|
||||
versionId: 1,
|
||||
allExtensions: extensionManager.getAllExtensionDescriptions(),
|
||||
myExtensions: extensionManager.getAllExtensionDescriptions().map((ext) => ext.identifier),
|
||||
activationEvents: extensionManager.getAllExtensionDescriptions().reduce(
|
||||
(events, ext) => {
|
||||
if (ext.activationEvents) {
|
||||
events[ext.identifier.value] = ext.activationEvents
|
||||
}
|
||||
return events
|
||||
},
|
||||
{} as { [extensionId: string]: string[] },
|
||||
),
|
||||
},
|
||||
telemetryInfo: {
|
||||
sessionId: "development-session",
|
||||
machineId: "development-machine",
|
||||
sqmId: "",
|
||||
devDeviceId: "",
|
||||
firstSessionDate: new Date().toISOString(),
|
||||
msftInternal: false,
|
||||
},
|
||||
logLevel: 0, // Info level
|
||||
loggers: [],
|
||||
logsLocation: URI.file(path.join(__dirname, "logs")),
|
||||
autoStart: true,
|
||||
consoleForward: {
|
||||
includeStack: false,
|
||||
logNative: false,
|
||||
},
|
||||
uiKind: UIKind.Desktop,
|
||||
}
|
||||
protocol?.send(VSBuffer.fromString(JSON.stringify(initData)))
|
||||
} else if (isMessageOfType(message, MessageType.Initialized)) {
|
||||
console.log("Extension host initialized")
|
||||
// Create RPCManager instance
|
||||
rpcManager = new RPCManager(protocol!, extensionManager)
|
||||
|
||||
rpcManager.startInitialize()
|
||||
|
||||
// Activate rooCode plugin
|
||||
const rpcProtocol = rpcManager.getRPCProtocol()
|
||||
if (rpcProtocol) {
|
||||
extensionManager.activateExtension(rooCodeIdentifier.value, rpcProtocol).catch((error: Error) => {
|
||||
console.error("Failed to load rooCode plugin:", error)
|
||||
})
|
||||
} else {
|
||||
console.error("Failed to get RPCProtocol from RPCManager")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function startExtensionHostProcess() {
|
||||
process.env.VSCODE_DEBUG = "true"
|
||||
let nodeOptions = process.env.VSCODE_DEBUG ? `--inspect-brk=9229` : `--inspect=${DEBUG_PORT}`
|
||||
console.log("will start extension host process with options:", nodeOptions)
|
||||
|
||||
// Create extension host process and pass environment variables
|
||||
extHostProcess = fork(path.join(__dirname, "extension.js"), [], {
|
||||
env: {
|
||||
...process.env,
|
||||
VSCODE_EXTHOST_WILL_SEND_SOCKET: "1",
|
||||
VSCODE_EXTHOST_SOCKET_HOST: "127.0.0.1",
|
||||
VSCODE_EXTHOST_SOCKET_PORT: (server.address() as net.AddressInfo)?.port?.toString() || "0",
|
||||
NODE_OPTIONS: nodeOptions,
|
||||
},
|
||||
})
|
||||
|
||||
// Handle extension host process exit
|
||||
extHostProcess.on("exit", (code: number | null, signal: string | null) => {
|
||||
console.log(`Extension host process exited with code ${code} and signal ${signal}`)
|
||||
server.close()
|
||||
})
|
||||
}
|
||||
|
||||
// Listen on random port
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address()
|
||||
if (address && typeof address !== "string") {
|
||||
console.log(`Server listening on port ${address.port}`)
|
||||
startExtensionHostProcess()
|
||||
}
|
||||
})
|
||||
|
||||
// Handle process exit
|
||||
process.on("SIGINT", () => {
|
||||
console.log("Cleaning up...")
|
||||
if (protocol) {
|
||||
protocol.send(createMessageOfType(MessageType.Terminate))
|
||||
}
|
||||
server.close()
|
||||
if (extHostProcess) {
|
||||
extHostProcess.kill()
|
||||
}
|
||||
process.exit(0)
|
||||
})
|
||||
997
jetbrains/host/src/rpcManager.ts
Normal file
997
jetbrains/host/src/rpcManager.ts
Normal file
|
|
@ -0,0 +1,997 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { RPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/rpcProtocol.js"
|
||||
import { IRPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
|
||||
import { PersistentProtocol } from "../deps/vscode/vs/base/parts/ipc/common/ipc.net.js"
|
||||
import { MainContext, ExtHostContext } from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
|
||||
import {
|
||||
IRPCProtocolLogger,
|
||||
RequestInitiator,
|
||||
} from "../deps/vscode/vs/workbench/services/extensions/common/rpcProtocol.js"
|
||||
import { UriComponents, UriDto } from "../deps/vscode/vs/base/common/uri.js"
|
||||
import { LogLevel } from "../deps/vscode/vs/platform/log/common/log.js"
|
||||
import { ILoggerResource } from "../deps/vscode/vs/platform/log/common/log.js"
|
||||
import { TerminalLaunchConfig } from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
|
||||
import { IRawFileMatch2 } from "../deps/vscode/vs/workbench/services/search/common/search.js"
|
||||
import { VSBuffer } from "../deps/vscode/vs/base/common/buffer.js"
|
||||
import { SerializedError, transformErrorFromSerialization } from "../deps/vscode/vs/base/common/errors.js"
|
||||
import { IRemoteConsoleLog } from "../deps/vscode/vs/base/common/console.js"
|
||||
import { FileType, FilePermission, FileSystemProviderErrorCode } from "../deps/vscode/vs/platform/files/common/files.js"
|
||||
import * as fs from "fs"
|
||||
import { promisify } from "util"
|
||||
import { ConfigurationModel } from "../deps/vscode/vs/platform/configuration/common/configurationModels.js"
|
||||
import { NullLogService } from "../deps/vscode/vs/platform/log/common/log.js"
|
||||
import { ExtensionIdentifier } from "../deps/vscode/vs/platform/extensions/common/extensions.js"
|
||||
import { ExtensionActivationReason } from "../deps/vscode/vs/workbench/services/extensions/common/extensions.js"
|
||||
import { IExtensionDescription } from "../deps/vscode/vs/platform/extensions/common/extensions.js"
|
||||
import { Dto } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
|
||||
import { ExtensionManager } from "./extensionManager.js"
|
||||
import { WebViewManager } from "./webViewManager.js"
|
||||
|
||||
// Promisify Node.js fs functions
|
||||
const fsStat = promisify(fs.stat)
|
||||
const fsReadDir = promisify(fs.readdir)
|
||||
const fsReadFile = promisify(fs.readFile)
|
||||
const fsWriteFile = promisify(fs.writeFile)
|
||||
const fsRename = promisify(fs.rename)
|
||||
const fsCopyFile = promisify(fs.copyFile)
|
||||
const fsUnlink = promisify(fs.unlink)
|
||||
const fsLstat = promisify(fs.lstat)
|
||||
const fsMkdir = promisify(fs.mkdir)
|
||||
|
||||
class RPCLogger implements IRPCProtocolLogger {
|
||||
logIncoming(msgLength: number, req: number, initiator: RequestInitiator, msg: string, data?: any): void {
|
||||
if (msg == "ack") {
|
||||
return
|
||||
}
|
||||
console.log(`[RPC] ExtHost: ${msg}`)
|
||||
}
|
||||
|
||||
logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, msg: string, data?: any): void {
|
||||
if (msg == "ack" || msg == "reply:") {
|
||||
return
|
||||
}
|
||||
console.log(`[RPC] Main: ${msg}`)
|
||||
}
|
||||
}
|
||||
|
||||
export class RPCManager {
|
||||
private rpcProtocol: IRPCProtocol
|
||||
private logger: RPCLogger
|
||||
private extensionManager: ExtensionManager
|
||||
|
||||
constructor(
|
||||
private protocol: PersistentProtocol,
|
||||
extensionManager: ExtensionManager,
|
||||
) {
|
||||
this.logger = new RPCLogger()
|
||||
this.rpcProtocol = new RPCProtocol(this.protocol, this.logger)
|
||||
this.extensionManager = extensionManager
|
||||
this.setupDefaultProtocols()
|
||||
this.setupExtensionRequiredProtocols()
|
||||
this.setupRooCodeRequiredProtocols()
|
||||
}
|
||||
|
||||
public startInitialize(): void {
|
||||
// ExtHostConfiguration
|
||||
const extHostConfiguration = this.rpcProtocol.getProxy(ExtHostContext.ExtHostConfiguration)
|
||||
|
||||
// Send initialization configuration message
|
||||
extHostConfiguration.$initializeConfiguration({
|
||||
defaults: ConfigurationModel.createEmptyModel(new NullLogService()),
|
||||
policy: ConfigurationModel.createEmptyModel(new NullLogService()),
|
||||
application: ConfigurationModel.createEmptyModel(new NullLogService()),
|
||||
userLocal: ConfigurationModel.createEmptyModel(new NullLogService()),
|
||||
userRemote: ConfigurationModel.createEmptyModel(new NullLogService()),
|
||||
workspace: ConfigurationModel.createEmptyModel(new NullLogService()),
|
||||
folders: [],
|
||||
configurationScopes: [],
|
||||
})
|
||||
|
||||
const extHostWorkspace = this.rpcProtocol.getProxy(ExtHostContext.ExtHostWorkspace)
|
||||
|
||||
// Initialize workspace
|
||||
extHostWorkspace.$initializeWorkspace(null, true)
|
||||
}
|
||||
|
||||
// Protocols needed for extHost process startup and initialization
|
||||
public setupDefaultProtocols(): void {
|
||||
if (!this.rpcProtocol) {
|
||||
throw new Error("RPCProtocol not initialized")
|
||||
}
|
||||
|
||||
// MainThreadErrors
|
||||
this.rpcProtocol.set(MainContext.MainThreadErrors, {
|
||||
dispose(): void {
|
||||
// Nothing to do
|
||||
},
|
||||
$onUnexpectedError(err: any | SerializedError): void {
|
||||
if (err && err.$isError) {
|
||||
err = transformErrorFromSerialization(err)
|
||||
}
|
||||
console.error("Unexpected error:", err)
|
||||
/*
|
||||
if (err instanceof Error && err.stack) {
|
||||
console.error('Stack trace:', err.stack);
|
||||
}
|
||||
*/
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadConsole
|
||||
this.rpcProtocol.set(MainContext.MainThreadConsole, {
|
||||
dispose(): void {
|
||||
// Nothing to do
|
||||
},
|
||||
$logExtensionHostMessage(entry: IRemoteConsoleLog): void {
|
||||
// Parse the entry
|
||||
const args = this.parseRemoteConsoleLog(entry)
|
||||
|
||||
// Log based on severity
|
||||
switch (entry.severity) {
|
||||
case "log":
|
||||
case "info":
|
||||
console.log("[Extension Host]", ...args)
|
||||
break
|
||||
case "warn":
|
||||
console.warn("[Extension Host]", ...args)
|
||||
break
|
||||
case "error":
|
||||
console.error("[Extension Host]", ...args)
|
||||
break
|
||||
case "debug":
|
||||
console.debug("[Extension Host]", ...args)
|
||||
break
|
||||
default:
|
||||
console.log("[Extension Host]", ...args)
|
||||
}
|
||||
},
|
||||
parseRemoteConsoleLog(entry: IRemoteConsoleLog): any[] {
|
||||
const args: any[] = []
|
||||
|
||||
try {
|
||||
// Parse the arguments string as JSON
|
||||
const parsedArguments = JSON.parse(entry.arguments)
|
||||
args.push(...parsedArguments)
|
||||
} catch (error) {
|
||||
// If parsing fails, just log the raw arguments string
|
||||
args.push("Unable to log remote console arguments", entry.arguments)
|
||||
}
|
||||
|
||||
return args
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadLogger
|
||||
this.rpcProtocol.set(MainContext.MainThreadLogger, {
|
||||
$log(file: UriComponents, messages: [LogLevel, string][]): void {
|
||||
console.log("Logger message:", { file, messages })
|
||||
},
|
||||
$flush(file: UriComponents): void {
|
||||
console.log("Flush logger:", file)
|
||||
},
|
||||
$createLogger(file: UriComponents, options?: any): Promise<void> {
|
||||
console.log("Create logger:", { file, options })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$registerLogger(logger: UriDto<ILoggerResource>): Promise<void> {
|
||||
console.log("Register logger (id: ", logger.id, ", name: ", logger.name, ")")
|
||||
return Promise.resolve()
|
||||
},
|
||||
$deregisterLogger(resource: UriComponents): Promise<void> {
|
||||
console.log("Deregister logger:", resource)
|
||||
return Promise.resolve()
|
||||
},
|
||||
$setVisibility(resource: UriComponents, visible: boolean): Promise<void> {
|
||||
console.log("Set logger visibility:", { resource, visible })
|
||||
return Promise.resolve()
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadCommands
|
||||
this.rpcProtocol.set(MainContext.MainThreadCommands, {
|
||||
$registerCommand(id: string): void {
|
||||
console.log("Register command:", id)
|
||||
},
|
||||
$unregisterCommand(id: string): void {
|
||||
console.log("Unregister command:", id)
|
||||
},
|
||||
$executeCommand<T>(id: string, ...args: any[]): Promise<T> {
|
||||
console.log("Execute command:", id, args)
|
||||
return Promise.resolve(null as T)
|
||||
},
|
||||
$fireCommandActivationEvent(id: string): void {
|
||||
console.log("Fire command activation event:", id)
|
||||
},
|
||||
$getCommands(): Promise<string[]> {
|
||||
return Promise.resolve([])
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadCommands")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadTerminalService
|
||||
this.rpcProtocol.set(MainContext.MainThreadTerminalService, {
|
||||
$registerProcessSupport(isSupported: boolean): void {
|
||||
console.log("Register process support:", isSupported)
|
||||
},
|
||||
$createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise<void> {
|
||||
console.log("Create terminal:", { extHostTerminalId, config })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$dispose(id: string): void {
|
||||
console.log("Dispose terminal:", id)
|
||||
},
|
||||
$hide(id: string): void {
|
||||
console.log("Hide terminal:", id)
|
||||
},
|
||||
$sendText(id: string, text: string, shouldExecute: boolean): void {
|
||||
console.log("Send text to terminal:", { id, text, shouldExecute })
|
||||
},
|
||||
$show(id: string, preserveFocus: boolean): void {
|
||||
console.log("Show terminal:", { id, preserveFocus })
|
||||
},
|
||||
$registerProfileProvider(id: string, extensionIdentifier: string): void {
|
||||
console.log("Register profile provider:", { id, extensionIdentifier })
|
||||
},
|
||||
$unregisterProfileProvider(id: string): void {
|
||||
console.log("Unregister profile provider:", id)
|
||||
},
|
||||
$registerCompletionProvider(id: string, extensionIdentifier: string, ...triggerCharacters: string[]): void {
|
||||
console.log("Register completion provider:", { id, extensionIdentifier, triggerCharacters })
|
||||
},
|
||||
$unregisterCompletionProvider(id: string): void {
|
||||
console.log("Unregister completion provider:", id)
|
||||
},
|
||||
$registerQuickFixProvider(id: string, extensionIdentifier: string): void {
|
||||
console.log("Register quick fix provider:", { id, extensionIdentifier })
|
||||
},
|
||||
$unregisterQuickFixProvider(id: string): void {
|
||||
console.log("Unregister quick fix provider:", id)
|
||||
},
|
||||
$setEnvironmentVariableCollection(
|
||||
extensionIdentifier: string,
|
||||
persistent: boolean,
|
||||
collection: any,
|
||||
descriptionMap: any,
|
||||
): void {
|
||||
console.log("Set environment variable collection:", {
|
||||
extensionIdentifier,
|
||||
persistent,
|
||||
collection,
|
||||
descriptionMap,
|
||||
})
|
||||
},
|
||||
$startSendingDataEvents(): void {
|
||||
console.log("Start sending data events")
|
||||
},
|
||||
$stopSendingDataEvents(): void {
|
||||
console.log("Stop sending data events")
|
||||
},
|
||||
$startSendingCommandEvents(): void {
|
||||
console.log("Start sending command events")
|
||||
},
|
||||
$stopSendingCommandEvents(): void {
|
||||
console.log("Stop sending command events")
|
||||
},
|
||||
$startLinkProvider(): void {
|
||||
console.log("Start link provider")
|
||||
},
|
||||
$stopLinkProvider(): void {
|
||||
console.log("Stop link provider")
|
||||
},
|
||||
$sendProcessData(terminalId: number, data: string): void {
|
||||
console.log("Send process data:", { terminalId, data })
|
||||
},
|
||||
$sendProcessReady(terminalId: number, pid: number, cwd: string, windowsPty: any): void {
|
||||
console.log("Send process ready:", { terminalId, pid, cwd, windowsPty })
|
||||
},
|
||||
$sendProcessProperty(terminalId: number, property: any): void {
|
||||
console.log("Send process property:", { terminalId, property })
|
||||
},
|
||||
$sendProcessExit(terminalId: number, exitCode: number | undefined): void {
|
||||
console.log("Send process exit:", { terminalId, exitCode })
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadTerminalService")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadWindow
|
||||
this.rpcProtocol.set(MainContext.MainThreadWindow, {
|
||||
$getInitialState(): Promise<{ isFocused: boolean; isActive: boolean }> {
|
||||
console.log("Get initial state")
|
||||
return Promise.resolve({ isFocused: false, isActive: false })
|
||||
},
|
||||
$openUri(uri: UriComponents, uriString: string | undefined, options: any): Promise<boolean> {
|
||||
console.log("Open URI:", { uri, uriString, options })
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
$asExternalUri(uri: UriComponents, options: any): Promise<UriComponents> {
|
||||
console.log("As external URI:", { uri, options })
|
||||
return Promise.resolve(uri)
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadWindow")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadSearch
|
||||
this.rpcProtocol.set(MainContext.MainThreadSearch, {
|
||||
$registerFileSearchProvider(handle: number, scheme: string): void {
|
||||
console.log("Register file search provider:", { handle, scheme })
|
||||
},
|
||||
$registerAITextSearchProvider(handle: number, scheme: string): void {
|
||||
console.log("Register AI text search provider:", { handle, scheme })
|
||||
},
|
||||
$registerTextSearchProvider(handle: number, scheme: string): void {
|
||||
console.log("Register text search provider:", { handle, scheme })
|
||||
},
|
||||
$unregisterProvider(handle: number): void {
|
||||
console.log("Unregister provider:", handle)
|
||||
},
|
||||
$handleFileMatch(handle: number, session: number, data: UriComponents[]): void {
|
||||
console.log("Handle file match:", { handle, session, data })
|
||||
},
|
||||
$handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void {
|
||||
console.log("Handle text match:", { handle, session, data })
|
||||
},
|
||||
$handleTelemetry(eventName: string, data: any): void {
|
||||
console.log("Handle telemetry:", { eventName, data })
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadSearch")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadTask
|
||||
this.rpcProtocol.set(MainContext.MainThreadTask, {
|
||||
$createTaskId(task: any): Promise<string> {
|
||||
console.log("Create task ID:", task)
|
||||
return Promise.resolve("task-id")
|
||||
},
|
||||
$registerTaskProvider(handle: number, type: string): Promise<void> {
|
||||
console.log("Register task provider:", { handle, type })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$unregisterTaskProvider(handle: number): Promise<void> {
|
||||
console.log("Unregister task provider:", handle)
|
||||
return Promise.resolve()
|
||||
},
|
||||
$fetchTasks(filter?: any): Promise<any[]> {
|
||||
console.log("Fetch tasks:", filter)
|
||||
return Promise.resolve([])
|
||||
},
|
||||
$getTaskExecution(value: any): Promise<any> {
|
||||
console.log("Get task execution:", value)
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
$executeTask(task: any): Promise<any> {
|
||||
console.log("Execute task:", task)
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
$terminateTask(id: string): Promise<void> {
|
||||
console.log("Terminate task:", id)
|
||||
return Promise.resolve()
|
||||
},
|
||||
$registerTaskSystem(scheme: string, info: any): void {
|
||||
console.log("Register task system:", { scheme, info })
|
||||
},
|
||||
$customExecutionComplete(id: string, result?: number): Promise<void> {
|
||||
console.log("Custom execution complete:", { id, result })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise<void> {
|
||||
console.log("Register supported executions:", { custom, shell, process })
|
||||
return Promise.resolve()
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadTask")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadConfiguration
|
||||
this.rpcProtocol.set(MainContext.MainThreadConfiguration, {
|
||||
$updateConfigurationOption(
|
||||
target: any,
|
||||
key: string,
|
||||
value: any,
|
||||
overrides: any,
|
||||
scopeToLanguage: boolean | undefined,
|
||||
): Promise<void> {
|
||||
console.log("Update configuration option:", { target, key, value, overrides, scopeToLanguage })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$removeConfigurationOption(
|
||||
target: any,
|
||||
key: string,
|
||||
overrides: any,
|
||||
scopeToLanguage: boolean | undefined,
|
||||
): Promise<void> {
|
||||
console.log("Remove configuration option:", { target, key, overrides, scopeToLanguage })
|
||||
return Promise.resolve()
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadConfiguration")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadFileSystem
|
||||
this.rpcProtocol.set(MainContext.MainThreadFileSystem, {
|
||||
async $registerFileSystemProvider(
|
||||
handle: number,
|
||||
scheme: string,
|
||||
capabilities: any,
|
||||
readonlyMessage?: any,
|
||||
): Promise<void> {
|
||||
console.log("Register file system provider:", { handle, scheme, capabilities, readonlyMessage })
|
||||
},
|
||||
$unregisterProvider(handle: number): void {
|
||||
console.log("Unregister provider:", handle)
|
||||
},
|
||||
$onFileSystemChange(handle: number, resource: any[]): void {
|
||||
console.log("File system change:", { handle, resource })
|
||||
},
|
||||
async $stat(resource: UriComponents): Promise<any> {
|
||||
console.log("Stat:", resource)
|
||||
try {
|
||||
const filePath = this.uriToPath(resource)
|
||||
const stats = await fsStat(filePath)
|
||||
|
||||
return {
|
||||
type: this.getFileType(stats),
|
||||
ctime: stats.birthtimeMs,
|
||||
mtime: stats.mtimeMs,
|
||||
size: stats.size,
|
||||
permissions: stats.mode & 0o444 ? FilePermission.Readonly : undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in $stat:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $readdir(resource: UriComponents): Promise<[string, FileType][]> {
|
||||
console.log("Read directory:", resource)
|
||||
try {
|
||||
const filePath = this.uriToPath(resource)
|
||||
const entries = await fsReadDir(filePath, { withFileTypes: true })
|
||||
|
||||
return entries.map((entry) => {
|
||||
let type = FileType.Unknown
|
||||
if (entry.isFile()) {
|
||||
type = FileType.File
|
||||
} else if (entry.isDirectory()) {
|
||||
type = FileType.Directory
|
||||
}
|
||||
|
||||
// Check if it's a symbolic link
|
||||
if (entry.isSymbolicLink()) {
|
||||
type |= FileType.SymbolicLink
|
||||
}
|
||||
|
||||
return [entry.name, type] as [string, FileType]
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error in $readdir:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $readFile(resource: UriComponents): Promise<any> {
|
||||
console.log("Read file:", resource)
|
||||
try {
|
||||
const filePath = this.uriToPath(resource)
|
||||
const buffer = await fsReadFile(filePath)
|
||||
return VSBuffer.wrap(buffer)
|
||||
} catch (error) {
|
||||
console.error("Error in $readFile:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $writeFile(resource: UriComponents, content: any): Promise<void> {
|
||||
console.log("Write file:", { resource, content })
|
||||
try {
|
||||
const filePath = this.uriToPath(resource)
|
||||
const buffer = content instanceof VSBuffer ? content.buffer : content
|
||||
await fsWriteFile(filePath, buffer)
|
||||
} catch (error) {
|
||||
console.error("Error in $writeFile:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $rename(resource: UriComponents, target: UriComponents, opts: any): Promise<void> {
|
||||
console.log("Rename:", { resource, target, opts })
|
||||
try {
|
||||
const sourcePath = this.uriToPath(resource)
|
||||
const targetPath = this.uriToPath(target)
|
||||
|
||||
// Check if target exists and handle overwrite option
|
||||
if (opts.overwrite) {
|
||||
try {
|
||||
await fsUnlink(targetPath)
|
||||
} catch (error) {
|
||||
// Ignore error if file doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
await fsRename(sourcePath, targetPath)
|
||||
} catch (error) {
|
||||
console.error("Error in $rename:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $copy(resource: UriComponents, target: UriComponents, opts: any): Promise<void> {
|
||||
console.log("Copy:", { resource, target, opts })
|
||||
try {
|
||||
const sourcePath = this.uriToPath(resource)
|
||||
const targetPath = this.uriToPath(target)
|
||||
|
||||
// Check if target exists and handle overwrite option
|
||||
if (opts.overwrite) {
|
||||
try {
|
||||
await fsUnlink(targetPath)
|
||||
} catch (error) {
|
||||
// Ignore error if file doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
await fsCopyFile(sourcePath, targetPath)
|
||||
} catch (error) {
|
||||
console.error("Error in $copy:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $mkdir(resource: UriComponents): Promise<void> {
|
||||
console.log("Make directory:", resource)
|
||||
try {
|
||||
const dirPath = this.uriToPath(resource)
|
||||
await fsMkdir(dirPath, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error("Error in $mkdir:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $delete(resource: UriComponents, opts: any): Promise<void> {
|
||||
console.log("Delete:", { resource, opts })
|
||||
try {
|
||||
const filePath = this.uriToPath(resource)
|
||||
|
||||
// Check if it's a directory
|
||||
const stats = await fsLstat(filePath)
|
||||
if (stats.isDirectory()) {
|
||||
// For directories, we need to implement recursive deletion
|
||||
// This is a simplified version
|
||||
await fs.promises.rm(filePath, { recursive: true })
|
||||
} else {
|
||||
await fsUnlink(filePath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in $delete:", error)
|
||||
throw this.handleFileSystemError(error)
|
||||
}
|
||||
},
|
||||
async $ensureActivation(scheme: string): Promise<void> {
|
||||
console.log("Ensure activation:", scheme)
|
||||
// No-op implementation
|
||||
return Promise.resolve()
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadFileSystem")
|
||||
},
|
||||
|
||||
// Helper methods
|
||||
uriToPath(uri: UriComponents): string {
|
||||
// Convert URI to file path
|
||||
// This is a simplified implementation
|
||||
if (uri.scheme !== "file") {
|
||||
throw new Error(`Unsupported URI scheme: ${uri.scheme}`)
|
||||
}
|
||||
|
||||
// Handle Windows paths
|
||||
let filePath = uri.path || ""
|
||||
if (process.platform === "win32" && filePath.startsWith("/")) {
|
||||
filePath = filePath.substring(1)
|
||||
}
|
||||
|
||||
return filePath
|
||||
},
|
||||
|
||||
getFileType(stats: fs.Stats): FileType {
|
||||
let type = FileType.Unknown
|
||||
|
||||
if (stats.isFile()) {
|
||||
type = FileType.File
|
||||
} else if (stats.isDirectory()) {
|
||||
type = FileType.Directory
|
||||
}
|
||||
|
||||
// Check if it's a symbolic link
|
||||
if (stats.isSymbolicLink()) {
|
||||
type |= FileType.SymbolicLink
|
||||
}
|
||||
|
||||
return type
|
||||
},
|
||||
|
||||
handleFileSystemError(error: any): Error {
|
||||
// Map Node.js errors to VSCode file system errors
|
||||
if (error.code === "ENOENT") {
|
||||
const err = new Error(error.message)
|
||||
err.name = FileSystemProviderErrorCode.FileNotFound
|
||||
return err
|
||||
} else if (error.code === "EACCES" || error.code === "EPERM") {
|
||||
const err = new Error(error.message)
|
||||
err.name = FileSystemProviderErrorCode.NoPermissions
|
||||
return err
|
||||
} else if (error.code === "EEXIST") {
|
||||
const err = new Error(error.message)
|
||||
err.name = FileSystemProviderErrorCode.FileExists
|
||||
return err
|
||||
} else if (error.code === "EISDIR") {
|
||||
const err = new Error(error.message)
|
||||
err.name = FileSystemProviderErrorCode.FileIsADirectory
|
||||
return err
|
||||
} else if (error.code === "ENOTDIR") {
|
||||
const err = new Error(error.message)
|
||||
err.name = FileSystemProviderErrorCode.FileNotADirectory
|
||||
return err
|
||||
}
|
||||
|
||||
// Default error
|
||||
return error
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadLanguageModelTools
|
||||
this.rpcProtocol.set(MainContext.MainThreadLanguageModelTools, {
|
||||
$getTools(): Promise<any[]> {
|
||||
console.log("Getting language model tools")
|
||||
return Promise.resolve([])
|
||||
},
|
||||
$invokeTool(dto: any, token: any): Promise<any> {
|
||||
console.log("Invoking language model tool:", dto)
|
||||
return Promise.resolve({})
|
||||
},
|
||||
$countTokensForInvocation(callId: string, input: string, token: any): Promise<number> {
|
||||
console.log("Counting tokens for invocation:", { callId, input })
|
||||
return Promise.resolve(0)
|
||||
},
|
||||
$registerTool(id: string): void {
|
||||
console.log("Registering language model tool:", id)
|
||||
},
|
||||
$unregisterTool(name: string): void {
|
||||
console.log("Unregistering language model tool:", name)
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Disposing MainThreadLanguageModelTools")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Protocols needed for general extension loading process
|
||||
public setupExtensionRequiredProtocols(): void {
|
||||
if (!this.rpcProtocol) {
|
||||
return
|
||||
}
|
||||
|
||||
this.rpcProtocol.set(MainContext.MainThreadExtensionService, {
|
||||
$getExtension: async (extensionId: string): Promise<Dto<IExtensionDescription> | undefined> => {
|
||||
console.log(`Getting extension: ${extensionId}`)
|
||||
return this.extensionManager.getExtensionDescription(extensionId)
|
||||
},
|
||||
$activateExtension: async (
|
||||
extensionId: ExtensionIdentifier,
|
||||
reason: ExtensionActivationReason,
|
||||
): Promise<void> => {
|
||||
console.log(`Activating extension ${extensionId.value} with reason:`, reason)
|
||||
await this.extensionManager.activateExtension(extensionId.value, this.rpcProtocol)
|
||||
},
|
||||
$onWillActivateExtension: async (extensionId: ExtensionIdentifier): Promise<void> => {
|
||||
console.log(`Extension ${extensionId.value} will be activated`)
|
||||
},
|
||||
$onDidActivateExtension: (
|
||||
extensionId: ExtensionIdentifier,
|
||||
codeLoadingTime: number,
|
||||
activateCallTime: number,
|
||||
activateResolvedTime: number,
|
||||
activationReason: ExtensionActivationReason,
|
||||
): void => {
|
||||
console.log(`Extension ${extensionId.value} was activated with reason:`, activationReason)
|
||||
},
|
||||
$onExtensionActivationError: async (
|
||||
extensionId: ExtensionIdentifier,
|
||||
error: any,
|
||||
missingExtensionDependency: any | null,
|
||||
): Promise<void> => {
|
||||
console.error(`Extension ${extensionId.value} activation error:`, error)
|
||||
},
|
||||
$onExtensionRuntimeError: (extensionId: ExtensionIdentifier, error: any): void => {
|
||||
console.error(`Extension ${extensionId.value} runtime error:`, error)
|
||||
},
|
||||
$setPerformanceMarks: async (marks: { name: string; startTime: number }[]): Promise<void> => {
|
||||
console.log("Setting performance marks:", marks)
|
||||
},
|
||||
$asBrowserUri: async (uri: any): Promise<any> => {
|
||||
console.log("Converting to browser URI:", uri)
|
||||
return uri
|
||||
},
|
||||
dispose: () => {
|
||||
console.log("Disposing MainThreadExtensionService")
|
||||
},
|
||||
})
|
||||
|
||||
this.rpcProtocol.set(MainContext.MainThreadTelemetry, {
|
||||
$publicLog(eventName: string, data?: any): void {
|
||||
console.log(`[Telemetry] ${eventName}`, data)
|
||||
},
|
||||
$publicLog2<E extends any = never, T extends any = never>(eventName: string, data?: any): void {
|
||||
console.log(`[Telemetry] ${eventName}`, data)
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Disposing MainThreadTelemetry")
|
||||
},
|
||||
})
|
||||
|
||||
this.rpcProtocol.set(MainContext.MainThreadDebugService, {
|
||||
$registerDebugTypes(debugTypes: string[]): void {
|
||||
console.log("Register debug types:", debugTypes)
|
||||
},
|
||||
$sessionCached(sessionID: string): void {
|
||||
console.log("Session cached:", sessionID)
|
||||
},
|
||||
$acceptDAMessage(handle: number, message: any): void {
|
||||
console.log("Accept debug adapter message:", { handle, message })
|
||||
},
|
||||
$acceptDAError(handle: number, name: string, message: string, stack: string | undefined): void {
|
||||
console.error("Debug adapter error:", { handle, name, message, stack })
|
||||
},
|
||||
$acceptDAExit(handle: number, code: number | undefined, signal: string | undefined): void {
|
||||
console.log("Debug adapter exit:", { handle, code, signal })
|
||||
},
|
||||
async $registerDebugConfigurationProvider(
|
||||
type: string,
|
||||
triggerKind: any,
|
||||
hasProvideMethod: boolean,
|
||||
hasResolveMethod: boolean,
|
||||
hasResolve2Method: boolean,
|
||||
handle: number,
|
||||
): Promise<void> {
|
||||
console.log("Register debug configuration provider:", {
|
||||
type,
|
||||
triggerKind,
|
||||
hasProvideMethod,
|
||||
hasResolveMethod,
|
||||
hasResolve2Method,
|
||||
handle,
|
||||
})
|
||||
},
|
||||
async $registerDebugAdapterDescriptorFactory(type: string, handle: number): Promise<void> {
|
||||
console.log("Register debug adapter descriptor factory:", { type, handle })
|
||||
},
|
||||
$unregisterDebugConfigurationProvider(handle: number): void {
|
||||
console.log("Unregister debug configuration provider:", handle)
|
||||
},
|
||||
$unregisterDebugAdapterDescriptorFactory(handle: number): void {
|
||||
console.log("Unregister debug adapter descriptor factory:", handle)
|
||||
},
|
||||
async $startDebugging(folder: any, nameOrConfig: string | any, options: any): Promise<boolean> {
|
||||
console.log("Start debugging:", { folder, nameOrConfig, options })
|
||||
return true
|
||||
},
|
||||
async $stopDebugging(sessionId: string | undefined): Promise<void> {
|
||||
console.log("Stop debugging:", sessionId)
|
||||
},
|
||||
$setDebugSessionName(id: string, name: string): void {
|
||||
console.log("Set debug session name:", { id, name })
|
||||
},
|
||||
async $customDebugAdapterRequest(id: string, command: string, args: any): Promise<any> {
|
||||
console.log("Custom debug adapter request:", { id, command, args })
|
||||
return null
|
||||
},
|
||||
async $getDebugProtocolBreakpoint(id: string, breakpoinId: string): Promise<any> {
|
||||
console.log("Get debug protocol breakpoint:", { id, breakpoinId })
|
||||
return undefined
|
||||
},
|
||||
$appendDebugConsole(value: string): void {
|
||||
console.log("Debug console:", value)
|
||||
},
|
||||
async $registerBreakpoints(breakpoints: any[]): Promise<void> {
|
||||
console.log("Register breakpoints:", breakpoints)
|
||||
},
|
||||
async $unregisterBreakpoints(
|
||||
breakpointIds: string[],
|
||||
functionBreakpointIds: string[],
|
||||
dataBreakpointIds: string[],
|
||||
): Promise<void> {
|
||||
console.log("Unregister breakpoints:", { breakpointIds, functionBreakpointIds, dataBreakpointIds })
|
||||
},
|
||||
$registerDebugVisualizer(extensionId: string, id: string): void {
|
||||
console.log("Register debug visualizer:", { extensionId, id })
|
||||
},
|
||||
$unregisterDebugVisualizer(extensionId: string, id: string): void {
|
||||
console.log("Unregister debug visualizer:", { extensionId, id })
|
||||
},
|
||||
$registerDebugVisualizerTree(treeId: string, canEdit: boolean): void {
|
||||
console.log("Register debug visualizer tree:", { treeId, canEdit })
|
||||
},
|
||||
$unregisterDebugVisualizerTree(treeId: string): void {
|
||||
console.log("Unregister debug visualizer tree:", treeId)
|
||||
},
|
||||
$registerCallHierarchyProvider(handle: number, supportsResolve: boolean): void {
|
||||
console.log("Register call hierarchy provider:", { handle, supportsResolve })
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Disposing MainThreadDebugService")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public setupRooCodeRequiredProtocols(): void {
|
||||
if (!this.rpcProtocol) {
|
||||
return
|
||||
}
|
||||
|
||||
// MainThreadTextEditors
|
||||
this.rpcProtocol.set(MainContext.MainThreadTextEditors, {
|
||||
$tryShowTextDocument(resource: UriComponents, options: any): Promise<string | undefined> {
|
||||
console.log("Try show text document:", { resource, options })
|
||||
return Promise.resolve(undefined)
|
||||
},
|
||||
$tryShowEditor(id: string, position?: any): Promise<void> {
|
||||
console.log("Try show editor:", { id, position })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$tryHideEditor(id: string): Promise<void> {
|
||||
console.log("Try hide editor:", id)
|
||||
return Promise.resolve()
|
||||
},
|
||||
$trySetSelections(id: string, selections: any[]): Promise<void> {
|
||||
console.log("Try set selections:", { id, selections })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$tryRevealRange(id: string, range: any, revealType: any): Promise<void> {
|
||||
console.log("Try reveal range:", { id, range, revealType })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$trySetOptions(id: string, options: any): Promise<void> {
|
||||
console.log("Try set options:", { id, options })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$tryApplyEdits(id: string, modelVersionId: number, edits: any[], opts: any): Promise<boolean> {
|
||||
console.log("Try apply edits:", { id, modelVersionId, edits, opts })
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
$registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: any): void {
|
||||
console.log("Register text editor decoration type:", { extensionId, key, options })
|
||||
},
|
||||
$removeTextEditorDecorationType(key: string): void {
|
||||
console.log("Remove text editor decoration type:", key)
|
||||
},
|
||||
$trySetDecorations(id: string, key: string, ranges: any[]): Promise<void> {
|
||||
console.log("Try set decorations:", { id, key, ranges })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$trySetDecorationsFast(id: string, key: string, ranges: any[]): Promise<void> {
|
||||
console.log("Try set decorations fast:", { id, key, ranges })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$tryInsertSnippet(id: string, snippet: any, location: any, options: any): Promise<boolean> {
|
||||
console.log("Try insert snippet:", { id, snippet, location, options })
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
$getDiffInformation(id: string): Promise<any> {
|
||||
console.log("Get diff information:", id)
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadTextEditors")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadStorage
|
||||
this.rpcProtocol.set(MainContext.MainThreadStorage, {
|
||||
$initializeExtensionStorage(shared: boolean, extensionId: string): Promise<string | undefined> {
|
||||
console.log("Initialize extension storage:", { shared, extensionId })
|
||||
return Promise.resolve(undefined)
|
||||
},
|
||||
$setValue(shared: boolean, extensionId: string, value: object): Promise<void> {
|
||||
console.log("Set value:", { shared, extensionId, value })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$registerExtensionStorageKeysToSync(extension: any, keys: string[]): void {
|
||||
console.log("Register extension storage keys to sync:", { extension, keys })
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadStorage")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadOutputService
|
||||
this.rpcProtocol.set(MainContext.MainThreadOutputService, {
|
||||
$register(
|
||||
label: string,
|
||||
file: UriComponents,
|
||||
languageId: string | undefined,
|
||||
extensionId: string,
|
||||
): Promise<string> {
|
||||
console.log("Register output channel:", { label, file, languageId, extensionId })
|
||||
return Promise.resolve(`output-${extensionId}-${label}`)
|
||||
},
|
||||
$update(channelId: string, mode: any, till?: number): Promise<void> {
|
||||
console.log("Update output channel:", { channelId, mode, till })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$reveal(channelId: string, preserveFocus: boolean): Promise<void> {
|
||||
console.log("Reveal output channel:", { channelId, preserveFocus })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$close(channelId: string): Promise<void> {
|
||||
console.log("Close output channel:", channelId)
|
||||
return Promise.resolve()
|
||||
},
|
||||
$dispose(channelId: string): Promise<void> {
|
||||
console.log("Dispose output channel:", channelId)
|
||||
return Promise.resolve()
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadOutputService")
|
||||
},
|
||||
})
|
||||
|
||||
// Create a single WebViewManager instance
|
||||
const webViewManager = new WebViewManager(this.rpcProtocol)
|
||||
|
||||
// MainThreadWebviewViews
|
||||
this.rpcProtocol.set(MainContext.MainThreadWebviewViews, webViewManager)
|
||||
|
||||
// MainThreadDocumentContentProviders
|
||||
this.rpcProtocol.set(MainContext.MainThreadDocumentContentProviders, {
|
||||
$registerTextContentProvider(handle: number, scheme: string): void {
|
||||
console.log("Register text content provider:", { handle, scheme })
|
||||
},
|
||||
$unregisterTextContentProvider(handle: number): void {
|
||||
console.log("Unregister text content provider:", handle)
|
||||
},
|
||||
$onVirtualDocumentChange(uri: UriComponents, value: string): Promise<void> {
|
||||
console.log("Virtual document change:", { uri, value })
|
||||
return Promise.resolve()
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadDocumentContentProviders")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadUrls
|
||||
this.rpcProtocol.set(MainContext.MainThreadUrls, {
|
||||
$registerUriHandler(
|
||||
handle: number,
|
||||
extensionId: ExtensionIdentifier,
|
||||
extensionDisplayName: string,
|
||||
): Promise<void> {
|
||||
console.log("Register URI handler:", { handle, extensionId, extensionDisplayName })
|
||||
return Promise.resolve()
|
||||
},
|
||||
$unregisterUriHandler(handle: number): Promise<void> {
|
||||
console.log("Unregister URI handler:", handle)
|
||||
return Promise.resolve()
|
||||
},
|
||||
$createAppUri(uri: UriComponents): Promise<UriComponents> {
|
||||
console.log("Create app URI:", uri)
|
||||
return Promise.resolve(uri)
|
||||
},
|
||||
dispose(): void {
|
||||
console.log("Dispose MainThreadUrls")
|
||||
},
|
||||
})
|
||||
|
||||
// MainThreadWebviews
|
||||
this.rpcProtocol.set(MainContext.MainThreadWebviews, webViewManager)
|
||||
}
|
||||
|
||||
public getRPCProtocol(): IRPCProtocol | null {
|
||||
return this.rpcProtocol
|
||||
}
|
||||
}
|
||||
159
jetbrains/host/src/webViewManager.ts
Normal file
159
jetbrains/host/src/webViewManager.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
ExtHostContext,
|
||||
ExtHostWebviewViewsShape,
|
||||
MainThreadWebviewViewsShape,
|
||||
WebviewExtensionDescription as ExtHostWebviewExtensionDescription,
|
||||
MainThreadWebviewsShape,
|
||||
IWebviewContentOptions,
|
||||
} from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
|
||||
import { IRPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
|
||||
import { WebviewContentOptions } from "../deps/vscode/vs/workbench/contrib/webview/browser/webview.js"
|
||||
import { URI } from "../deps/vscode/vs/base/common/uri.js"
|
||||
import { CancellationToken } from "../deps/vscode/vs/base/common/cancellation.js"
|
||||
import { VSBuffer } from "../deps/vscode/vs/base/common/buffer.js"
|
||||
|
||||
/**
|
||||
* A simplified webview implementation that only includes methods used by WebViewManager
|
||||
*/
|
||||
class SimpleWebview {
|
||||
contentOptions: WebviewContentOptions = {}
|
||||
|
||||
setHtml(html: string): void {
|
||||
console.log("[SimpleWebview] Set HTML:", html)
|
||||
}
|
||||
|
||||
setTitle(title: string): void {
|
||||
console.log("[SimpleWebview] Set title:", title)
|
||||
}
|
||||
|
||||
postMessage(message: any, transfer?: readonly VSBuffer[]): Promise<boolean> {
|
||||
console.log("[SimpleWebview] Post message:", message)
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
console.log("[SimpleWebview] Focus")
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log("[SimpleWebview] Dispose")
|
||||
}
|
||||
}
|
||||
|
||||
export class WebViewManager implements MainThreadWebviewViewsShape, MainThreadWebviewsShape {
|
||||
private readonly _proxy: ExtHostWebviewViewsShape
|
||||
private readonly _webviews = new Map<string, SimpleWebview>()
|
||||
|
||||
constructor(private readonly rpcProtocol: IRPCProtocol) {
|
||||
this._proxy = this.rpcProtocol.getProxy(ExtHostContext.ExtHostWebviewViews)
|
||||
}
|
||||
|
||||
// MainThreadWebviewViewsShape implementation
|
||||
$registerWebviewViewProvider(
|
||||
extension: ExtHostWebviewExtensionDescription,
|
||||
viewType: string,
|
||||
options: { retainContextWhenHidden?: boolean; serializeBuffersForPostMessage: boolean },
|
||||
): void {
|
||||
console.log("Register webview view provider:", { extension, viewType, options })
|
||||
|
||||
// Create a new webview instance
|
||||
const webview = new SimpleWebview()
|
||||
|
||||
// Store the webview instance
|
||||
this._webviews.set(viewType, webview)
|
||||
|
||||
// Generate a unique handle for this webview
|
||||
const webviewHandle = `webview-${viewType}-${Date.now()}`
|
||||
|
||||
// Notify the extension host that the webview is ready
|
||||
this._proxy.$resolveWebviewView(
|
||||
webviewHandle,
|
||||
viewType,
|
||||
undefined, // title
|
||||
undefined, // state
|
||||
CancellationToken.None, // cancellation
|
||||
)
|
||||
}
|
||||
|
||||
$unregisterWebviewViewProvider(viewType: string): void {
|
||||
console.log("Unregister webview view provider:", viewType)
|
||||
|
||||
// Remove the webview instance
|
||||
const webview = this._webviews.get(viewType)
|
||||
if (webview) {
|
||||
webview.dispose()
|
||||
this._webviews.delete(viewType)
|
||||
}
|
||||
}
|
||||
|
||||
$setWebviewViewTitle(handle: string, value: string | undefined): void {
|
||||
console.log("Set webview view title:", { handle, value })
|
||||
const webview = this._webviews.get(handle)
|
||||
if (webview) {
|
||||
webview.setTitle(value || "")
|
||||
}
|
||||
}
|
||||
|
||||
$setWebviewViewDescription(handle: string, value: string | undefined): void {
|
||||
console.log("Set webview view description:", { handle, value })
|
||||
}
|
||||
|
||||
$setWebviewViewBadge(handle: string, badge: any | undefined): void {
|
||||
console.log("Set webview view badge:", { handle, badge })
|
||||
}
|
||||
|
||||
$show(handle: string, preserveFocus: boolean): void {
|
||||
console.log("Show webview view:", { handle, preserveFocus })
|
||||
const webview = this._webviews.get(handle)
|
||||
if (webview) {
|
||||
webview.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// MainThreadWebviewsShape implementation
|
||||
$setHtml(handle: string, value: string): void {
|
||||
console.log("Set webview HTML:", { handle, value })
|
||||
const webview = this._webviews.get(handle)
|
||||
if (webview) {
|
||||
webview.setHtml(value)
|
||||
}
|
||||
}
|
||||
|
||||
$setOptions(handle: string, options: IWebviewContentOptions): void {
|
||||
console.log("Set webview panel options:", { handle, options })
|
||||
const webview = this._webviews.get(handle)
|
||||
if (webview) {
|
||||
// Convert IWebviewContentOptions to WebviewContentOptions
|
||||
const contentOptions: WebviewContentOptions = {
|
||||
allowScripts: options.enableScripts,
|
||||
allowForms: options.enableForms,
|
||||
localResourceRoots: options.localResourceRoots?.map((uri) => URI.revive(uri)),
|
||||
portMapping: options.portMapping,
|
||||
}
|
||||
webview.contentOptions = contentOptions
|
||||
}
|
||||
}
|
||||
|
||||
$postMessage(handle: string, value: string, ...buffers: VSBuffer[]): Promise<boolean> {
|
||||
console.log("Post message to webview:", { handle, value, buffers })
|
||||
const webview = this._webviews.get(handle)
|
||||
if (webview) {
|
||||
return webview.postMessage(value, buffers)
|
||||
}
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log("Dispose WebViewManager")
|
||||
// Dispose all webviews
|
||||
for (const webview of this._webviews.values()) {
|
||||
webview.dispose()
|
||||
}
|
||||
this._webviews.clear()
|
||||
}
|
||||
}
|
||||
21
jetbrains/host/tsconfig.base.json
Normal file
21
jetbrains/host/tsconfig.base.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "legacy",
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": false,
|
||||
"noImplicitOverride": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUncheckedSideEffectImports": false,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker.ImportScripts"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
51
jetbrains/host/tsconfig.json
Normal file
51
jetbrains/host/tsconfig.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
// This is the configuration for plugins/base independent project, compiled separately from the outer project
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": false,
|
||||
"outDir": "./dist",
|
||||
"skipLibCheck": true, // Skip library file checking to avoid type conflicts
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": [
|
||||
"@types/mocha",
|
||||
"@types/semver",
|
||||
"@types/sinon",
|
||||
"@types/trusted-types",
|
||||
"@types/winreg",
|
||||
"@types/wicg-file-system-access"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts", // Include all TS files under src
|
||||
"src/**/*.js",
|
||||
"electron.d.ts",
|
||||
"deps/vscode/typings",
|
||||
"deps/vscode/vs/workbench/api/node",
|
||||
"deps/vscode/vs/workbench/contrib/debug/common/debugProtocol.d.ts",
|
||||
"deps/vscode/vscode-dts/vscode.d.ts",
|
||||
"deps/vscode/vscode-dts/vscode.proposed.*.d.ts",
|
||||
"deps/vscode/vs/base/common/marked",
|
||||
"deps/vscode/vs/base/common/semver"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test",
|
||||
"webview-ui",
|
||||
"deps/vscode/**/test/**/*.ts",
|
||||
"deps/vscode/**/test/**/*.js",
|
||||
"deps/vscode/**/fixtures/**/*.js",
|
||||
"deps/vscode/**/fixtures/**/*.ts",
|
||||
"node_modules/@types/vscode",
|
||||
"node_modules/@types/electron",
|
||||
"dist/**/*"
|
||||
]
|
||||
}
|
||||
37
jetbrains/host/tsconfig.monaco.json
Normal file
37
jetbrains/host/tsconfig.monaco.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["@webgpu/types", "trusted-types", "wicg-file-system-access"],
|
||||
"paths": {},
|
||||
"module": "amd",
|
||||
"moduleResolution": "node",
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"target": "ES2022",
|
||||
"sourceMap": false,
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"typings/css.d.ts",
|
||||
"typings/thenable.d.ts",
|
||||
"typings/vscode-globals-product.d.ts",
|
||||
"typings/vscode-globals-nls.d.ts",
|
||||
"typings/editContext.d.ts",
|
||||
"vs/monaco.d.ts",
|
||||
"vs/editor/*",
|
||||
"vs/base/common/*",
|
||||
"vs/base/browser/*",
|
||||
"vs/platform/*/common/*",
|
||||
"vs/platform/*/browser/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules/*",
|
||||
"vs/platform/files/browser/htmlFileSystemProvider.ts",
|
||||
"vs/platform/files/browser/webFileSystemAccess.ts",
|
||||
"vs/platform/telemetry/*",
|
||||
"vs/platform/assignment/*",
|
||||
"vs/platform/terminal/*",
|
||||
"vs/platform/externalTerminal/*"
|
||||
]
|
||||
}
|
||||
14
jetbrains/host/tsconfig.tsec.json
Normal file
14
jetbrains/host/tsconfig.tsec.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "tsec",
|
||||
"exemptionConfig": "./tsec.exemptions.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": ["./vs/workbench/contrib/webview/browser/pre/service-worker.js", "*/test/*", "**/*.test.ts"]
|
||||
}
|
||||
18
jetbrains/host/tsconfig.vscode-dts.json
Normal file
18
jetbrains/host/tsconfig.vscode-dts.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"module": "None",
|
||||
"experimentalDecorators": false,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": [],
|
||||
"lib": ["ES2022"]
|
||||
},
|
||||
"include": ["vscode-dts/vscode.d.ts"]
|
||||
}
|
||||
4
jetbrains/host/tsconfig.vscode-proposed-dts.json
Normal file
4
jetbrains/host/tsconfig.vscode-proposed-dts.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.vscode-dts.json",
|
||||
"include": ["vscode-dts/vscode.d.ts", "vscode-dts/vscode.proposed.*.d.ts"]
|
||||
}
|
||||
34
jetbrains/host/tsec.exemptions.json
Normal file
34
jetbrains/host/tsec.exemptions.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"ban-document-execcommand": [
|
||||
"vs/workbench/contrib/codeEditor/electron-sandbox/inputClipboardActions.ts",
|
||||
"vs/editor/contrib/clipboard/browser/clipboard.ts"
|
||||
],
|
||||
"ban-eval-calls": ["vs/workbench/api/worker/extHostExtensionService.ts"],
|
||||
"ban-function-calls": [
|
||||
"vs/workbench/api/worker/extHostExtensionService.ts",
|
||||
"vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts",
|
||||
"vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts"
|
||||
],
|
||||
"ban-trustedtypes-createpolicy": [
|
||||
"bootstrap-window.ts",
|
||||
"vs/amdX.ts",
|
||||
"vs/base/browser/trustedTypes.ts",
|
||||
"vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts"
|
||||
],
|
||||
"ban-worker-calls": [
|
||||
"vs/base/browser/webWorkerFactory.ts",
|
||||
"vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts"
|
||||
],
|
||||
"ban-worker-importscripts": [
|
||||
"vs/amdX.ts",
|
||||
"vs/workbench/services/extensions/worker/polyfillNestedWorker.ts",
|
||||
"vs/workbench/api/worker/extensionHostWorker.ts"
|
||||
],
|
||||
"ban-domparser-parsefromstring": [
|
||||
"vs/base/browser/markdownRenderer.ts",
|
||||
"vs/base/test/browser/markdownRenderer.test.ts"
|
||||
],
|
||||
"ban-element-setattribute": ["**/*.ts"],
|
||||
"ban-element-insertadjacenthtml": ["**/*.ts"],
|
||||
"ban-script-content-assignments": ["bootstrap-window.ts"]
|
||||
}
|
||||
25
jetbrains/host/tsup.config.ts
Normal file
25
jetbrains/host/tsup.config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// tsup.config.ts
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
import { dependencies } from "./package.json"
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
"src/main": "src/main.ts",
|
||||
"src/extension": "src/extension.ts",
|
||||
}, // Build both server and ext host
|
||||
format: ["esm"], // Output format, e.g., ES Module and CommonJS
|
||||
minify: true, // Minify code
|
||||
clean: true, // Clean output directory
|
||||
splitting: false,
|
||||
platform: "node", // Target platform, e.g., Node.js
|
||||
target: "node18", // Target environment, e.g., latest ECMAScript standard
|
||||
skipNodeModulesBundle: false, // Don't bundle dependencies in node_modules
|
||||
// noExternal: Object.keys(dependencies),
|
||||
// external:[/^@vscode\/.*$/], // Don't bundle vscode-related dependencies
|
||||
dts: false, // Don't generate type declaration files, as we usually handle type declarations separately
|
||||
})
|
||||
44
jetbrains/host/turbo.json
Normal file
44
jetbrains/host/turbo.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"deps:check": {
|
||||
"cache": false,
|
||||
"env": ["DEVENV"]
|
||||
},
|
||||
"deps:clean": {
|
||||
"cache": false
|
||||
},
|
||||
"deps:patch": {
|
||||
"cache": false,
|
||||
"dependsOn": ["deps:check", "deps:clean"],
|
||||
"env": ["DEVENV"]
|
||||
},
|
||||
"deps:copy": {
|
||||
"cache": false,
|
||||
"dependsOn": ["deps:check", "deps:patch"],
|
||||
"env": ["DEVENV"]
|
||||
},
|
||||
"prop:deps": {
|
||||
"cache": false
|
||||
},
|
||||
"bundle:build": {
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": ["package.json", "tsconfig.json", "src/**", "deps/vscode/**"],
|
||||
"dependsOn": ["clean", "deps:clean", "deps:patch", "deps:copy"]
|
||||
},
|
||||
"bundle:package": {
|
||||
"cache": false,
|
||||
"dependsOn": ["bundle:build"]
|
||||
},
|
||||
"bundle": {
|
||||
"cache": false,
|
||||
"dependsOn": ["bundle:package"]
|
||||
},
|
||||
"build": {
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": ["package.json", "tsconfig.json", "src/**", "deps/vscode/**"],
|
||||
"dependsOn": ["clean", "deps:clean", "deps:patch", "deps:copy"]
|
||||
}
|
||||
}
|
||||
}
|
||||
18
jetbrains/host/typings/base-common.d.ts
vendored
Normal file
18
jetbrains/host/typings/base-common.d.ts
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Declare types that we probe for to implement util and/or polyfill functions
|
||||
|
||||
declare global {
|
||||
interface IdleDeadline {
|
||||
readonly didTimeout: boolean
|
||||
timeRemaining(): number
|
||||
}
|
||||
|
||||
function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number
|
||||
function cancelIdleCallback(handle: number): void
|
||||
}
|
||||
|
||||
export {}
|
||||
81
jetbrains/host/typings/crypto.d.ts
vendored
Normal file
81
jetbrains/host/typings/crypto.d.ts
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// NOTE that this is a partial copy from lib.dom.d.ts which is NEEDED because these utils are used in the /common/
|
||||
// layer which has no dependency on the DOM/browser-context. However, `crypto` is available as global in all browsers and
|
||||
// in nodejs. Therefore it's OK to spell out its typings here
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto).
|
||||
* Available only in secure contexts.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto)
|
||||
*/
|
||||
interface SubtleCrypto {
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) */
|
||||
// decrypt(algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) */
|
||||
// deriveBits(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, length?: number | null): Promise<ArrayBuffer>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) */
|
||||
// deriveKey(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams | HkdfParams | Pbkdf2Params, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey>;
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */
|
||||
digest(algorithm: { name: string } | string, data: ArrayBufferView | ArrayBuffer): Promise<ArrayBuffer>
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) */
|
||||
// encrypt(algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) */
|
||||
// exportKey(format: "jwk", key: CryptoKey): Promise<JsonWebKey>;
|
||||
// exportKey(format: Exclude<KeyFormat, "jwk">, key: CryptoKey): Promise<ArrayBuffer>;
|
||||
// exportKey(format: KeyFormat, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) */
|
||||
// generateKey(algorithm: "Ed25519", extractable: boolean, keyUsages: ReadonlyArray<"sign" | "verify">): Promise<CryptoKeyPair>;
|
||||
// generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams, extractable: boolean, keyUsages: ReadonlyArray<KeyUsage>): Promise<CryptoKeyPair>;
|
||||
// generateKey(algorithm: AesKeyGenParams | HmacKeyGenParams | Pbkdf2Params, extractable: boolean, keyUsages: ReadonlyArray<KeyUsage>): Promise<CryptoKey>;
|
||||
// generateKey(algorithm: AlgorithmIdentifier, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKeyPair | CryptoKey>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) */
|
||||
// importKey(format: "jwk", keyData: JsonWebKey, algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: ReadonlyArray<KeyUsage>): Promise<CryptoKey>;
|
||||
// importKey(format: Exclude<KeyFormat, "jwk">, keyData: BufferSource, algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) */
|
||||
// sign(algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) */
|
||||
// unwrapKey(format: KeyFormat, wrappedKey: BufferSource, unwrappingKey: CryptoKey, unwrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, unwrappedKeyAlgorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) */
|
||||
// verify(algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, signature: BufferSource, data: BufferSource): Promise<boolean>;
|
||||
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) */
|
||||
// wrapKey(format: KeyFormat, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams): Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic cryptography features available in the current context. It allows access to a cryptographically strong random number generator and to cryptographic primitives.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto)
|
||||
*/
|
||||
interface Crypto {
|
||||
/**
|
||||
* Available only in secure contexts.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle)
|
||||
*/
|
||||
readonly subtle: SubtleCrypto
|
||||
/**
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)
|
||||
*/
|
||||
getRandomValues<T extends ArrayBufferView | null>(array: T): T
|
||||
/**
|
||||
* Available only in secure contexts.
|
||||
*
|
||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
|
||||
*/
|
||||
randomUUID(): `${string}-${string}-${string}-${string}-${string}`
|
||||
}
|
||||
|
||||
var Crypto: {
|
||||
prototype: Crypto
|
||||
new (): Crypto
|
||||
}
|
||||
|
||||
var crypto: Crypto
|
||||
}
|
||||
export {}
|
||||
8
jetbrains/host/typings/css.d.ts
vendored
Normal file
8
jetbrains/host/typings/css.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Recognize all CSS files as valid module imports
|
||||
declare module "vs/css!*" {}
|
||||
declare module "*.css" {}
|
||||
138
jetbrains/host/typings/editContext.d.ts
vendored
Normal file
138
jetbrains/host/typings/editContext.d.ts
vendored
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
type DOMString = string
|
||||
|
||||
interface EditContext extends EventTarget {
|
||||
updateText(rangeStart: number, rangeEnd: number, text: DOMString): void
|
||||
updateSelection(start: number, end: number): void
|
||||
updateControlBounds(controlBounds: DOMRect): void
|
||||
updateSelectionBounds(selectionBounds: DOMRect): void
|
||||
updateCharacterBounds(rangeStart: number, characterBounds: DOMRect[]): void
|
||||
|
||||
attachedElements(): HTMLElement[]
|
||||
|
||||
get text(): DOMString
|
||||
get selectionStart(): number
|
||||
get selectionEnd(): number
|
||||
get characterBoundsRangeStart(): number
|
||||
characterBounds(): DOMRect[]
|
||||
|
||||
get ontextupdate(): EventHandler<TextUpdateEvent> | null
|
||||
set ontextupdate(value: EventHandler | null)
|
||||
|
||||
get ontextformatupdate(): EventHandler | null
|
||||
set ontextformatupdate(value: EventHandler | null)
|
||||
|
||||
get oncharacterboundsupdate(): EventHandler | null
|
||||
set oncharacterboundsupdate(value: EventHandler | null)
|
||||
|
||||
get oncompositionstart(): EventHandler | null
|
||||
set oncompositionstart(value: EventHandler | null)
|
||||
|
||||
get oncompositionend(): EventHandler | null
|
||||
set oncompositionend(value: EventHandler | null)
|
||||
|
||||
addEventListener<K extends keyof EditContextEventHandlersEventMap>(
|
||||
type: K,
|
||||
listener: (this: GlobalEventHandlers, ev: EditContextEventHandlersEventMap[K]) => any,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void
|
||||
removeEventListener<K extends keyof EditContextEventHandlersEventMap>(
|
||||
type: K,
|
||||
listener: (this: GlobalEventHandlers, ev: EditContextEventHandlersEventMap[K]) => any,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void
|
||||
removeEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void
|
||||
}
|
||||
|
||||
interface EditContextInit {
|
||||
text: DOMString
|
||||
selectionStart: number
|
||||
selectionEnd: number
|
||||
}
|
||||
|
||||
interface EditContextEventHandlersEventMap {
|
||||
textupdate: TextUpdateEvent
|
||||
textformatupdate: TextFormatUpdateEvent
|
||||
characterboundsupdate: CharacterBoundsUpdateEvent
|
||||
compositionstart: Event
|
||||
compositionend: Event
|
||||
}
|
||||
|
||||
type EventHandler<TEvent extends Event = Event> = (event: TEvent) => void
|
||||
|
||||
declare class TextUpdateEvent extends Event {
|
||||
constructor(type: DOMString, options?: TextUpdateEventInit)
|
||||
|
||||
readonly updateRangeStart: number
|
||||
readonly updateRangeEnd: number
|
||||
readonly text: DOMString
|
||||
readonly selectionStart: number
|
||||
readonly selectionEnd: number
|
||||
}
|
||||
|
||||
interface TextUpdateEventInit extends EventInit {
|
||||
updateRangeStart: number
|
||||
updateRangeEnd: number
|
||||
text: DOMString
|
||||
selectionStart: number
|
||||
selectionEnd: number
|
||||
compositionStart: number
|
||||
compositionEnd: number
|
||||
}
|
||||
|
||||
interface TextFormat {
|
||||
new (options?: TextFormatInit): TextFormat
|
||||
|
||||
readonly rangeStart: number
|
||||
readonly rangeEnd: number
|
||||
readonly underlineStyle: UnderlineStyle
|
||||
readonly underlineThickness: UnderlineThickness
|
||||
}
|
||||
|
||||
interface TextFormatInit {
|
||||
rangeStart: number
|
||||
rangeEnd: number
|
||||
underlineStyle: UnderlineStyle
|
||||
underlineThickness: UnderlineThickness
|
||||
}
|
||||
|
||||
type UnderlineStyle = "none" | "solid" | "dotted" | "dashed" | "wavy"
|
||||
type UnderlineThickness = "none" | "thin" | "thick"
|
||||
|
||||
interface TextFormatUpdateEvent extends Event {
|
||||
new (type: DOMString, options?: TextFormatUpdateEventInit): TextFormatUpdateEvent
|
||||
getTextFormats(): TextFormat[]
|
||||
}
|
||||
|
||||
interface TextFormatUpdateEventInit extends EventInit {
|
||||
textFormats: TextFormat[]
|
||||
}
|
||||
|
||||
interface CharacterBoundsUpdateEvent extends Event {
|
||||
new (type: DOMString, options?: CharacterBoundsUpdateEventInit): CharacterBoundsUpdateEvent
|
||||
|
||||
readonly rangeStart: number
|
||||
readonly rangeEnd: number
|
||||
}
|
||||
|
||||
interface CharacterBoundsUpdateEventInit extends EventInit {
|
||||
rangeStart: number
|
||||
rangeEnd: number
|
||||
}
|
||||
|
||||
interface HTMLElement {
|
||||
editContext?: EditContext
|
||||
}
|
||||
12
jetbrains/host/typings/thenable.d.ts
vendored
Normal file
12
jetbrains/host/typings/thenable.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
|
||||
* and others. This API makes no assumption about what promise library is being used which
|
||||
* enables reusing existing code without migrating to a specific promise implementation. Still,
|
||||
* we recommend the use of native promises which are available in VS Code.
|
||||
*/
|
||||
interface Thenable<T> extends PromiseLike<T> {}
|
||||
40
jetbrains/host/typings/vscode-globals-nls.d.ts
vendored
Normal file
40
jetbrains/host/typings/vscode-globals-nls.d.ts
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// AMD2ESM migration relevant
|
||||
|
||||
/**
|
||||
* NLS Globals: these need to be defined in all contexts that make
|
||||
* use of our `nls.localize` and `nls.localize2` functions. This includes:
|
||||
* - Electron main process
|
||||
* - Electron window (renderer) process
|
||||
* - Utility Process
|
||||
* - Node.js
|
||||
* - Browser
|
||||
* - Web worker
|
||||
*
|
||||
* That is because during build time we strip out all english strings from
|
||||
* the resulting JS code and replace it with a <number> that is then looked
|
||||
* up from the `_VSCODE_NLS_MESSAGES` array.
|
||||
*/
|
||||
declare global {
|
||||
/**
|
||||
* All NLS messages produced by `localize` and `localize2` calls
|
||||
* under `src/vs` translated to the language as indicated by
|
||||
* `_VSCODE_NLS_LANGUAGE`.
|
||||
*
|
||||
* Instead of accessing this global variable directly, use function getNLSMessages.
|
||||
*/
|
||||
var _VSCODE_NLS_MESSAGES: string[]
|
||||
/**
|
||||
* The actual language of the NLS messages (e.g. 'en', de' or 'pt-br').
|
||||
*
|
||||
* Instead of accessing this global variable directly, use function getNLSLanguage.
|
||||
*/
|
||||
var _VSCODE_NLS_LANGUAGE: string | undefined
|
||||
}
|
||||
|
||||
// fake export to make global work
|
||||
export {}
|
||||
31
jetbrains/host/typings/vscode-globals-product.d.ts
vendored
Normal file
31
jetbrains/host/typings/vscode-globals-product.d.ts
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// AMD2ESM migration relevant
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Holds the file root for resources.
|
||||
*/
|
||||
var _VSCODE_FILE_ROOT: string
|
||||
|
||||
/**
|
||||
* CSS loader that's available during development time.
|
||||
* DO NOT call directly, instead just import css modules, like `import 'some.css'`
|
||||
*/
|
||||
var _VSCODE_CSS_LOAD: (module: string) => void
|
||||
|
||||
/**
|
||||
* @deprecated You MUST use `IProductService` whenever possible.
|
||||
*/
|
||||
var _VSCODE_PRODUCT_JSON: Record<string, any>
|
||||
/**
|
||||
* @deprecated You MUST use `IProductService` whenever possible.
|
||||
*/
|
||||
var _VSCODE_PACKAGE_JSON: Record<string, any>
|
||||
}
|
||||
|
||||
// fake export to make global work
|
||||
export {}
|
||||
20
jetbrains/host/typings/vscode-globals-ttp.d.ts
vendored
Normal file
20
jetbrains/host/typings/vscode-globals-ttp.d.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// AMD2ESM migration relevant
|
||||
|
||||
declare global {
|
||||
var _VSCODE_WEB_PACKAGE_TTP:
|
||||
| Pick<
|
||||
TrustedTypePolicy<{
|
||||
createScriptURL(value: string): string
|
||||
}>,
|
||||
"name" | "createScriptURL"
|
||||
>
|
||||
| undefined
|
||||
}
|
||||
|
||||
// fake export to make global work
|
||||
export {}
|
||||
50
jetbrains/plugin/.gitignore
vendored
Normal file
50
jetbrains/plugin/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
src/main/resources/ai/roocode/jetbrains/plugin/config/plugin.properties
|
||||
|
||||
### Deps ###
|
||||
prodDep.txt
|
||||
plugins
|
||||
31
jetbrains/plugin/.run/Run Plugin.run.xml
Normal file
31
jetbrains/plugin/.run/Run Plugin.run.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Plugin" 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$/jetbrains/plugin" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="runIde" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
255
jetbrains/plugin/build.gradle.kts
Normal file
255
jetbrains/plugin/build.gradle.kts
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: APACHE2.0
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Convenient for reading variables from gradle.properties
|
||||
fun properties(key: String) = providers.gradleProperty(key)
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("org.jetbrains.kotlin.jvm") version "1.8.10"
|
||||
id("org.jetbrains.intellij") version "1.17.4"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
|
||||
id("io.gitlab.arturbosch.detekt") version "1.23.4"
|
||||
}
|
||||
|
||||
apply("genPlatform.gradle")
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// The 'debugMode' setting controls how plugin resources are prepared during the build process.
|
||||
// It supports the following three modes:
|
||||
//
|
||||
// 1. "idea" — Local development mode (used for debugging VSCode plugin integration)
|
||||
// - Copies theme resources from src/main/resources/themes to:
|
||||
// ../resources/<vscodePlugin>/src/integrations/theme/default-themes/
|
||||
// - Automatically creates a .env file, which the Extension Host (Node.js side) reads at runtime.
|
||||
// - Enables the VSCode plugin to load resources from this directory for integration testing.
|
||||
// - Typically used when running IntelliJ with an Extension Host for live debugging and hot-reloading.
|
||||
//
|
||||
// 2. "release" — Production build mode (used to generate deployment artifacts)
|
||||
// - Requires platform.zip to exist, which can be retrieved via git-lfs or generated with genPlatform.gradle.
|
||||
// - This file includes the full runtime environment for VSCode plugins (e.g., node_modules, platform.txt).
|
||||
// - The zip is extracted to build/platform/, and its node_modules take precedence over other dependencies.
|
||||
// - Copies compiled host outputs (dist, package.json, node_modules) and plugin resources.
|
||||
// - The result is a fully self-contained package ready for deployment across platforms.
|
||||
//
|
||||
// 3. "none" (default) — Lightweight mode (used for testing and CI)
|
||||
// - Does not rely on platform.zip or prepare VSCode runtime resources.
|
||||
// - Only copies the plugin's core assets such as themes.
|
||||
// - Useful for early-stage development, static analysis, unit tests, and continuous integration pipelines.
|
||||
//
|
||||
// How to configure:
|
||||
// - Set via gradle argument: -PdebugMode=idea / release / none
|
||||
// Example: ./gradlew prepareSandbox -PdebugMode=idea
|
||||
// - Defaults to "none" if not explicitly set.
|
||||
// ------------------------------------------------------------
|
||||
ext {
|
||||
set("debugMode", project.findProperty("debugMode") ?: "none")
|
||||
set("debugResource", project.projectDir.resolve("../resources").absolutePath)
|
||||
set("vscodePlugin", project.findProperty("vscodePlugin") ?: "roo-code")
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
tasks.findByName(":prepareSandbox")?.inputs?.properties?.put("build_mode", ext.get("debugMode"))
|
||||
}
|
||||
|
||||
fun Sync.prepareSandbox() {
|
||||
// Set duplicate strategy to include files, with later sources taking precedence
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
|
||||
if (ext.get("debugMode") == "idea") {
|
||||
from("${project.projectDir.absolutePath}/src/main/resources/themes/") {
|
||||
into("${ext.get("debugResource")}/${ext.get("vscodePlugin")}/integrations/theme/default-themes/")
|
||||
}
|
||||
doLast {
|
||||
val vscodePluginDir = File("${ext.get("debugResource")}/${ext.get("vscodePlugin")}")
|
||||
vscodePluginDir.mkdirs()
|
||||
File(vscodePluginDir, ".env").createNewFile()
|
||||
}
|
||||
} else {
|
||||
val vscodePluginDir = File("./plugins/${ext.get("vscodePlugin")}")
|
||||
if (!vscodePluginDir.exists()) {
|
||||
throw IllegalStateException("missing plugin dir")
|
||||
}
|
||||
val list = mutableListOf<String>()
|
||||
val depfile = File("prodDep.txt")
|
||||
if (!depfile.exists()) {
|
||||
throw IllegalStateException("missing prodDep.txt")
|
||||
}
|
||||
depfile.readLines().let {
|
||||
it.forEach { line ->
|
||||
list.add(line.substringAfterLast("node_modules/") + "/**")
|
||||
}
|
||||
}
|
||||
|
||||
from("../host/dist") { into("${intellij.pluginName.get()}/runtime/") }
|
||||
from("../host/package.json") { into("${intellij.pluginName.get()}/runtime/") }
|
||||
|
||||
// First copy host node_modules
|
||||
from("../resources/node_modules") {
|
||||
into("${intellij.pluginName.get()}/node_modules/")
|
||||
list.forEach {
|
||||
include(it)
|
||||
}
|
||||
}
|
||||
|
||||
from("${vscodePluginDir.path}/extension") { into("${intellij.pluginName.get()}/${ext.get("vscodePlugin")}") }
|
||||
from("src/main/resources/themes/") { into("${intellij.pluginName.get()}/${ext.get("vscodePlugin")}/integrations/theme/default-themes/") }
|
||||
|
||||
// The platform.zip file required for release mode is associated with the code in ../base/vscode, currently using version 1.100.0. If upgrading this code later
|
||||
// Need to modify the vscodeVersion value in gradle.properties, then execute the task named genPlatform, which will generate a new platform.zip file for submission
|
||||
// To support new architectures, modify according to the logic in genPlatform.gradle script
|
||||
if (ext.get("debugMode") == "release") {
|
||||
// Check if platform.zip file exists and is larger than 1MB, otherwise throw exception
|
||||
val platformZip = File("platform.zip")
|
||||
if (platformZip.exists() && platformZip.length() >= 1024 * 1024) {
|
||||
// Extract platform.zip to the platform subdirectory under the project build directory
|
||||
val platformDir = File("${layout.buildDirectory.get().asFile}/platform")
|
||||
platformDir.mkdirs()
|
||||
copy {
|
||||
from(zipTree(platformZip))
|
||||
into(platformDir)
|
||||
}
|
||||
} else {
|
||||
throw IllegalStateException("platform.zip file does not exist or is smaller than 1MB. This file is supported through git lfs and needs to be obtained through git lfs")
|
||||
}
|
||||
|
||||
from(File(layout.buildDirectory.get().asFile, "platform/platform.txt")) { into("${intellij.pluginName.get()}/") }
|
||||
// Copy platform node_modules last to ensure it takes precedence over host node_modules
|
||||
from(File(layout.buildDirectory.get().asFile, "platform/node_modules")) { into("${intellij.pluginName.get()}/node_modules") }
|
||||
}
|
||||
|
||||
doLast {
|
||||
File("${destinationDir}/${intellij.pluginName.get()}/${ext.get("vscodePlugin")}/.env").createNewFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group = properties("pluginGroup").get()
|
||||
version = properties("pluginVersion").get()
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.squareup.okhttp3:okhttp:4.10.0")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4")
|
||||
}
|
||||
|
||||
// Configure Java toolchain to force Java 17
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
// Configure Gradle IntelliJ Plugin
|
||||
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
|
||||
intellij {
|
||||
version = properties("platformVersion")
|
||||
type = properties("platformType")
|
||||
|
||||
plugins.set(
|
||||
listOf(
|
||||
"com.intellij.java",
|
||||
// Add JCEF support
|
||||
"org.jetbrains.plugins.terminal"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
// Create task for generating configuration files
|
||||
register("generateConfigProperties") {
|
||||
description = "Generate properties file containing plugin configuration"
|
||||
doLast {
|
||||
val configDir = File("$projectDir/src/main/resources/ai/roocode/jetbrains/plugin/config")
|
||||
configDir.mkdirs()
|
||||
|
||||
val configFile = File(configDir, "plugin.properties")
|
||||
configFile.writeText("debug.mode=${ext.get("debugMode")}")
|
||||
configFile.appendText("\n")
|
||||
configFile.appendText("debug.resource=${ext.get("debugResource")}")
|
||||
println("Configuration file generated: ${configFile.absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
prepareSandbox {
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
// Generate configuration file before compilation
|
||||
withType<JavaCompile> {
|
||||
dependsOn("generateConfigProperties")
|
||||
}
|
||||
|
||||
// Set the JVM compatibility versions
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
dependsOn("generateConfigProperties")
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
withType<JavaCompile> {
|
||||
sourceCompatibility = "17"
|
||||
targetCompatibility = "17"
|
||||
}
|
||||
|
||||
patchPluginXml {
|
||||
version.set(properties("pluginVersion"))
|
||||
sinceBuild.set(properties("pluginSinceBuild"))
|
||||
untilBuild.set("")
|
||||
}
|
||||
|
||||
signPlugin {
|
||||
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
|
||||
privateKey.set(System.getenv("PRIVATE_KEY"))
|
||||
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
|
||||
}
|
||||
|
||||
publishPlugin {
|
||||
token.set(System.getenv("PUBLISH_TOKEN"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure ktlint
|
||||
ktlint {
|
||||
version.set("0.50.0")
|
||||
debug.set(false)
|
||||
verbose.set(true)
|
||||
android.set(false)
|
||||
outputToConsole.set(true)
|
||||
outputColorName.set("RED")
|
||||
ignoreFailures.set(true)
|
||||
enableExperimentalRules.set(false)
|
||||
filter {
|
||||
exclude("**/generated/**")
|
||||
include("**/kotlin/**")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure detekt
|
||||
detekt {
|
||||
toolVersion = "1.23.4"
|
||||
config.setFrom(file("detekt.yml"))
|
||||
buildUponDefaultConfig = true
|
||||
allRules = false
|
||||
|
||||
reports {
|
||||
html.required.set(true)
|
||||
xml.required.set(true)
|
||||
txt.required.set(true)
|
||||
sarif.required.set(true)
|
||||
md.required.set(true)
|
||||
}
|
||||
}
|
||||
729
jetbrains/plugin/detekt.yml
Normal file
729
jetbrains/plugin/detekt.yml
Normal file
|
|
@ -0,0 +1,729 @@
|
|||
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Detekt configuration file for Roo Code JetBrains Plugin
|
||||
# https://detekt.github.io/detekt/configurations.html
|
||||
|
||||
build:
|
||||
maxIssues: 5000
|
||||
excludeCorrectable: false
|
||||
weights:
|
||||
# complexity: 2
|
||||
# LongParameterList: 1
|
||||
# style: 1
|
||||
# comments: 1
|
||||
|
||||
config:
|
||||
validation: true
|
||||
warningsAsErrors: false
|
||||
checkExhaustiveness: false
|
||||
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
||||
excludes: ''
|
||||
|
||||
processors:
|
||||
active: true
|
||||
exclude:
|
||||
- 'DetektProgressListener'
|
||||
# - 'KtFileCountProcessor'
|
||||
# - 'PackageCountProcessor'
|
||||
# - 'ClassCountProcessor'
|
||||
# - 'FunctionCountProcessor'
|
||||
# - 'PropertyCountProcessor'
|
||||
# - 'ProjectComplexityProcessor'
|
||||
# - 'ProjectCognitiveComplexityProcessor'
|
||||
# - 'ProjectLLOCProcessor'
|
||||
# - 'ProjectCLOCProcessor'
|
||||
# - 'ProjectLOCProcessor'
|
||||
# - 'ProjectSLOCProcessor'
|
||||
# - 'LicenseHeaderLoaderExtension'
|
||||
|
||||
console-reports:
|
||||
active: true
|
||||
exclude:
|
||||
- 'ProjectStatisticsReport'
|
||||
- 'ComplexityReport'
|
||||
- 'NotificationReport'
|
||||
- 'FindingsReport'
|
||||
- 'FileBasedFindingsReport'
|
||||
- 'LiteFindingsReport'
|
||||
|
||||
output-reports:
|
||||
active: true
|
||||
exclude:
|
||||
# - 'TxtOutputReport'
|
||||
# - 'XmlOutputReport'
|
||||
# - 'HtmlOutputReport'
|
||||
# - 'MdOutputReport'
|
||||
# - 'SarifOutputReport'
|
||||
|
||||
comments:
|
||||
active: true
|
||||
AbsentOrWrongFileLicense:
|
||||
active: false
|
||||
licenseTemplateFile: 'license.template'
|
||||
licenseTemplateIsRegex: false
|
||||
CommentOverPrivateFunction:
|
||||
active: false
|
||||
CommentOverPrivateProperty:
|
||||
active: false
|
||||
DeprecatedBlockTag:
|
||||
active: false
|
||||
EndOfSentenceFormat:
|
||||
active: false
|
||||
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
|
||||
KDocReferencesNonPublicProperty:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
OutdatedDocumentation:
|
||||
active: false
|
||||
matchTypeParameters: true
|
||||
matchDeclarationsOrder: true
|
||||
allowParamOnConstructorProperties: false
|
||||
UndocumentedPublicClass:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
searchInNestedClass: true
|
||||
searchInInnerClass: true
|
||||
searchInInnerObject: true
|
||||
searchInInnerInterface: true
|
||||
UndocumentedPublicFunction:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UndocumentedPublicProperty:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
|
||||
complexity:
|
||||
active: true
|
||||
CognitiveComplexMethod:
|
||||
active: false
|
||||
threshold: 15
|
||||
ComplexCondition:
|
||||
active: true
|
||||
threshold: 4
|
||||
ComplexInterface:
|
||||
active: false
|
||||
threshold: 10
|
||||
includeStaticDeclarations: false
|
||||
includePrivateDeclarations: false
|
||||
CyclomaticComplexMethod:
|
||||
active: true
|
||||
threshold: 15
|
||||
ignoreSingleWhenExpression: false
|
||||
ignoreSimpleWhenEntries: false
|
||||
ignoreNestingFunctions: false
|
||||
nestingFunctions:
|
||||
- 'also'
|
||||
- 'apply'
|
||||
- 'forEach'
|
||||
- 'isNotNull'
|
||||
- 'ifNull'
|
||||
- 'let'
|
||||
- 'run'
|
||||
- 'use'
|
||||
- 'with'
|
||||
LabeledExpression:
|
||||
active: false
|
||||
ignoredLabels: []
|
||||
LargeClass:
|
||||
active: true
|
||||
threshold: 600
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 60
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 6
|
||||
constructorThreshold: 7
|
||||
ignoreDefaultParameters: false
|
||||
ignoreDataClasses: true
|
||||
ignoreAnnotatedParameter: []
|
||||
MethodOverloading:
|
||||
active: false
|
||||
threshold: 6
|
||||
NamedArguments:
|
||||
active: false
|
||||
threshold: 3
|
||||
ignoreArgumentsMatchingNames: false
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 4
|
||||
NestedScopeFunctions:
|
||||
active: false
|
||||
threshold: 1
|
||||
functions:
|
||||
- 'kotlin.apply'
|
||||
- 'kotlin.run'
|
||||
- 'kotlin.with'
|
||||
- 'kotlin.let'
|
||||
- 'kotlin.also'
|
||||
ReplaceSafeCallChainWithRun:
|
||||
active: false
|
||||
StringLiteralDuplication:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
threshold: 3
|
||||
ignoreAnnotation: true
|
||||
excludeStringsWithLessThan5Characters: true
|
||||
ignoreStringsRegex: '$^'
|
||||
TooManyFunctions:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
thresholdInFiles: 11
|
||||
thresholdInClasses: 11
|
||||
thresholdInInterfaces: 11
|
||||
thresholdInObjects: 11
|
||||
thresholdInEnums: 11
|
||||
ignoreDeprecated: false
|
||||
ignorePrivate: false
|
||||
ignoreOverridden: false
|
||||
|
||||
coroutines:
|
||||
active: true
|
||||
GlobalCoroutineUsage:
|
||||
active: false
|
||||
InjectDispatcher:
|
||||
active: true
|
||||
dispatcherNames:
|
||||
- 'IO'
|
||||
- 'Default'
|
||||
- 'Unconfined'
|
||||
RedundantSuspendModifier:
|
||||
active: true
|
||||
SleepInsteadOfDelay:
|
||||
active: true
|
||||
SuspendFunWithCoroutineScopeReceiver:
|
||||
active: false
|
||||
SuspendFunWithFlowReturnType:
|
||||
active: true
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
EmptyCatchBlock:
|
||||
active: true
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
EmptyClassBlock:
|
||||
active: true
|
||||
EmptyDefaultConstructor:
|
||||
active: true
|
||||
EmptyDoWhileBlock:
|
||||
active: true
|
||||
EmptyElseBlock:
|
||||
active: true
|
||||
EmptyFinallyBlock:
|
||||
active: true
|
||||
EmptyForBlock:
|
||||
active: true
|
||||
EmptyFunctionBlock:
|
||||
active: true
|
||||
ignoreOverridden: false
|
||||
EmptyIfBlock:
|
||||
active: true
|
||||
EmptyInitBlock:
|
||||
active: true
|
||||
EmptyKtFile:
|
||||
active: true
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
EmptyTryBlock:
|
||||
active: true
|
||||
EmptyWhenBlock:
|
||||
active: true
|
||||
EmptyWhileBlock:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
ExceptionRaisedInUnexpectedLocation:
|
||||
active: true
|
||||
methodNames:
|
||||
- 'equals'
|
||||
- 'finalize'
|
||||
- 'hashCode'
|
||||
- 'toString'
|
||||
InstanceOfCheckForException:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
NotImplementedDeclaration:
|
||||
active: false
|
||||
ObjectExtendsThrowable:
|
||||
active: false
|
||||
PrintStackTrace:
|
||||
active: true
|
||||
RethrowCaughtException:
|
||||
active: true
|
||||
ReturnFromFinally:
|
||||
active: true
|
||||
ignoreLabeled: false
|
||||
SwallowedException:
|
||||
active: true
|
||||
ignoredExceptionTypes:
|
||||
- 'InterruptedException'
|
||||
- 'MalformedURLException'
|
||||
- 'NumberFormatException'
|
||||
- 'ParseException'
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
ThrowingExceptionFromFinally:
|
||||
active: true
|
||||
ThrowingExceptionInMain:
|
||||
active: false
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptions:
|
||||
- 'ArrayIndexOutOfBoundsException'
|
||||
- 'Exception'
|
||||
- 'IllegalArgumentException'
|
||||
- 'IllegalMonitorStateException'
|
||||
- 'IllegalStateException'
|
||||
- 'IndexOutOfBoundsException'
|
||||
- 'NullPointerException'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
ThrowingNewInstanceOfSameException:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptionNames:
|
||||
- 'ArrayIndexOutOfBoundsException'
|
||||
- 'Error'
|
||||
- 'Exception'
|
||||
- 'IllegalMonitorStateException'
|
||||
- 'NullPointerException'
|
||||
- 'IndexOutOfBoundsException'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
exceptionNames:
|
||||
- 'Error'
|
||||
- 'Exception'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
|
||||
naming:
|
||||
active: true
|
||||
BooleanPropertyNaming:
|
||||
active: false
|
||||
allowedPattern: '^(is|has|are)'
|
||||
ClassNaming:
|
||||
active: true
|
||||
classPattern: '[A-Z][a-zA-Z0-9]*'
|
||||
ConstructorParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
EnumNaming:
|
||||
active: true
|
||||
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
||||
ForbiddenClassName:
|
||||
active: false
|
||||
forbiddenName: []
|
||||
FunctionMaxLength:
|
||||
active: false
|
||||
maximumFunctionNameLength: 30
|
||||
FunctionMinLength:
|
||||
active: false
|
||||
minimumFunctionNameLength: 3
|
||||
FunctionNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
functionPattern: '[a-z][a-zA-Z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreAnnotated: ['Composable']
|
||||
FunctionParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
InvalidPackageDeclaration:
|
||||
active: true
|
||||
rootPackage: ''
|
||||
requireRootInDeclaration: false
|
||||
LambdaParameterNaming:
|
||||
active: false
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*|_'
|
||||
MatchingDeclarationName:
|
||||
active: true
|
||||
mustBeFirst: true
|
||||
MemberNameEqualsClassName:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
NoNameShadowing:
|
||||
active: true
|
||||
NonBooleanPropertyPrefixedWithIs:
|
||||
active: false
|
||||
ObjectPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
||||
PackageNaming:
|
||||
active: true
|
||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||
VariableMaxLength:
|
||||
active: false
|
||||
maximumVariableNameLength: 64
|
||||
VariableMinLength:
|
||||
active: false
|
||||
minimumVariableNameLength: 1
|
||||
VariableNaming:
|
||||
active: true
|
||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
|
||||
performance:
|
||||
active: true
|
||||
ArrayPrimitive:
|
||||
active: true
|
||||
CouldBeSequence:
|
||||
active: false
|
||||
threshold: 3
|
||||
ForEachOnRange:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
SpreadOperator:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UnnecessaryTemporaryInstantiation:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
AvoidReferentialEquality:
|
||||
active: true
|
||||
forbiddenTypePatterns:
|
||||
- 'kotlin.String'
|
||||
CastToNullableType:
|
||||
active: false
|
||||
Deprecation:
|
||||
active: false
|
||||
DontDowncastCollectionTypes:
|
||||
active: false
|
||||
DoubleMutabilityForCollection:
|
||||
active: true
|
||||
mutableTypes:
|
||||
- 'kotlin.collections.MutableList'
|
||||
- 'kotlin.collections.MutableMap'
|
||||
- 'kotlin.collections.MutableSet'
|
||||
- 'java.util.ArrayList'
|
||||
- 'java.util.LinkedHashSet'
|
||||
- 'java.util.HashSet'
|
||||
- 'java.util.LinkedHashMap'
|
||||
- 'java.util.HashMap'
|
||||
ElseCaseInsteadOfExhaustiveWhen:
|
||||
active: false
|
||||
EqualsAlwaysReturnsTrueOrFalse:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: false
|
||||
ExplicitGarbageCollectionCall:
|
||||
active: true
|
||||
HasPlatformType:
|
||||
active: true
|
||||
IgnoredReturnValue:
|
||||
active: true
|
||||
restrictToConfig: true
|
||||
returnValueAnnotations:
|
||||
- '*.CheckResult'
|
||||
- '*.CheckReturnValue'
|
||||
ignoreReturnValueAnnotations:
|
||||
- '*.CanIgnoreReturnValue'
|
||||
returnValueTypes:
|
||||
- 'kotlin.sequences.Sequence'
|
||||
- 'kotlinx.coroutines.flow.Flow'
|
||||
- 'java.util.stream.Stream'
|
||||
ignoreFunctionCall: []
|
||||
ImplicitDefaultLocale:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: false
|
||||
allowExplicitReturnType: true
|
||||
InvalidRange:
|
||||
active: true
|
||||
IteratorHasNextCallsNextMethod:
|
||||
active: true
|
||||
IteratorNotThrowingNoSuchElementException:
|
||||
active: true
|
||||
LateinitUsage:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ignoreOnClassesPattern: ''
|
||||
MapGetWithNotNullAssertionOperator:
|
||||
active: true
|
||||
MissingPackageDeclaration:
|
||||
active: false
|
||||
excludes: ['**/*.kts']
|
||||
NullCheckOnMutableProperty:
|
||||
active: false
|
||||
NullableToStringCall:
|
||||
active: false
|
||||
PropertyUsedBeforeDeclaration:
|
||||
active: false
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: false
|
||||
UnnecessaryNotNullOperator:
|
||||
active: true
|
||||
UnnecessarySafeCall:
|
||||
active: true
|
||||
UnreachableCatchBlock:
|
||||
active: true
|
||||
UnreachableCode:
|
||||
active: true
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UnsafeCast:
|
||||
active: true
|
||||
UnusedUnaryOperator:
|
||||
active: true
|
||||
UselessPostfixExpression:
|
||||
active: true
|
||||
WrongEqualsTypeParameter:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
AlsoCouldBeApply:
|
||||
active: false
|
||||
CanBeNonNullable:
|
||||
active: false
|
||||
CascadingCallWrapping:
|
||||
active: false
|
||||
includeElvis: true
|
||||
ClassOrdering:
|
||||
active: false
|
||||
CollapsibleIfStatements:
|
||||
active: false
|
||||
DataClassContainsFunctions:
|
||||
active: false
|
||||
conversionFunctionPrefix:
|
||||
- 'to'
|
||||
DataClassShouldBeImmutable:
|
||||
active: false
|
||||
DestructuringDeclarationWithTooManyEntries:
|
||||
active: true
|
||||
maxDestructuringEntries: 3
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: false
|
||||
ExplicitCollectionElementAccessMethod:
|
||||
active: false
|
||||
ExplicitItLambdaParameter:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ExpressionBodySyntax:
|
||||
active: false
|
||||
includeLineWrapping: false
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
comments:
|
||||
- value: 'FIXME:'
|
||||
reason: 'Forbidden FIXME todo marker'
|
||||
- value: 'STOPSHIP:'
|
||||
reason: 'Forbidden STOPSHIP todo marker'
|
||||
- value: 'TODO:'
|
||||
reason: 'Forbidden TODO todo marker'
|
||||
allowedPatterns: ''
|
||||
ForbiddenImport:
|
||||
active: false
|
||||
imports: []
|
||||
forbiddenPatterns: ''
|
||||
ForbiddenMethodCall:
|
||||
active: false
|
||||
methods:
|
||||
- 'kotlin.io.print'
|
||||
- 'kotlin.io.println'
|
||||
ForbiddenPublicDataClass:
|
||||
active: false
|
||||
excludes: ['**']
|
||||
ignorePackages:
|
||||
- '*.internal'
|
||||
- '*.internal.*'
|
||||
ForbiddenVoid:
|
||||
active: true
|
||||
ignoreOverridden: false
|
||||
ignoreUsageInGenerics: false
|
||||
FunctionOnlyReturningConstant:
|
||||
active: true
|
||||
ignoreOverridableFunction: true
|
||||
ignoreActualFunction: true
|
||||
excludedFunctions: []
|
||||
LibraryCodeMustSpecifyReturnType:
|
||||
active: false
|
||||
excludes: ['**']
|
||||
LibraryEntitiesShouldNotBePublic:
|
||||
active: false
|
||||
excludes: ['**']
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 1
|
||||
MagicNumber:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ignoreNumbers:
|
||||
- '-1'
|
||||
- '0'
|
||||
- '1'
|
||||
- '2'
|
||||
ignoreHashCodeFunction: true
|
||||
ignorePropertyDeclaration: false
|
||||
ignoreLocalVariableDeclaration: false
|
||||
ignoreConstantDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotation: false
|
||||
ignoreNamedArgument: true
|
||||
ignoreEnums: false
|
||||
ignoreRanges: false
|
||||
ignoreExtensionFunctions: true
|
||||
BracesOnIfStatements:
|
||||
active: false
|
||||
singleLine: 'always'
|
||||
multiLine: 'always'
|
||||
MandatoryBracesLoops:
|
||||
active: false
|
||||
MaxChainedCallsOnSameLine:
|
||||
active: false
|
||||
maxChainedCalls: 5
|
||||
MaxLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
excludeCommentStatements: false
|
||||
MayBeConst:
|
||||
active: true
|
||||
ModifierOrder:
|
||||
active: true
|
||||
MultilineLambdaItParameter:
|
||||
active: false
|
||||
NestedClassesVisibility:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: true
|
||||
NoTabs:
|
||||
active: false
|
||||
NullableBooleanCheck:
|
||||
active: false
|
||||
ObjectLiteralToLambda:
|
||||
active: true
|
||||
OptionalAbstractKeyword:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: false
|
||||
PreferToOverPairSyntax:
|
||||
active: false
|
||||
ProtectedMemberInFinalClass:
|
||||
active: true
|
||||
RedundantExplicitType:
|
||||
active: false
|
||||
RedundantHigherOrderMapUsage:
|
||||
active: true
|
||||
RedundantVisibilityModifierRule:
|
||||
active: false
|
||||
ReturnCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludedFunctions:
|
||||
- 'equals'
|
||||
excludeLabeled: false
|
||||
excludeReturnFromLambda: true
|
||||
excludeGuardClauses: false
|
||||
SafeCast:
|
||||
active: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: true
|
||||
SpacingBetweenPackageAndImports:
|
||||
active: false
|
||||
ThrowsCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludeGuardClauses: false
|
||||
TrailingWhitespace:
|
||||
active: false
|
||||
TrimMultilineRawString:
|
||||
active: false
|
||||
UnderscoresInNumericLiterals:
|
||||
active: false
|
||||
acceptableLength: 4
|
||||
allowNonStandardGrouping: false
|
||||
UnnecessaryAbstractClass:
|
||||
active: true
|
||||
UnnecessaryAnnotationUseSiteTarget:
|
||||
active: false
|
||||
UnnecessaryApply:
|
||||
active: true
|
||||
UnnecessaryFilter:
|
||||
active: true
|
||||
UnnecessaryInheritance:
|
||||
active: true
|
||||
UnnecessaryInnerClass:
|
||||
active: false
|
||||
UnnecessaryLet:
|
||||
active: false
|
||||
UnnecessaryParentheses:
|
||||
active: false
|
||||
UntilInsteadOfRangeTo:
|
||||
active: false
|
||||
UnusedImports:
|
||||
active: false
|
||||
UnusedParameter:
|
||||
active: true
|
||||
allowedNames: 'ignored|expected'
|
||||
UnusedPrivateClass:
|
||||
active: true
|
||||
UnusedPrivateMember:
|
||||
active: true
|
||||
allowedNames: ''
|
||||
UnusedPrivateProperty:
|
||||
active: true
|
||||
allowedNames: '_|ignored|expected|serialVersionUID'
|
||||
UseAnyOrNoneInsteadOfFind:
|
||||
active: true
|
||||
UseArrayLiteralsInAnnotations:
|
||||
active: true
|
||||
UseCheckNotNull:
|
||||
active: true
|
||||
UseCheckOrError:
|
||||
active: true
|
||||
UseDataClass:
|
||||
active: false
|
||||
allowVars: false
|
||||
UseEmptyCounterpart:
|
||||
active: false
|
||||
UseIfEmptyOrIfBlank:
|
||||
active: false
|
||||
UseIfInsteadOfWhen:
|
||||
active: false
|
||||
UseIsNullOrEmpty:
|
||||
active: true
|
||||
UseOrEmpty:
|
||||
active: true
|
||||
UseRequire:
|
||||
active: true
|
||||
UseRequireNotNull:
|
||||
active: true
|
||||
UselessCallOnNotNull:
|
||||
active: true
|
||||
UtilityClassWithPublicConstructor:
|
||||
active: true
|
||||
VarCouldBeVal:
|
||||
active: true
|
||||
ignoreLateinitVar: false
|
||||
WildcardImport:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
excludeImports:
|
||||
- 'java.util.*'
|
||||
174
jetbrains/plugin/genPlatform.gradle
Normal file
174
jetbrains/plugin/genPlatform.gradle
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import java.nio.file.*
|
||||
import java.security.MessageDigest
|
||||
|
||||
|
||||
tasks.register('genPlatform', Zip) {
|
||||
download(project)
|
||||
from(new File(project.buildDir, "genPlatform/gen"))
|
||||
into("")
|
||||
destinationDirectory = project.projectDir
|
||||
archiveFileName = "platform.zip"
|
||||
}
|
||||
def download(Project project){
|
||||
|
||||
def version = project.findProperty("vscodeVersion")
|
||||
String windows_x64 = "https://update.code.visualstudio.com/${version}/win32-x64-archive/stable"
|
||||
String mac_x64 = "https://update.code.visualstudio.com/${version}/darwin/stable"
|
||||
String mac_arm64 = "https://update.code.visualstudio.com/${version}/darwin-arm64/stable"
|
||||
String linux_x64 = "https://update.code.visualstudio.com/${version}/linux-x64/stable"
|
||||
// To support other platforms, need to synchronously modify the initPlatfromFiles method in WecoderPlugin
|
||||
|
||||
def list = [] // node_module directories for multiple platforms
|
||||
|
||||
def projectBuild = new File(project.buildDir,"genPlatform")
|
||||
projectBuild.mkdirs();
|
||||
println "Downloading Windows platform files"
|
||||
def windowsZipFile = new File(projectBuild,"windows-x64.zip")
|
||||
if (!windowsZipFile.exists()) {
|
||||
windowsZipFile << new URL(windows_x64).openStream()
|
||||
} else {
|
||||
println "Windows platform file already exists, skipping download"
|
||||
}
|
||||
|
||||
def windowsDir = new File(projectBuild, "windows-x64")
|
||||
copy {
|
||||
from(zipTree(new File(projectBuild, "windows-x64.zip")))
|
||||
into(windowsDir)
|
||||
}
|
||||
new File(windowsDir, "resources/app/node_modules").renameTo( new File(windowsDir, "resources/app/windows-x64"))
|
||||
list << new File(windowsDir, "resources/app/windows-x64")
|
||||
|
||||
println "Downloading Mac x64 platform files"
|
||||
def macX64ZipFile = new File(projectBuild,"darwin-x64.zip")
|
||||
if (!macX64ZipFile.exists()) {
|
||||
macX64ZipFile << new URL(mac_x64).openStream()
|
||||
} else {
|
||||
println "Mac x64 platform file already exists, skipping download"
|
||||
}
|
||||
|
||||
def macX64Dir = new File(projectBuild, "darwin-x64")
|
||||
copy {
|
||||
from(zipTree(new File(projectBuild, "darwin-x64.zip")))
|
||||
into(macX64Dir)
|
||||
}
|
||||
new File(macX64Dir, "Visual Studio Code.app/Contents/Resources/app/node_modules").renameTo(new File(macX64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-x64"))
|
||||
list << new File(macX64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-x64")
|
||||
|
||||
println "Downloading Mac arm64 platform files"
|
||||
def macArm64ZipFile = new File(projectBuild,"darwin-arm64.zip")
|
||||
if (!macArm64ZipFile.exists()) {
|
||||
macArm64ZipFile << new URL(mac_arm64).openStream()
|
||||
} else {
|
||||
println "Mac arm64 platform file already exists, skipping download"
|
||||
}
|
||||
|
||||
def macArm64Dir = new File(projectBuild, "darwin-arm64")
|
||||
copy {
|
||||
from(zipTree(new File(projectBuild, "darwin-arm64.zip")))
|
||||
into(macArm64Dir)
|
||||
}
|
||||
new File(macArm64Dir, "Visual Studio Code.app/Contents/Resources/app/node_modules").renameTo(new File(macArm64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-arm64"))
|
||||
list << new File(macArm64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-arm64")
|
||||
|
||||
println "Downloading Linux x64 platform files"
|
||||
def linuxZipFile = new File(projectBuild,"linux-x64.zip")
|
||||
if (!linuxZipFile.exists()) {
|
||||
linuxZipFile << new URL(linux_x64).openStream()
|
||||
} else {
|
||||
println "Linux x64 platform file already exists, skipping download"
|
||||
}
|
||||
|
||||
def linuxDir = new File(projectBuild, "linux-x64")
|
||||
copy {
|
||||
from(tarTree(resources.gzip(projectBuild.toPath().resolve("linux-x64.zip"))))
|
||||
into(linuxDir)
|
||||
}
|
||||
new File(linuxDir, "VSCode-linux-x64/resources/app/node_modules").renameTo(new File(linuxDir, "VSCode-linux-x64/resources/app/linux-x64"))
|
||||
list << new File(linuxDir, "VSCode-linux-x64/resources/app/linux-x64")
|
||||
|
||||
def targetDir = new File(projectBuild, "gen/node_modules")
|
||||
def txtFile = new File(projectBuild, "gen/platform.txt")
|
||||
mergeDirectories(list, targetDir,txtFile)
|
||||
|
||||
def zipFile = new File(project.projectDir,"platform.zip")
|
||||
if(zipFile.exists()) {
|
||||
zipFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def mergeDirectories(List<File> dirs, File targetDir, File outputFile = null) {
|
||||
def outputContent = new StringBuilder()
|
||||
if (!targetDir.exists()) {
|
||||
targetDir.mkdirs()
|
||||
}
|
||||
|
||||
// Collect all file paths (relative paths), ignore .DS_Store
|
||||
def allFiles = []
|
||||
dirs.each { dir ->
|
||||
if (dir.exists()) {
|
||||
dir.eachFileRecurse { file ->
|
||||
if (file.isFile() && file.name != ".DS_Store") { // Ignore .DS_Store
|
||||
def relativePath = dir.toPath().relativize(file.toPath()).toString()
|
||||
allFiles << [dir: dir, file: file, relativePath: relativePath]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group by relative path
|
||||
def groupedFiles = allFiles.groupBy { it.relativePath }
|
||||
|
||||
groupedFiles.each { relativePath, entries ->
|
||||
def targetFile = new File(targetDir, relativePath)
|
||||
targetFile.parentFile.mkdirs()
|
||||
|
||||
if (entries.size() == 1) {
|
||||
// Unique file, copy directly
|
||||
Files.copy(entries[0].file.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
} else {
|
||||
// Check if all file contents are the same
|
||||
def uniqueHashes = entries.collect { entry ->
|
||||
def file = entry.file
|
||||
def digest = MessageDigest.getInstance("SHA-256")
|
||||
file.withInputStream { is ->
|
||||
byte[] buffer = new byte[8192]
|
||||
int read
|
||||
while ((read = is.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, read)
|
||||
}
|
||||
}
|
||||
def hash = digest.digest().encodeHex().toString()
|
||||
[hash: hash, file: file, dir: entry.dir]
|
||||
}.groupBy { it.hash }
|
||||
|
||||
if (uniqueHashes.size() == 1) {
|
||||
// Same content, keep one copy
|
||||
Files.copy(entries[0].file.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
} else {
|
||||
// Different content, add directory name as suffix
|
||||
if (outputFile) {
|
||||
outputContent.append("$relativePath\n")
|
||||
} else {
|
||||
println "$relativePath"
|
||||
}
|
||||
uniqueHashes.each { hash, files ->
|
||||
def sourceFile = files[0].file
|
||||
def dirName = files[0].dir.name
|
||||
def newName = targetFile.name + dirName
|
||||
def conflictFile = new File(targetFile.parentFile, newName)
|
||||
Files.copy(sourceFile.toPath(), conflictFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (outputFile && outputContent.length() > 0) {
|
||||
outputFile.parentFile.mkdirs()
|
||||
outputFile.text = outputContent.toString()
|
||||
}
|
||||
}
|
||||
11
jetbrains/plugin/gradle.properties
Normal file
11
jetbrains/plugin/gradle.properties
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Plugin basic information
|
||||
pluginGroup=ai.roocode.jetbrains
|
||||
pluginVersion=4.88.0
|
||||
|
||||
# Platform basic information
|
||||
platformVersion=2024.1.4
|
||||
platformType=IC
|
||||
pluginSinceBuild=241
|
||||
|
||||
# Other project configurations can be added here
|
||||
vscodeVersion=1.100.0
|
||||
BIN
jetbrains/plugin/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
jetbrains/plugin/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
jetbrains/plugin/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
jetbrains/plugin/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#Wed Aug 20 11:09:31 ART 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
234
jetbrains/plugin/gradlew
vendored
Executable file
234
jetbrains/plugin/gradlew
vendored
Executable file
|
|
@ -0,0 +1,234 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
jetbrains/plugin/gradlew.bat
vendored
Normal file
89
jetbrains/plugin/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
31
jetbrains/plugin/package.json
Normal file
31
jetbrains/plugin/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@roo-code/jetbrains-plugin",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"del-cli": "^5.1.0",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"mkdirp": "^3.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "./gradlew clean",
|
||||
"build": "./gradlew buildPlugin -PdebugMode=idea",
|
||||
"run": "./gradlew runIde -PdebugMode=idea",
|
||||
"run:bundle": "./gradlew runIde -PdebugMode=release",
|
||||
"bundle": "./gradlew buildPlugin -PdebugMode=release",
|
||||
"bundle:name": "node scripts/get_bundle_name.js",
|
||||
"clean:roocode": "npx del-cli ./plugins/roo-code --force && npx mkdirp ./plugins/roo-code",
|
||||
"copy:roocode": "npx cpy '../../bin-unpacked/extension/**' './plugins/roo-code/extension' --parents",
|
||||
"clean:resource-roocode": "npx del-cli ../resources/roo-code --force",
|
||||
"copy:resource-roocode": "npx cpy '../../bin-unpacked/extension/**' '../resources/roo-code' --parents",
|
||||
"clean:resource-host": "npx del-cli ../resources/runtime --force",
|
||||
"copy:resource-host": "npx cpy '../host/dist/**' '../resources/runtime' --parents",
|
||||
"clean:resource-logs": "npx del-cli ../resources/logs --force",
|
||||
"copy:resource-logs": "npx mkdirp ../resources/logs",
|
||||
"clean:resource-nodemodules": "npx del-cli ../resources/node_modules --force && npx del-cli ../resources/package.json --force",
|
||||
"copy:resource-nodemodules": "cp ../host/package.json ../resources/package.json && npm install --prefix ../resources",
|
||||
"propDep": "npx del-cli ./propDep.txt --force && npm ls --omit=dev --all --parseable --prefix ../resources > ./prodDep.txt",
|
||||
"sync:version": "node scripts/sync_version.js",
|
||||
"sync:changelog": "node scripts/update_change_notes.js"
|
||||
}
|
||||
}
|
||||
44
jetbrains/plugin/scripts/get_bundle_name.js
Normal file
44
jetbrains/plugin/scripts/get_bundle_name.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from "fs"
|
||||
import { join, dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Get the bundle zip file name based on version from gradle.properties
|
||||
*/
|
||||
function getBundleName() {
|
||||
try {
|
||||
// Read version from gradle.properties
|
||||
const gradlePropertiesPath = join(__dirname, "../gradle.properties")
|
||||
const gradlePropertiesContent = readFileSync(gradlePropertiesPath, "utf8")
|
||||
|
||||
const gradleVersionMatch = gradlePropertiesContent.match(/^pluginVersion=(.+)$/m)
|
||||
if (!gradleVersionMatch) {
|
||||
throw new Error("pluginVersion not found in gradle.properties")
|
||||
}
|
||||
|
||||
const version = gradleVersionMatch[1].trim()
|
||||
|
||||
// Generate the bundle name following the pattern: Roo Code-{version}.zip
|
||||
const bundleName = `Roo Code-${version}.zip`
|
||||
|
||||
// Output just the filename for CI usage
|
||||
process.stdout.write(bundleName)
|
||||
|
||||
return bundleName
|
||||
} catch (error) {
|
||||
console.error("❌ Error getting bundle name:", error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the function if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
getBundleName()
|
||||
}
|
||||
|
||||
export default getBundleName
|
||||
54
jetbrains/plugin/scripts/sync_version.js
Normal file
54
jetbrains/plugin/scripts/sync_version.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from "fs"
|
||||
import { join, dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Sync version from src/package.json to jetbrains/plugin/gradle.properties
|
||||
*/
|
||||
function syncVersion() {
|
||||
try {
|
||||
// Read version from src/package.json
|
||||
const srcPackageJsonPath = join(__dirname, "../../../src/package.json")
|
||||
const srcPackageJson = JSON.parse(readFileSync(srcPackageJsonPath, "utf8"))
|
||||
const version = srcPackageJson.version
|
||||
|
||||
if (!version) {
|
||||
throw new Error("Version not found in src/package.json")
|
||||
}
|
||||
|
||||
console.log(`Found version: ${version}`)
|
||||
|
||||
// Read gradle.properties
|
||||
const gradlePropertiesPath = join(__dirname, "../gradle.properties")
|
||||
const gradlePropertiesContent = readFileSync(gradlePropertiesPath, "utf8")
|
||||
|
||||
// Update pluginVersion in gradle.properties
|
||||
const updatedContent = gradlePropertiesContent.replace(/^pluginVersion=.*$/m, `pluginVersion=${version}`)
|
||||
|
||||
// Check if the replacement was successful
|
||||
if (updatedContent === gradlePropertiesContent) {
|
||||
console.warn("Warning: pluginVersion property not found or already up to date")
|
||||
return
|
||||
}
|
||||
|
||||
// Write updated gradle.properties
|
||||
writeFileSync(gradlePropertiesPath, updatedContent, "utf8")
|
||||
|
||||
console.log(`✅ Successfully updated pluginVersion to ${version} in gradle.properties`)
|
||||
} catch (error) {
|
||||
console.error("❌ Error syncing version:", error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the sync if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
syncVersion()
|
||||
}
|
||||
|
||||
export default syncVersion
|
||||
158
jetbrains/plugin/scripts/update_change_notes.js
Normal file
158
jetbrains/plugin/scripts/update_change_notes.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from "fs"
|
||||
import { join, dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Update change-notes in plugin.xml based on version from gradle.properties and CHANGELOG.md
|
||||
*/
|
||||
function updateChangeNotes() {
|
||||
try {
|
||||
// Read version from gradle.properties
|
||||
const gradlePropertiesPath = join(__dirname, "../gradle.properties")
|
||||
const gradlePropertiesContent = readFileSync(gradlePropertiesPath, "utf8")
|
||||
|
||||
const gradleVersionMatch = gradlePropertiesContent.match(/^pluginVersion=(.+)$/m)
|
||||
if (!gradleVersionMatch) {
|
||||
throw new Error("pluginVersion not found in gradle.properties")
|
||||
}
|
||||
|
||||
const version = gradleVersionMatch[1].trim()
|
||||
console.log(`Found plugin version: ${version}`)
|
||||
|
||||
// Read CHANGELOG.md
|
||||
const changelogPath = join(__dirname, "../../../CHANGELOG.md")
|
||||
const changelogContent = readFileSync(changelogPath, "utf8")
|
||||
|
||||
// Find the version section in changelog
|
||||
const versionPattern = new RegExp(`## \\[v${version.replace(/\./g, "\\.")}\\]([\\s\\S]*?)(?=## \\[v|$)`)
|
||||
const changelogVersionMatch = changelogContent.match(versionPattern)
|
||||
|
||||
if (!changelogVersionMatch) {
|
||||
throw new Error(`Version ${version} not found in CHANGELOG.md`)
|
||||
}
|
||||
|
||||
const changelogSection = changelogVersionMatch[1].trim()
|
||||
console.log(`Found changelog section for version ${version}`)
|
||||
|
||||
// Convert markdown to HTML format suitable for plugin.xml
|
||||
const changeNotesHtml = convertMarkdownToHtml(changelogSection, version)
|
||||
|
||||
// Read plugin.xml
|
||||
const pluginXmlPath = join(__dirname, "../src/main/resources/META-INF/plugin.xml")
|
||||
const pluginXmlContent = readFileSync(pluginXmlPath, "utf8")
|
||||
|
||||
// Replace change-notes section
|
||||
const changeNotesPattern = /(<change-notes><!\[CDATA\[)([\s\S]*?)(\]\]><\/change-notes>)/
|
||||
const updatedPluginXml = pluginXmlContent.replace(changeNotesPattern, `$1\n${changeNotesHtml}\n $3`)
|
||||
|
||||
// Check if the replacement was successful
|
||||
if (updatedPluginXml === pluginXmlContent) {
|
||||
console.warn("Warning: change-notes section not found or already up to date")
|
||||
return
|
||||
}
|
||||
|
||||
// Write updated plugin.xml
|
||||
writeFileSync(pluginXmlPath, updatedPluginXml, "utf8")
|
||||
|
||||
console.log(`✅ Successfully updated change-notes for version ${version} in plugin.xml`)
|
||||
} catch (error) {
|
||||
console.error("❌ Error updating change-notes:", error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown changelog to HTML format suitable for plugin.xml
|
||||
*/
|
||||
function convertMarkdownToHtml(markdown, version) {
|
||||
let html = ` <h3>Version ${version}</h3>\n <ul>`
|
||||
|
||||
// Split into lines and process
|
||||
const lines = markdown.split("\n").filter((line) => line.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
// Skip empty lines and section headers
|
||||
if (!trimmedLine || trimmedLine.startsWith("##") || trimmedLine.startsWith("###")) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle main bullet points (features/changes)
|
||||
if (trimmedLine.startsWith("- ")) {
|
||||
const content = trimmedLine.substring(2).trim()
|
||||
const cleanContent = cleanMarkdownContent(content)
|
||||
|
||||
if (cleanContent) {
|
||||
html += `\n <li>${escapeHtml(cleanContent)}</li>`
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sub-bullet points (patch changes, etc.)
|
||||
else if (trimmedLine.match(/^\s*-\s/)) {
|
||||
const content = trimmedLine.replace(/^\s*-\s/, "").trim()
|
||||
const cleanContent = cleanMarkdownContent(content)
|
||||
|
||||
if (cleanContent) {
|
||||
html += `\n <li>${escapeHtml(cleanContent)}</li>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html += "\n </ul>"
|
||||
return html
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean markdown content by removing links, PR references, and contributor mentions
|
||||
*/
|
||||
function cleanMarkdownContent(content) {
|
||||
return (
|
||||
content
|
||||
// Remove PR links like [#2012](https://github.com/...)
|
||||
.replace(/\[#\d+\]\([^)]+\)\s*/g, "")
|
||||
// Remove commit hash links like [`1fd698a`](https://github.com/...)
|
||||
.replace(/\[`[^`]+`\]\([^)]+\)\s*/g, "")
|
||||
// Remove GitHub user links like [@catrielmuller](https://github.com/catrielmuller)
|
||||
.replace(/\[@[^\]]+\]\([^)]+\)\s*/g, "")
|
||||
// Remove "Thanks @username!" mentions at the beginning
|
||||
.replace(/^Thanks\s+@[^!]+!\s*-?\s*/g, "")
|
||||
// Remove "Thanks @username!" mentions anywhere
|
||||
.replace(/Thanks\s+@[^!]+!\s*-?\s*/g, "")
|
||||
// Remove "Thanks @username" mentions (without exclamation)
|
||||
.replace(/Thanks\s+@[^,)]+[,)]\s*/g, "")
|
||||
// Remove standalone contributor mentions like "(thanks @username!)"
|
||||
.replace(/\(thanks\s+@[^)]+\)\s*/g, "")
|
||||
// Remove leftover "Thanks !" patterns
|
||||
.replace(/Thanks\s*!\s*-?\s*/g, "")
|
||||
// Remove leading dashes and spaces
|
||||
.replace(/^[-\s]+/g, "")
|
||||
// Clean up multiple spaces
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML special characters
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
// Run the update if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
updateChangeNotes()
|
||||
}
|
||||
|
||||
export default updateChangeNotes
|
||||
5
jetbrains/plugin/settings.gradle.kts
Normal file
5
jetbrains/plugin/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
rootProject.name = "Roo Code"
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actions
|
||||
|
||||
/**
|
||||
* Constants for action names displayed in the UI.
|
||||
* These represent the text shown to users in menus and context options.
|
||||
*/
|
||||
object ActionNames {
|
||||
/** Action to explain selected code */
|
||||
const val EXPLAIN = "Explain Code"
|
||||
/** Action to fix issues in selected code */
|
||||
const val FIX = "Fix Code"
|
||||
/** Action to fix logical issues in selected code */
|
||||
const val FIX_LOGIC = "Fix Logic"
|
||||
/** Action to improve selected code */
|
||||
const val IMPROVE = "Improve Code"
|
||||
/** Action to add selected code to context */
|
||||
const val ADD_TO_CONTEXT = "Add to Context"
|
||||
/** Action to create a new task */
|
||||
const val NEW_TASK = "New Task"
|
||||
}
|
||||
|
||||
/**
|
||||
* Command identifiers used for internal command registration and execution.
|
||||
* These IDs are used to register commands with the IDE.
|
||||
*/
|
||||
object CommandIds {
|
||||
/** Command ID for explaining code */
|
||||
const val EXPLAIN = "roo-code.explainCode"
|
||||
/** Command ID for fixing code */
|
||||
const val FIX = "roo-code.fixCode"
|
||||
/** Command ID for improving code */
|
||||
const val IMPROVE = "roo-code.improveCode"
|
||||
/** Command ID for adding to context */
|
||||
const val ADD_TO_CONTEXT = "roo-code.addToContext"
|
||||
/** Command ID for creating a new task */
|
||||
const val NEW_TASK = "roo-code.newTask"
|
||||
}
|
||||
|
||||
/** Type alias for prompt type identifiers */
|
||||
typealias SupportPromptType = String
|
||||
/** Type alias for prompt parameters map */
|
||||
typealias PromptParams = Map<String, Any?>
|
||||
|
||||
/**
|
||||
* Data class representing a prompt configuration with a template string.
|
||||
* Templates contain placeholders that will be replaced with actual values.
|
||||
*/
|
||||
data class SupportPromptConfig(val template: String)
|
||||
|
||||
/**
|
||||
* Collection of predefined prompt configurations for different use cases.
|
||||
* Each configuration contains a template with placeholders for dynamic content.
|
||||
*/
|
||||
object SupportPromptConfigs {
|
||||
/**
|
||||
* Template for enhancing user prompts.
|
||||
* Instructs the AI to generate an improved version of the user's input.
|
||||
*/
|
||||
val ENHANCE = SupportPromptConfig(
|
||||
"""Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):
|
||||
|
||||
${'$'}{userInput}"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for explaining code.
|
||||
* Provides structure for code explanation requests with file path and line information.
|
||||
*/
|
||||
val EXPLAIN = SupportPromptConfig(
|
||||
"""Explain the following code from file path ${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
|
||||
${'$'}{userInput}
|
||||
|
||||
```
|
||||
${'$'}{selectedText}
|
||||
```
|
||||
|
||||
Please provide a clear and concise explanation of what this code does, including:
|
||||
1. The purpose and functionality
|
||||
2. Key components and their interactions
|
||||
3. Important patterns or techniques used"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for fixing code issues.
|
||||
* Includes diagnostic information and structured format for issue resolution.
|
||||
*/
|
||||
val FIX = SupportPromptConfig(
|
||||
"""Fix any issues in the following code from file path ${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
|
||||
${'$'}{diagnosticText}
|
||||
${'$'}{userInput}
|
||||
|
||||
```
|
||||
${'$'}{selectedText}
|
||||
```
|
||||
|
||||
Please:
|
||||
1. Address all detected problems listed above (if any)
|
||||
2. Identify any other potential bugs or issues
|
||||
3. Provide corrected code
|
||||
4. Explain what was fixed and why"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for improving code quality.
|
||||
* Focuses on readability, performance, best practices, and error handling.
|
||||
*/
|
||||
val IMPROVE = SupportPromptConfig(
|
||||
"""Improve the following code from file path ${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
|
||||
${'$'}{userInput}
|
||||
|
||||
```
|
||||
${'$'}{selectedText}
|
||||
```
|
||||
|
||||
Please suggest improvements for:
|
||||
1. Code readability and maintainability
|
||||
2. Performance optimization
|
||||
3. Best practices and patterns
|
||||
4. Error handling and edge cases
|
||||
|
||||
Provide the improved code along with explanations for each enhancement."""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for adding code to context.
|
||||
* Simple format that includes file path, line range, and selected code.
|
||||
*/
|
||||
val ADD_TO_CONTEXT = SupportPromptConfig(
|
||||
"""${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
|
||||
```
|
||||
${'$'}{selectedText}
|
||||
```"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for adding terminal output to context.
|
||||
* Includes user input and terminal content.
|
||||
*/
|
||||
val TERMINAL_ADD_TO_CONTEXT = SupportPromptConfig(
|
||||
"""${'$'}{userInput}
|
||||
Terminal output:
|
||||
```
|
||||
${'$'}{terminalContent}
|
||||
```"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for fixing terminal commands.
|
||||
* Structured format for identifying and resolving command issues.
|
||||
*/
|
||||
val TERMINAL_FIX = SupportPromptConfig(
|
||||
"""${'$'}{userInput}
|
||||
Fix this terminal command:
|
||||
```
|
||||
${'$'}{terminalContent}
|
||||
```
|
||||
|
||||
Please:
|
||||
1. Identify any issues in the command
|
||||
2. Provide the corrected command
|
||||
3. Explain what was fixed and why"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for explaining terminal commands.
|
||||
* Provides structure for command explanation with focus on functionality and behavior.
|
||||
*/
|
||||
val TERMINAL_EXPLAIN = SupportPromptConfig(
|
||||
"""${'$'}{userInput}
|
||||
Explain this terminal command:
|
||||
```
|
||||
${'$'}{terminalContent}
|
||||
```
|
||||
|
||||
Please provide:
|
||||
1. What the command does
|
||||
2. Explanation of each part/flag
|
||||
3. Expected output and behavior"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Template for creating a new task.
|
||||
* Simple format that passes through user input directly.
|
||||
*/
|
||||
val NEW_TASK = SupportPromptConfig(
|
||||
"""${'$'}{userInput}"""
|
||||
)
|
||||
|
||||
/**
|
||||
* Map of all available prompt configurations indexed by their type identifiers.
|
||||
* Used for lookup when creating prompts.
|
||||
*/
|
||||
val configs = mapOf(
|
||||
"ENHANCE" to ENHANCE,
|
||||
"EXPLAIN" to EXPLAIN,
|
||||
"FIX" to FIX,
|
||||
"IMPROVE" to IMPROVE,
|
||||
"ADD_TO_CONTEXT" to ADD_TO_CONTEXT,
|
||||
"TERMINAL_ADD_TO_CONTEXT" to TERMINAL_ADD_TO_CONTEXT,
|
||||
"TERMINAL_FIX" to TERMINAL_FIX,
|
||||
"TERMINAL_EXPLAIN" to TERMINAL_EXPLAIN,
|
||||
"NEW_TASK" to NEW_TASK
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility object for working with support prompts.
|
||||
* Provides methods for creating and customizing prompts based on templates.
|
||||
*/
|
||||
object SupportPrompt {
|
||||
/**
|
||||
* Generates formatted diagnostic text from a list of diagnostic items.
|
||||
*
|
||||
* @param diagnostics List of diagnostic items containing source, message, and code
|
||||
* @return Formatted string of diagnostic messages or empty string if no diagnostics
|
||||
*/
|
||||
private fun generateDiagnosticText(diagnostics: List<Map<String, Any?>>?): String {
|
||||
if (diagnostics.isNullOrEmpty()) return ""
|
||||
return "\nCurrent problems detected:\n" + diagnostics.joinToString("\n") { d ->
|
||||
val source = d["source"] as? String ?: "Error"
|
||||
val message = d["message"] as? String ?: ""
|
||||
val code = d["code"] as? String
|
||||
"- [$source] $message${code?.let { " ($it)" } ?: ""}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a prompt by replacing placeholders in a template with actual values.
|
||||
*
|
||||
* @param template The prompt template with placeholders
|
||||
* @param params Map of parameter values to replace placeholders
|
||||
* @return The processed prompt with placeholders replaced by actual values
|
||||
*/
|
||||
private fun createPrompt(template: String, params: PromptParams): String {
|
||||
val pattern = Regex("""\$\{(.*?)}""")
|
||||
return pattern.replace(template) { matchResult ->
|
||||
val key = matchResult.groupValues[1]
|
||||
if (key == "diagnosticText") {
|
||||
generateDiagnosticText(params["diagnostics"] as? List<Map<String, Any?>>)
|
||||
} else if (params.containsKey(key)) {
|
||||
// Ensure the value is treated as a string for replacement
|
||||
val value = params[key]
|
||||
when (value) {
|
||||
is String -> value
|
||||
else -> {
|
||||
// Convert non-string values to string for replacement
|
||||
value?.toString() ?: ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the placeholder key is not in params, replace with empty string
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template for a specific prompt type, with optional custom overrides.
|
||||
*
|
||||
* @param customSupportPrompts Optional map of custom prompt templates
|
||||
* @param type The type of prompt to retrieve
|
||||
* @return The template string for the specified prompt type
|
||||
*/
|
||||
fun get(customSupportPrompts: Map<String, String>?, type: SupportPromptType): String {
|
||||
return customSupportPrompts?.get(type) ?: SupportPromptConfigs.configs[type]?.template ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete prompt by getting the template and replacing placeholders.
|
||||
*
|
||||
* @param type The type of prompt to create
|
||||
* @param params Parameters to substitute into the template
|
||||
* @param customSupportPrompts Optional custom prompt templates
|
||||
* @return The final prompt with all placeholders replaced
|
||||
*/
|
||||
fun create(type: SupportPromptType, params: PromptParams, customSupportPrompts: Map<String, String>? = null): String {
|
||||
val template = get(customSupportPrompts, type)
|
||||
return createPrompt(template, params)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actions
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import ai.roocode.jetbrains.webview.WebViewManager
|
||||
|
||||
/**
|
||||
* Code action provider, similar to VSCode's CodeActionProvider.
|
||||
* Provides functionality for creating and managing code-related actions.
|
||||
*/
|
||||
class CodeActionProvider {
|
||||
|
||||
/**
|
||||
* Creates a single code action with the specified title and command.
|
||||
*
|
||||
* @param title The display title for the action
|
||||
* @param command The command identifier to execute when action is triggered
|
||||
* @return An AnAction instance that can be registered with the IDE
|
||||
*/
|
||||
private fun createAction(
|
||||
title: String,
|
||||
command: String
|
||||
): AnAction {
|
||||
return object : AnAction(title) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
|
||||
val file = e.dataContext.getData(CommonDataKeys.VIRTUAL_FILE) ?: return
|
||||
|
||||
// Get current parameters when the action is clicked
|
||||
val effectiveRange = getEffectiveRange(editor)
|
||||
if (effectiveRange == null) return
|
||||
|
||||
val args = mutableMapOf<String, Any?>()
|
||||
args["filePath"] = file.path
|
||||
args["selectedText"] = effectiveRange.text
|
||||
args["startLine"] = effectiveRange.startLine + 1
|
||||
args["endLine"] = effectiveRange.endLine + 1
|
||||
|
||||
handleCodeAction(command, title, args, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pair of actions (new task version and current task version).
|
||||
*
|
||||
* @param baseTitle The base title for the actions
|
||||
* @param baseCommand The base command identifier
|
||||
* @return List of AnAction instances
|
||||
*/
|
||||
private fun createActionPair(
|
||||
baseTitle: String,
|
||||
baseCommand: String
|
||||
): List<AnAction> {
|
||||
return listOf(
|
||||
createAction("$baseTitle in Current Task", "${baseCommand}InCurrentTask")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective range and text from the current editor selection.
|
||||
*
|
||||
* @param editor The current editor instance
|
||||
* @return EffectiveRange object containing selected text and line numbers, or null if no selection
|
||||
*/
|
||||
private fun getEffectiveRange(editor: Editor): EffectiveRange? {
|
||||
val document = editor.document
|
||||
val selectionModel = editor.selectionModel
|
||||
|
||||
return if (selectionModel.hasSelection()) {
|
||||
val selectedText = selectionModel.selectedText ?: ""
|
||||
val startLine = document.getLineNumber(selectionModel.selectionStart)
|
||||
val endLine = document.getLineNumber(selectionModel.selectionEnd)
|
||||
EffectiveRange(selectedText, startLine, endLine)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of code actions for the given action event.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
* @return List of available code actions
|
||||
*/
|
||||
fun provideCodeActions(e: AnActionEvent): List<AnAction> {
|
||||
val actions = mutableListOf<AnAction>()
|
||||
|
||||
// Add to context action
|
||||
actions.add(
|
||||
createAction(
|
||||
ActionNames.ADD_TO_CONTEXT,
|
||||
CommandIds.ADD_TO_CONTEXT
|
||||
)
|
||||
)
|
||||
|
||||
// Explain code action pair
|
||||
actions.addAll(
|
||||
createActionPair(
|
||||
ActionNames.EXPLAIN,
|
||||
CommandIds.EXPLAIN
|
||||
)
|
||||
)
|
||||
|
||||
// Fix code action pair (logic fix)
|
||||
actions.addAll(
|
||||
createActionPair(
|
||||
ActionNames.FIX_LOGIC,
|
||||
CommandIds.FIX
|
||||
)
|
||||
)
|
||||
|
||||
// Improve code action pair
|
||||
actions.addAll(
|
||||
createActionPair(
|
||||
ActionNames.IMPROVE,
|
||||
CommandIds.IMPROVE
|
||||
)
|
||||
)
|
||||
|
||||
return actions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing an effective range of selected text.
|
||||
* Contains the selected text and its start/end line numbers.
|
||||
*
|
||||
* @property text The selected text content
|
||||
* @property startLine The starting line number (0-based)
|
||||
* @property endLine The ending line number (0-based)
|
||||
*/
|
||||
data class EffectiveRange(
|
||||
val text: String,
|
||||
val startLine: Int,
|
||||
val endLine: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers a code action with the specified parameters.
|
||||
*
|
||||
* @param command The command identifier
|
||||
* @param promptType The type of prompt to use
|
||||
* @param inputPrompt Optional prompt text for user input dialog
|
||||
* @param inputPlaceholder Optional placeholder text for input field
|
||||
* @return An AnAction instance that can be registered with the IDE
|
||||
*/
|
||||
fun registerCodeAction(
|
||||
command: String,
|
||||
promptType: String,
|
||||
inputPrompt: String? = null,
|
||||
inputPlaceholder: String? = null
|
||||
) : AnAction {
|
||||
return object : AnAction(command) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
|
||||
|
||||
var userInput: String? = null
|
||||
if (inputPrompt != null) {
|
||||
userInput = Messages.showInputDialog(
|
||||
project,
|
||||
inputPrompt,
|
||||
"Kilo Code",
|
||||
null,
|
||||
inputPlaceholder,
|
||||
null
|
||||
)
|
||||
if (userInput == null) return // Cancelled
|
||||
}
|
||||
|
||||
// Get selected content, line numbers, etc.
|
||||
val document = editor.document
|
||||
val selectionModel = editor.selectionModel
|
||||
val selectedText = selectionModel.selectedText ?: ""
|
||||
val startLine = if (selectionModel.hasSelection()) document.getLineNumber(selectionModel.selectionStart) else null
|
||||
val endLine = if (selectionModel.hasSelection()) document.getLineNumber(selectionModel.selectionEnd) else null
|
||||
val file = e.getData(CommonDataKeys.VIRTUAL_FILE)
|
||||
val filePath = file?.path ?: ""
|
||||
|
||||
val params = mutableMapOf<String, Any?>(
|
||||
"filePath" to filePath,
|
||||
"selectedText" to selectedText
|
||||
)
|
||||
if (startLine != null) params["startLine"] = (startLine + 1).toString()
|
||||
if (endLine != null) params["endLine"] = (endLine + 1).toString()
|
||||
if (!userInput.isNullOrEmpty()) params["userInput"] = userInput
|
||||
|
||||
handleCodeAction(command, promptType, params, e.project)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Registers a pair of code actions with the specified parameters.
|
||||
*
|
||||
* @param baseCommand The base command identifier
|
||||
* @param inputPrompt Optional prompt text for user input dialog
|
||||
* @param inputPlaceholder Optional placeholder text for input field
|
||||
* @return An AnAction instance for the new task version
|
||||
*/
|
||||
fun registerCodeActionPair(
|
||||
baseCommand: String,
|
||||
inputPrompt: String? = null,
|
||||
inputPlaceholder: String? = null
|
||||
) : AnAction {
|
||||
// New task version
|
||||
return registerCodeAction(baseCommand, baseCommand, inputPrompt, inputPlaceholder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Core logic for handling code actions.
|
||||
* Processes different types of commands and sends appropriate messages to the webview.
|
||||
*
|
||||
* @param command The command identifier
|
||||
* @param promptType The type of prompt to use
|
||||
* @param params Parameters for the action (can be Map or List)
|
||||
* @param project The current project
|
||||
*/
|
||||
fun handleCodeAction(command: String, promptType: String, params: Any, project: Project?) {
|
||||
val latestWebView = project?.getService(WebViewManager::class.java)?.getLatestWebView()
|
||||
if (latestWebView == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create message content based on command type
|
||||
val messageContent = when {
|
||||
// Add to context command
|
||||
command.contains("addToContext") -> {
|
||||
val promptParams = if (params is Map<*, *>) params as Map<String, Any?> else emptyMap()
|
||||
mapOf(
|
||||
"type" to "invoke",
|
||||
"invoke" to "setChatBoxMessage",
|
||||
"text" to SupportPrompt.create("ADD_TO_CONTEXT", promptParams)
|
||||
)
|
||||
}
|
||||
// Command executed in current task
|
||||
command.endsWith("InCurrentTask") -> {
|
||||
val promptParams = if (params is Map<*, *>) params as Map<String, Any?> else emptyMap()
|
||||
val basePromptType = when {
|
||||
command.contains("explain") -> "EXPLAIN"
|
||||
command.contains("fix") -> "FIX"
|
||||
command.contains("improve") -> "IMPROVE"
|
||||
else -> promptType
|
||||
}
|
||||
mapOf(
|
||||
"type" to "invoke",
|
||||
"invoke" to "sendMessage",
|
||||
"text" to SupportPrompt.create(basePromptType, promptParams)
|
||||
)
|
||||
}
|
||||
// Command executed in new task
|
||||
else -> {
|
||||
val promptParams = if (params is List<*>) {
|
||||
// Process parameter list from createAction
|
||||
val argsList = params as List<Any>
|
||||
if (argsList.size >= 4) {
|
||||
mapOf(
|
||||
"filePath" to argsList[0],
|
||||
"selectedText" to argsList[1],
|
||||
"startLine" to argsList[2],
|
||||
"endLine" to argsList[3]
|
||||
)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
} else if (params is Map<*, *>) {
|
||||
params as Map<String, Any?>
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val basePromptType = when {
|
||||
command.contains("explain") -> "EXPLAIN"
|
||||
command.contains("fix") -> "FIX"
|
||||
command.contains("improve") -> "IMPROVE"
|
||||
else -> promptType
|
||||
}
|
||||
|
||||
mapOf(
|
||||
"type" to "invoke",
|
||||
"invoke" to "initClineWithTask",
|
||||
"text" to SupportPrompt.create(basePromptType, promptParams)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to JSON and send
|
||||
val messageJson = com.google.gson.Gson().toJson(messageContent)
|
||||
latestWebView.postMessageToWebView(messageJson)
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actions
|
||||
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
|
||||
/**
|
||||
* Right-click menu code action group, similar to VSCode's code action provider.
|
||||
* This class manages the dynamic actions that appear in the context menu when text is selected.
|
||||
* Implements DumbAware to ensure the action works during indexing, and ActionUpdateThreadAware
|
||||
* to specify which thread should handle action updates.
|
||||
*/
|
||||
class RightClickChatActionGroup : DefaultActionGroup(), DumbAware, ActionUpdateThreadAware {
|
||||
|
||||
/**
|
||||
* Provider that supplies the actual code actions to be displayed in the menu.
|
||||
*/
|
||||
private val codeActionProvider = CodeActionProvider()
|
||||
|
||||
/**
|
||||
* Updates the action group based on the current context.
|
||||
* This method is called each time the menu needs to be displayed.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun update(e: AnActionEvent) {
|
||||
removeAll()
|
||||
|
||||
// Check if there is an editor and selected text
|
||||
val editor = e.getData(CommonDataKeys.EDITOR)
|
||||
val hasSelection = editor?.selectionModel?.hasSelection() == true
|
||||
|
||||
if (hasSelection) {
|
||||
loadDynamicActions(e)
|
||||
}
|
||||
|
||||
// Set the visibility of the action group
|
||||
e.presentation.isVisible = hasSelection
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads dynamic actions into this action group based on the current context.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
private fun loadDynamicActions(e: AnActionEvent) {
|
||||
// Use actions provided by CodeActionProvider
|
||||
val actions = codeActionProvider.provideCodeActions(e)
|
||||
actions.forEach { action ->
|
||||
add(action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which thread should be used for updating this action.
|
||||
* EDT (Event Dispatch Thread) is used for UI-related operations.
|
||||
*
|
||||
* @return The thread to use for action updates
|
||||
*/
|
||||
override fun getActionUpdateThread(): ActionUpdateThread {
|
||||
return ActionUpdateThread.EDT
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actions
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ai.roocode.jetbrains.core.PluginContext
|
||||
import ai.roocode.jetbrains.core.ServiceProxyRegistry
|
||||
/**
|
||||
* Executes a VSCode command with the given command ID.
|
||||
* This function uses the RPC protocol to communicate with the extension host.
|
||||
*
|
||||
* @param commandId The identifier of the command to execute
|
||||
* @param project The current project context
|
||||
*/
|
||||
fun executeCommand(commandId: String, project: Project?) {
|
||||
val proxy =
|
||||
project?.getService(PluginContext::class.java)?.getRPCProtocol()?.getProxy(ServiceProxyRegistry.ExtHostContext.ExtHostCommands)
|
||||
proxy?.executeContributedCommand(commandId, emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that handles clicks on the Plus button in the UI.
|
||||
* Executes the corresponding VSCode command when triggered.
|
||||
*/
|
||||
class PlusButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(PlusButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.plusButtonClicked"
|
||||
|
||||
/**
|
||||
* Performs the action when the Plus button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("Plus button clicked")
|
||||
executeCommand(commandId,e.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that handles clicks on the Prompts button in the UI.
|
||||
* Executes the corresponding VSCode command when triggered.
|
||||
*/
|
||||
class PromptsButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(PromptsButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.promptsButtonClicked"
|
||||
|
||||
/**
|
||||
* Performs the action when the Prompts button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("Prompts button clicked")
|
||||
executeCommand(commandId, e.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that handles clicks on the MCP button in the UI.
|
||||
* Executes the corresponding VSCode command when triggered.
|
||||
*/
|
||||
class MCPButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(MCPButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.mcpButtonClicked"
|
||||
|
||||
/**
|
||||
* Performs the action when the MCP button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("MCP button clicked")
|
||||
executeCommand(commandId, e.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that handles clicks on the History button in the UI.
|
||||
* Executes the corresponding VSCode command when triggered.
|
||||
*/
|
||||
class HistoryButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(HistoryButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.historyButtonClicked"
|
||||
|
||||
/**
|
||||
* Performs the action when the History button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("History button clicked")
|
||||
executeCommand(commandId, e.project)
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(ProfileButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.profileButtonClicked"
|
||||
/**
|
||||
* Performs the action when the Profile button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("Profile button clicked")
|
||||
executeCommand(commandId, e.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that handles clicks on the Settings button in the UI.
|
||||
* Executes the corresponding VSCode command when triggered.
|
||||
*/
|
||||
class SettingsButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(SettingsButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.settingsButtonClicked"
|
||||
|
||||
/**
|
||||
* Performs the action when the Settings button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("Settings button clicked")
|
||||
executeCommand(commandId, e.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that handles clicks on the Marketplace button in the UI.
|
||||
* Executes the corresponding VSCode command when triggered.
|
||||
*/
|
||||
class MarketplaceButtonClickAction : AnAction() {
|
||||
private val logger: Logger = Logger.getInstance(MarketplaceButtonClickAction::class.java)
|
||||
private val commandId: String = "roo-code.marketplaceButtonClicked"
|
||||
|
||||
/**
|
||||
* Performs the action when the Marketplace button is clicked.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
logger.info("Marketplace button clicked")
|
||||
executeCommand(commandId, e.project)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that opens developer tools for the WebView.
|
||||
* Takes a function that provides the current WebView instance.
|
||||
*
|
||||
* @property getWebViewInstance Function that returns the current WebView instance or null if not available
|
||||
*/
|
||||
class OpenDevToolsAction(private val getWebViewInstance: () -> ai.roocode.jetbrains.webview.WebViewInstance?) : AnAction("Open Developer Tools") {
|
||||
private val logger: Logger = Logger.getInstance(OpenDevToolsAction::class.java)
|
||||
|
||||
/**
|
||||
* Performs the action to open developer tools for the WebView.
|
||||
*
|
||||
* @param e The action event containing context information
|
||||
*/
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val webView = getWebViewInstance()
|
||||
if (webView != null) {
|
||||
webView.openDevTools()
|
||||
} else {
|
||||
logger.warn("No WebView instance available, cannot open developer tools")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import ai.roocode.jetbrains.editor.EditorAndDocManager
|
||||
import ai.roocode.jetbrains.editor.EditorHolder
|
||||
import ai.roocode.jetbrains.editor.WorkspaceEdit
|
||||
import ai.roocode.jetbrains.ipc.proxy.SerializableObjectWithBuffers
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
/**
|
||||
* Interface for handling bulk edits in the main thread.
|
||||
* Provides functionality to apply workspace edits that may include multiple file and text changes.
|
||||
*/
|
||||
interface MainThreadBulkEditsShape {
|
||||
/**
|
||||
* Attempts to apply a workspace edit.
|
||||
*
|
||||
* @param workspaceEditDto The workspace edit data transfer object
|
||||
* @param undoRedoGroupId Optional ID for grouping undo/redo operations
|
||||
* @param respectAutoSaveConfig Whether to respect auto-save configuration
|
||||
* @return True if all edits were applied successfully, false otherwise
|
||||
*/
|
||||
suspend fun tryApplyWorkspaceEdit(workspaceEditDto: SerializableObjectWithBuffers<Any>, undoRedoGroupId: Int?, respectAutoSaveConfig: Boolean?): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of MainThreadBulkEditsShape that handles bulk edits in the main thread.
|
||||
* Processes workspace edits including file operations (create, delete, rename) and text edits.
|
||||
*
|
||||
* @property project The current project context
|
||||
*/
|
||||
class MainThreadBulkEdits(val project: Project) : MainThreadBulkEditsShape {
|
||||
val logger = Logger.getInstance(MainThreadBulkEditsShape::class.java)
|
||||
|
||||
/**
|
||||
* Attempts to apply a workspace edit by processing file operations and text edits.
|
||||
*
|
||||
* @param workspaceEditDto The workspace edit data transfer object
|
||||
* @param undoRedoGroupId Optional ID for grouping undo/redo operations
|
||||
* @param respectAutoSaveConfig Whether to respect auto-save configuration
|
||||
* @return True if all edits were applied successfully, false otherwise
|
||||
*/
|
||||
override suspend fun tryApplyWorkspaceEdit(workspaceEditDto: SerializableObjectWithBuffers<Any>, undoRedoGroupId: Int?, respectAutoSaveConfig: Boolean?): Boolean {
|
||||
val json = workspaceEditDto.value as String
|
||||
logger.info("[Bulk Edit] Starting process: $json")
|
||||
val cto = WorkspaceEdit.from(json)
|
||||
var allSuccess = true
|
||||
|
||||
// Process file edits - using background thread to avoid EDT violations
|
||||
cto.files.forEach { fileEdit ->
|
||||
if (fileEdit.oldResource != null && fileEdit.newResource != null) {
|
||||
val oldResource = File(fileEdit.oldResource.path)
|
||||
val newResource = File(fileEdit.newResource.path)
|
||||
try {
|
||||
Files.move(oldResource.toPath(), newResource.toPath())
|
||||
// Move VFS refresh operations to background thread
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val vfs = LocalFileSystem.getInstance()
|
||||
vfs.refreshIoFiles(listOf(oldResource, newResource))
|
||||
}
|
||||
logger.info("[Bulk Edit] Renamed file: ${oldResource.path} -> ${newResource.path}")
|
||||
} catch (e: Exception) {
|
||||
logger.error("[Bulk Edit] Failed to rename file: ${oldResource.path} -> ${newResource.path}", e)
|
||||
allSuccess = false
|
||||
}
|
||||
} else if (fileEdit.oldResource != null) {
|
||||
val oldResource = File(fileEdit.oldResource.path)
|
||||
try {
|
||||
oldResource.delete()
|
||||
// Move VFS refresh operations to background thread
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val vfs = LocalFileSystem.getInstance()
|
||||
vfs.refreshIoFiles(listOf(oldResource.parentFile))
|
||||
}
|
||||
logger.info("[Bulk Edit] Deleted file: ${oldResource.path}")
|
||||
} catch (e: Exception) {
|
||||
logger.error("[Bulk Edit] Failed to delete file: ${oldResource.path}", e)
|
||||
allSuccess = false
|
||||
}
|
||||
} else if (fileEdit.newResource != null) {
|
||||
val newResource = File(fileEdit.newResource.path)
|
||||
try {
|
||||
val parentDir = newResource.parentFile
|
||||
if (!parentDir.exists()) {
|
||||
parentDir.mkdirs()
|
||||
}
|
||||
if (fileEdit.options?.contents != null) {
|
||||
Files.write(newResource.toPath(), fileEdit.options!!.contents!!.toByteArray(Charsets.UTF_8))
|
||||
} else {
|
||||
newResource.createNewFile()
|
||||
}
|
||||
// Move VFS refresh operations to background thread
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val vfs = LocalFileSystem.getInstance()
|
||||
vfs.refreshIoFiles(listOf(newResource))
|
||||
}
|
||||
logger.info("[Bulk Edit] Created file: ${newResource.path}")
|
||||
} catch (e: Exception) {
|
||||
logger.error("[Bulk Edit] Failed to create file: ${newResource.path}", e)
|
||||
allSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process text edits
|
||||
cto.texts.forEach { textEdit ->
|
||||
logger.info("[Bulk Edit] Processing text edit: ${textEdit.resource.path}")
|
||||
if (textEdit.resource.scheme != "file") {
|
||||
logger.error("[Bulk Edit] Non-file resources not supported: ${textEdit.resource.path}")
|
||||
allSuccess = false
|
||||
return@forEach
|
||||
}
|
||||
|
||||
var handle:EditorHolder? = null;
|
||||
try {
|
||||
handle = project.getService(EditorAndDocManager::class.java).getEditorHandleByUri(textEdit.resource,true)
|
||||
if (handle == null) {
|
||||
handle = project.getService(EditorAndDocManager::class.java).sync2ExtHost(textEdit.resource,true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.info("[Bulk Edit] Failed to get editor handle: ${textEdit.resource.path}", e)
|
||||
}
|
||||
|
||||
if (handle == null) {
|
||||
logger.info("[Bulk Edit] Editor handle not found: ${textEdit.resource.path}")
|
||||
allSuccess = false
|
||||
return@forEach
|
||||
}
|
||||
|
||||
try {
|
||||
val result = handle.applyEdit(textEdit)
|
||||
if (!result) {
|
||||
logger.info("[Bulk Edit] Failed to apply edit: ${textEdit.resource.path}")
|
||||
allSuccess = false
|
||||
} else {
|
||||
logger.info("[Bulk Edit] Successfully updated file: ${textEdit.resource.path}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("[Bulk Edit] Exception applying edit: ${textEdit.resource.path}", e)
|
||||
allSuccess = false
|
||||
}
|
||||
}
|
||||
return allSuccess
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
|
||||
/**
|
||||
* Main thread clipboard interface.
|
||||
* Corresponds to the MainThreadClipboardShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadClipboardShape : Disposable {
|
||||
/**
|
||||
* Reads text from the clipboard.
|
||||
* @return The string from the clipboard, or null if no text is available
|
||||
*/
|
||||
fun readText(): String?
|
||||
|
||||
/**
|
||||
* Writes text to the clipboard.
|
||||
* @param value The string to write to the clipboard
|
||||
*/
|
||||
fun writeText(value: String?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the MainThreadClipboardShape interface.
|
||||
* Provides functionality to read from and write to the system clipboard.
|
||||
*/
|
||||
class MainThreadClipboard : MainThreadClipboardShape {
|
||||
private val logger = Logger.getInstance(MainThreadClipboardShape::class.java)
|
||||
|
||||
/**
|
||||
* Reads text from the system clipboard.
|
||||
*
|
||||
* @return The string from the clipboard, or null if no text is available or an error occurs
|
||||
*/
|
||||
override fun readText(): String? {
|
||||
logger.info("Reading clipboard text")
|
||||
return try {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val data = clipboard.getContents(null)
|
||||
if (data != null && data.isDataFlavorSupported(DataFlavor.stringFlavor)) {
|
||||
data.getTransferData(DataFlavor.stringFlavor) as? String
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to read clipboard", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes text to the system clipboard.
|
||||
*
|
||||
* @param value The string to write to the clipboard
|
||||
*/
|
||||
override fun writeText(value: String?) {
|
||||
value?.let {
|
||||
logger.info("Writing clipboard text: $value")
|
||||
try {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val selection = StringSelection(value)
|
||||
clipboard.setContents(selection, selection)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to write to clipboard", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources used by this clipboard handler.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Releasing resources: MainThreadClipboard")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ai.roocode.jetbrains.commands.CommandRegistry
|
||||
import ai.roocode.jetbrains.commands.ICommand
|
||||
import ai.roocode.jetbrains.editor.registerOpenEditorAPICommands
|
||||
import ai.roocode.jetbrains.terminal.registerTerminalAPICommands
|
||||
import ai.roocode.jetbrains.util.doInvokeMethod
|
||||
import kotlin.reflect.full.functions
|
||||
|
||||
/**
|
||||
* Main thread commands interface.
|
||||
* Corresponds to the MainThreadCommandsShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadCommandsShape : Disposable {
|
||||
/**
|
||||
* Registers a command.
|
||||
* @param id The command identifier
|
||||
*/
|
||||
fun registerCommand(id: String)
|
||||
|
||||
/**
|
||||
* Unregisters a command.
|
||||
* @param id The command identifier
|
||||
*/
|
||||
fun unregisterCommand(id: String)
|
||||
|
||||
/**
|
||||
* Fires a command activation event.
|
||||
* @param id The command identifier
|
||||
*/
|
||||
fun fireCommandActivationEvent(id: String)
|
||||
|
||||
/**
|
||||
* Executes a command.
|
||||
* @param id The command identifier
|
||||
* @param args List of arguments for the command
|
||||
* @return The execution result
|
||||
*/
|
||||
suspend fun executeCommand(id: String, args: List<Any?>): Any?
|
||||
|
||||
/**
|
||||
* Gets all registered commands.
|
||||
* @return List of command identifiers
|
||||
*/
|
||||
fun getCommands(): List<String>
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of MainThreadCommandsShape that handles command registration and execution.
|
||||
* Manages a registry of commands and provides methods to interact with them.
|
||||
*
|
||||
* @property project The current project context
|
||||
*/
|
||||
class MainThreadCommands(val project: Project) : MainThreadCommandsShape {
|
||||
private val registry = CommandRegistry(project)
|
||||
private val logger = Logger.getInstance(MainThreadCommandsShape::class.java)
|
||||
|
||||
/**
|
||||
* Initializes the command registry with default commands.
|
||||
*/
|
||||
init {
|
||||
registerOpenEditorAPICommands(project,registry);
|
||||
registerTerminalAPICommands(project,registry);
|
||||
//TODO other commands
|
||||
}
|
||||
/**
|
||||
* Registers a command with the given identifier.
|
||||
*
|
||||
* @param id The command identifier
|
||||
*/
|
||||
override fun registerCommand(id: String) {
|
||||
logger.info("Registering command: $id")
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a command with the given identifier.
|
||||
*
|
||||
* @param id The command identifier
|
||||
*/
|
||||
override fun unregisterCommand(id: String) {
|
||||
logger.info("Unregistering command: $id")
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires an activation event for the specified command.
|
||||
*
|
||||
* @param id The command identifier
|
||||
*/
|
||||
override fun fireCommandActivationEvent(id: String) {
|
||||
logger.info("Firing command activation event: $id")
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command with the given identifier and arguments.
|
||||
*
|
||||
* @param id The command identifier
|
||||
* @param args List of arguments for the command
|
||||
* @return The execution result
|
||||
*/
|
||||
override suspend fun executeCommand(id: String, args: List<Any?>): Any? {
|
||||
logger.info("Executing command: $id ")
|
||||
registry.getCommand(id)?.let { cmd->
|
||||
runCmd(cmd,args)
|
||||
}?: run {
|
||||
logger.warn("Command not found: $id")
|
||||
}
|
||||
return Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all registered command identifiers.
|
||||
*
|
||||
* @return List of command identifiers
|
||||
*/
|
||||
override fun getCommands(): List<String> {
|
||||
logger.info("Getting all commands")
|
||||
return registry.getCommands().keys.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources used by this command handler.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Releasing resources: MainThreadCommands")
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command with the given arguments.
|
||||
* Finds the appropriate method on the command handler and invokes it.
|
||||
*
|
||||
* @param cmd The command to run
|
||||
* @param args List of arguments for the command
|
||||
*/
|
||||
private suspend fun runCmd(cmd: ICommand, args: List<Any?>) {
|
||||
val handler = cmd.handler();
|
||||
val method = try {
|
||||
// handler.javaClass.methods.first { it.name == cmd.getMethod()}
|
||||
handler::class.functions.first{ it.name == cmd.getMethod()}
|
||||
}catch (e: Exception){
|
||||
logger.error("Command method not found: ${cmd.getMethod()}")
|
||||
return
|
||||
}
|
||||
doInvokeMethod(method,args,handler)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import ai.roocode.jetbrains.core.PluginContext
|
||||
import ai.roocode.jetbrains.util.URI
|
||||
import ai.roocode.jetbrains.util.URIComponents
|
||||
|
||||
/**
|
||||
* Enum for configuration targets.
|
||||
* Corresponds to the ConfigurationTarget enum in VSCode.
|
||||
* Defines the different scopes where configuration can be applied.
|
||||
*/
|
||||
enum class ConfigurationTarget(val value: Int) {
|
||||
/** Application-level configuration, applies globally to the entire IDE */
|
||||
APPLICATION(1),
|
||||
/** User-level configuration, applies to the current user across all projects */
|
||||
USER(2),
|
||||
/** Local user configuration, specific to the local machine */
|
||||
USER_LOCAL(3),
|
||||
/** Remote user configuration, for remote development scenarios */
|
||||
USER_REMOTE(4),
|
||||
/** Workspace-level configuration, applies to the current project workspace */
|
||||
WORKSPACE(5),
|
||||
/** Workspace folder-level configuration, applies to specific folders within a workspace */
|
||||
WORKSPACE_FOLDER(6),
|
||||
/** Default configuration target when no specific target is provided */
|
||||
DEFAULT(7),
|
||||
/** Memory-only configuration, temporary and not persisted */
|
||||
MEMORY(8);
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a ConfigurationTarget from its integer value.
|
||||
* @param value The integer value representing the configuration target
|
||||
* @return The corresponding ConfigurationTarget enum value, or null if not found
|
||||
*/
|
||||
fun fromValue(value: Int?): ConfigurationTarget? {
|
||||
return values().find { it.value == value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ConfigurationTarget enum to its string representation.
|
||||
* @param target The configuration target to convert
|
||||
* @return The string name of the configuration target
|
||||
*/
|
||||
fun toString(target: ConfigurationTarget): String {
|
||||
return when(target) {
|
||||
APPLICATION -> "APPLICATION"
|
||||
USER -> "USER"
|
||||
USER_LOCAL -> "USER_LOCAL"
|
||||
USER_REMOTE -> "USER_REMOTE"
|
||||
WORKSPACE -> "WORKSPACE"
|
||||
WORKSPACE_FOLDER -> "WORKSPACE_FOLDER"
|
||||
DEFAULT -> "DEFAULT"
|
||||
MEMORY -> "MEMORY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for configuration overrides.
|
||||
* Corresponds to the IConfigurationOverrides interface in VSCode.
|
||||
* Used to provide context-specific configuration overrides.
|
||||
*/
|
||||
data class ConfigurationOverrides(
|
||||
/** Optional identifier for overriding configuration values, typically used for language-specific settings */
|
||||
val overrideIdentifier: String? = null,
|
||||
/** Optional URI specifying the resource context for the configuration override */
|
||||
val resource: URI? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Main thread configuration interface.
|
||||
* Corresponds to the MainThreadConfigurationShape interface in VSCode.
|
||||
* Defines the contract for configuration management operations that can be performed
|
||||
* from the main thread of the IDE.
|
||||
*/
|
||||
interface MainThreadConfigurationShape : Disposable {
|
||||
/**
|
||||
* Updates a configuration option with the specified parameters.
|
||||
* @param target Configuration target scope (application, user, workspace, etc.)
|
||||
* @param key Configuration key path (e.g., "editor.fontSize")
|
||||
* @param value Configuration value to set, can be null to unset
|
||||
* @param overrides Optional configuration overrides for specific contexts
|
||||
* @param scopeToLanguage Whether to scope this configuration to a specific language
|
||||
*/
|
||||
fun updateConfigurationOption(
|
||||
target: Int,
|
||||
key: String,
|
||||
value: Any?,
|
||||
overrides: Map<String, Any>?,
|
||||
scopeToLanguage: Boolean?
|
||||
)
|
||||
|
||||
/**
|
||||
* Removes a configuration option from the specified target scope.
|
||||
* @param target Configuration target scope from which to remove the setting
|
||||
* @param key Configuration key path to remove
|
||||
* @param overrides Optional configuration overrides to consider during removal
|
||||
* @param scopeToLanguage Whether the configuration was scoped to a specific language
|
||||
*/
|
||||
fun removeConfigurationOption(
|
||||
target: Int,
|
||||
key: String,
|
||||
overrides: Map<String, Any>?,
|
||||
scopeToLanguage: Boolean?
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the main thread configuration interface.
|
||||
* Provides concrete implementation for managing IDE configuration settings
|
||||
* across different scopes and contexts.
|
||||
*/
|
||||
class MainThreadConfiguration : MainThreadConfigurationShape {
|
||||
private val logger = Logger.getInstance(MainThreadConfiguration::class.java)
|
||||
|
||||
/**
|
||||
* Updates a configuration option in the specified target scope.
|
||||
* Handles the conversion of parameters and delegates to the appropriate
|
||||
* storage mechanism based on the configuration target.
|
||||
*/
|
||||
override fun updateConfigurationOption(
|
||||
target: Int,
|
||||
key: String,
|
||||
value: Any?,
|
||||
overrides: Map<String, Any>?,
|
||||
scopeToLanguage: Boolean?
|
||||
) {
|
||||
// Convert parameter types from raw values to type-safe objects
|
||||
val configTarget = ConfigurationTarget.fromValue(target)
|
||||
val configOverrides = convertToConfigurationOverrides(overrides)
|
||||
|
||||
// Log the configuration update for debugging purposes
|
||||
logger.info("Update configuration option: target=${configTarget?.let { ConfigurationTarget.toString(it) }}, key=$key, value=$value, " +
|
||||
"overrideIdentifier=${configOverrides?.overrideIdentifier}, resource=${configOverrides?.resource}, " +
|
||||
"scopeToLanguage=$scopeToLanguage")
|
||||
|
||||
// Build the complete configuration key including overrides and language scoping
|
||||
val fullKey = buildConfigurationKey(key, configOverrides, scopeToLanguage)
|
||||
|
||||
// Store the configuration value based on the target scope
|
||||
when (configTarget) {
|
||||
ConfigurationTarget.APPLICATION -> {
|
||||
// Application-level configuration applies to all projects and users
|
||||
val properties = PropertiesComponent.getInstance()
|
||||
storeValue(properties, fullKey, value)
|
||||
}
|
||||
ConfigurationTarget.WORKSPACE, ConfigurationTarget.WORKSPACE_FOLDER -> {
|
||||
// Project-level configuration applies to the current project
|
||||
val activeProject = getActiveProject()
|
||||
if (activeProject != null) {
|
||||
val properties = PropertiesComponent.getInstance(activeProject)
|
||||
storeValue(properties, fullKey, value)
|
||||
} else {
|
||||
logger.warn("Failed to save project-level configuration, no active project found")
|
||||
}
|
||||
}
|
||||
ConfigurationTarget.USER, ConfigurationTarget.USER_LOCAL -> {
|
||||
// User-level configuration applies to the current user across projects
|
||||
val properties = PropertiesComponent.getInstance()
|
||||
val userPrefixedKey = "user.$fullKey"
|
||||
storeValue(properties, userPrefixedKey, value)
|
||||
}
|
||||
else -> {
|
||||
// Memory-level configuration is temporary and not persisted
|
||||
val properties = PropertiesComponent.getInstance()
|
||||
val memoryPrefixedKey = "memory.$fullKey"
|
||||
storeValue(properties, memoryPrefixedKey, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a configuration option from the specified target scope.
|
||||
* Handles the conversion of parameters and delegates to the appropriate
|
||||
* removal mechanism based on the configuration target.
|
||||
*/
|
||||
override fun removeConfigurationOption(
|
||||
target: Int,
|
||||
key: String,
|
||||
overrides: Map<String, Any>?,
|
||||
scopeToLanguage: Boolean?
|
||||
) {
|
||||
// Convert parameter types from raw values to type-safe objects
|
||||
val configTarget = ConfigurationTarget.fromValue(target)
|
||||
val configOverrides = convertToConfigurationOverrides(overrides)
|
||||
|
||||
// Log the configuration removal for debugging purposes
|
||||
logger.info("Remove configuration option: target=${configTarget?.let { ConfigurationTarget.toString(it) }}, key=$key, " +
|
||||
"overrideIdentifier=${configOverrides?.overrideIdentifier}, resource=${configOverrides?.resource}, " +
|
||||
"scopeToLanguage=$scopeToLanguage")
|
||||
|
||||
// Build the complete configuration key including overrides and language scoping
|
||||
val fullKey = buildConfigurationKey(key, configOverrides, scopeToLanguage)
|
||||
|
||||
// Remove the configuration value based on the target scope
|
||||
when (configTarget) {
|
||||
ConfigurationTarget.APPLICATION -> {
|
||||
// Remove application-level configuration
|
||||
val properties = PropertiesComponent.getInstance()
|
||||
properties.unsetValue(fullKey)
|
||||
}
|
||||
ConfigurationTarget.WORKSPACE, ConfigurationTarget.WORKSPACE_FOLDER -> {
|
||||
// Remove project-level configuration
|
||||
val activeProject = getActiveProject()
|
||||
if (activeProject != null) {
|
||||
val properties = PropertiesComponent.getInstance(activeProject)
|
||||
properties.unsetValue(fullKey)
|
||||
} else {
|
||||
logger.warn("Failed to remove project-level configuration, no active project found")
|
||||
}
|
||||
}
|
||||
ConfigurationTarget.USER, ConfigurationTarget.USER_LOCAL -> {
|
||||
// Remove user-level configuration
|
||||
val properties = PropertiesComponent.getInstance()
|
||||
val userPrefixedKey = "user.$fullKey"
|
||||
properties.unsetValue(userPrefixedKey)
|
||||
}
|
||||
else -> {
|
||||
// Remove memory-level configuration
|
||||
val properties = PropertiesComponent.getInstance()
|
||||
val memoryPrefixedKey = "memory.$fullKey"
|
||||
properties.unsetValue(memoryPrefixedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Map<String, Any> to a ConfigurationOverrides object.
|
||||
* Handles the parsing of URI strings and map structures into proper URI objects.
|
||||
* @param overridesMap The overrides map containing configuration override data
|
||||
* @return The configuration overrides object, or null if conversion fails
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun convertToConfigurationOverrides(overridesMap: Map<String, Any>?): ConfigurationOverrides? {
|
||||
if (overridesMap.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val overrideIdentifier = overridesMap["overrideIdentifier"] as? String
|
||||
val resourceUri = when (val uriObj = overridesMap["resource"]) {
|
||||
is Map<*, *> -> {
|
||||
// Extract URI components from the map structure
|
||||
val scheme = uriObj["scheme"] as? String ?: ""
|
||||
val path = uriObj["path"] as? String ?: ""
|
||||
val authority = uriObj["authority"] as? String ?: ""
|
||||
val query = uriObj["query"] as? String ?: ""
|
||||
val fragment = uriObj["fragment"] as? String ?: ""
|
||||
|
||||
if (path.isNotEmpty()) {
|
||||
// Create URI instance using URI.from static method
|
||||
val uriComponents = object : URIComponents {
|
||||
override val scheme: String = scheme
|
||||
override val authority: String = authority
|
||||
override val path: String = path
|
||||
override val query: String = query
|
||||
override val fragment: String = fragment
|
||||
}
|
||||
URI.from(uriComponents)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is String -> {
|
||||
try {
|
||||
// Parse URI string using URI.parse static method
|
||||
URI.parse(uriObj)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Failed to parse URI string: $uriObj", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
return ConfigurationOverrides(overrideIdentifier, resourceUri)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to convert configuration overrides: $overridesMap", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a complete configuration key based on base key, overrides, and language scoping.
|
||||
* Constructs a unique key that incorporates override identifiers and resource contexts.
|
||||
* @param baseKey The base configuration key
|
||||
* @param overrides Optional configuration overrides to include in the key
|
||||
* @param scopeToLanguage Whether to scope the configuration to a specific language
|
||||
* @return The complete configuration key string
|
||||
*/
|
||||
private fun buildConfigurationKey(baseKey: String, overrides: ConfigurationOverrides?, scopeToLanguage: Boolean?): String {
|
||||
val keyBuilder = StringBuilder(baseKey)
|
||||
|
||||
// Add override identifier if language scoping is enabled
|
||||
overrides?.let {
|
||||
it.overrideIdentifier?.let { identifier ->
|
||||
if (scopeToLanguage == true) {
|
||||
keyBuilder.append(".").append(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
// Add resource identifier if URI is provided
|
||||
it.resource?.let { uri ->
|
||||
keyBuilder.append("@").append(uri.toString().hashCode())
|
||||
}
|
||||
}
|
||||
|
||||
return keyBuilder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently active project in the IDE.
|
||||
* @return The active project instance, or null if no project is currently open
|
||||
*/
|
||||
private fun getActiveProject(): Project? {
|
||||
val openProjects = ProjectManager.getInstance().openProjects
|
||||
return openProjects.firstOrNull { it.isInitialized && !it.isDisposed }
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a configuration value in the properties component based on its type.
|
||||
* Handles type-specific storage for common data types and falls back to string
|
||||
* representation for complex objects.
|
||||
* @param properties The properties component to store the value in
|
||||
* @param key The configuration key
|
||||
* @param value The configuration value to store
|
||||
*/
|
||||
private fun storeValue(properties: PropertiesComponent, key: String, value: Any?) {
|
||||
when (value) {
|
||||
null -> properties.unsetValue(key)
|
||||
is String -> properties.setValue(key, value)
|
||||
is Boolean -> properties.setValue(key, value)
|
||||
is Int -> properties.setValue(key, value, 0)
|
||||
is Float -> properties.setValue(key, value.toString())
|
||||
is Double -> properties.setValue(key, value.toString())
|
||||
is Long -> properties.setValue(key, value.toString())
|
||||
else -> {
|
||||
// Convert complex objects to JSON string for storage
|
||||
try {
|
||||
properties.setValue(key, value.toString())
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to serialize configuration value, type: ${value.javaClass.name}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of resources when the configuration manager is no longer needed.
|
||||
* Called when the plugin or component is being unloaded.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Releasing resources: MainThreadConfiguration")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* Remote console log.
|
||||
* Corresponds to the IRemoteConsoleLog interface in TypeScript.
|
||||
*/
|
||||
data class RemoteConsoleLog(
|
||||
val type: String, // Log type: "log", "warn", "error", "info", "debug"
|
||||
val severity: Int, // Severity level
|
||||
val args: List<Any?>, // Log arguments
|
||||
val source: String? = null, // Log source
|
||||
val line: Int? = null, // Source line number
|
||||
val columnNumber: Int? = null, // Column number
|
||||
val timestamp: Long = System.currentTimeMillis() // Timestamp
|
||||
)
|
||||
|
||||
/**
|
||||
* Main thread console service interface.
|
||||
* Corresponds to the MainThreadConsoleShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadConsoleShape : Disposable {
|
||||
/**
|
||||
* Logs extension host message.
|
||||
* @param msg Log message object
|
||||
*/
|
||||
fun logExtensionHostMessage(msg: Map<String, Any>)
|
||||
|
||||
/**
|
||||
* Releases resources.
|
||||
*/
|
||||
override fun dispose()
|
||||
}
|
||||
|
||||
class MainThreadConsole : MainThreadConsoleShape {
|
||||
private val logger = Logger.getInstance(MainThreadConsole::class.java)
|
||||
|
||||
/**
|
||||
* Logs extension host message.
|
||||
* @param msg Log message object
|
||||
*/
|
||||
override fun logExtensionHostMessage(msg: Map<String, Any>) {
|
||||
val type = msg["type"]
|
||||
val severity = msg["severity"]
|
||||
val arguments = msg["arguments"]?.let { args ->
|
||||
if (args is List<*>) {
|
||||
args.joinToString(", ") { it.toString() }
|
||||
} else {
|
||||
args.toString()
|
||||
}
|
||||
} ?: return
|
||||
|
||||
try {
|
||||
when (severity) {
|
||||
// "log", "info" -> logger.info("[Extension Host] $arguments")
|
||||
"warn" -> logger.warn("[Extension Host] $arguments")
|
||||
"error" -> logger.warn("[Extension Host] ERROR: $arguments")
|
||||
// "debug" -> logger.debug("[Extension Host] $arguments")
|
||||
// else -> logger.info("[Extension Host] $arguments")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to process extension host log message", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadConsole")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Main thread debug service interface.
|
||||
* This interface defines the contract for debug services that operate on the main thread,
|
||||
* providing methods for managing debug sessions, breakpoints, and debug adapter communication.
|
||||
*/
|
||||
interface MainThreadDebugServiceShape : Disposable {
|
||||
/**
|
||||
* Registers debug types that this service can handle.
|
||||
* @param debugTypes List of debug type identifiers (e.g., "java", "python", "node")
|
||||
*/
|
||||
fun registerDebugTypes(debugTypes: List<String>)
|
||||
|
||||
/**
|
||||
* Notifies that a debug session has been cached/stored for later use.
|
||||
* @param sessionID Unique identifier for the debug session
|
||||
*/
|
||||
fun sessionCached(sessionID: String)
|
||||
|
||||
/**
|
||||
* Accepts and processes a message from the debug adapter.
|
||||
* @param handle Unique handle identifying the debug adapter connection
|
||||
* @param message The protocol message received from the debug adapter
|
||||
*/
|
||||
fun acceptDAMessage(handle: Int, message: Any)
|
||||
|
||||
/**
|
||||
* Accepts and processes an error reported by the debug adapter.
|
||||
* @param handle Unique handle identifying the debug adapter connection
|
||||
* @param name The error name/type
|
||||
* @param message Human-readable error message
|
||||
* @param stack Optional stack trace for the error
|
||||
*/
|
||||
fun acceptDAError(handle: Int, name: String, message: String, stack: String?)
|
||||
|
||||
/**
|
||||
* Accepts notification that the debug adapter has exited.
|
||||
* @param handle Unique handle identifying the debug adapter connection
|
||||
* @param code Optional exit code (null if terminated by signal)
|
||||
* @param signal Optional signal name that caused termination (null if exited normally)
|
||||
*/
|
||||
fun acceptDAExit(handle: Int, code: Int?, signal: String?)
|
||||
|
||||
/**
|
||||
* Registers a debug configuration provider for a specific debug type.
|
||||
* @param type The debug type this provider handles
|
||||
* @param triggerKind When this provider should be triggered (1=initial, 2=dynamic)
|
||||
* @param hasProvideMethod Whether this provider has a provideDebugConfigurations method
|
||||
* @param hasResolveMethod Whether this provider has a resolveDebugConfiguration method
|
||||
* @param hasResolve2Method Whether this provider has a resolveDebugConfigurationWithSubstitutedVariables method
|
||||
* @param handle Unique handle for this provider registration
|
||||
* @return Registration result (typically Unit or success indicator)
|
||||
*/
|
||||
fun registerDebugConfigurationProvider(
|
||||
type: String,
|
||||
triggerKind: Int,
|
||||
hasProvideMethod: Boolean,
|
||||
hasResolveMethod: Boolean,
|
||||
hasResolve2Method: Boolean,
|
||||
handle: Int
|
||||
): Any
|
||||
|
||||
/**
|
||||
* Registers a debug adapter descriptor factory for a specific debug type.
|
||||
* @param type The debug type this factory creates adapters for
|
||||
* @param handle Unique handle for this factory registration
|
||||
* @return Registration result (typically Unit or success indicator)
|
||||
*/
|
||||
fun registerDebugAdapterDescriptorFactory(type: String, handle: Int): Any
|
||||
|
||||
/**
|
||||
* Unregisters a debug configuration provider.
|
||||
* @param handle The handle of the provider to unregister
|
||||
*/
|
||||
fun unregisterDebugConfigurationProvider(handle: Int)
|
||||
|
||||
/**
|
||||
* Unregisters a debug adapter descriptor factory.
|
||||
* @param handle The handle of the factory to unregister
|
||||
*/
|
||||
fun unregisterDebugAdapterDescriptorFactory(handle: Int)
|
||||
|
||||
/**
|
||||
* Starts a new debugging session.
|
||||
* @param folder Optional workspace folder URI for the debug session
|
||||
* @param nameOrConfig Either the name of a predefined configuration or the configuration object itself
|
||||
* @param options Launch options for the debug session
|
||||
* @return Success indicator (true if debugging started successfully)
|
||||
*/
|
||||
fun startDebugging(folder: URI?, nameOrConfig: Any, options: Any): Any
|
||||
|
||||
/**
|
||||
* Stops an active debugging session.
|
||||
* @param sessionId Optional session ID to stop (null stops all sessions)
|
||||
* @return Operation result (typically Unit)
|
||||
*/
|
||||
fun stopDebugging(sessionId: String?): Any
|
||||
|
||||
/**
|
||||
* Sets a custom name for a debug session.
|
||||
* @param id The session ID to name
|
||||
* @param name The display name for the session
|
||||
*/
|
||||
fun setDebugSessionName(id: String, name: String)
|
||||
|
||||
/**
|
||||
* Sends a custom request to the debug adapter.
|
||||
* @param id The session ID to send the request to
|
||||
* @param command The debug adapter protocol command
|
||||
* @param args Arguments for the command
|
||||
* @return The response from the debug adapter
|
||||
*/
|
||||
fun customDebugAdapterRequest(id: String, command: String, args: Any): Any
|
||||
|
||||
/**
|
||||
* Retrieves information about a specific breakpoint from the debug protocol.
|
||||
* @param id The session ID
|
||||
* @param breakpoinId The breakpoint ID to query
|
||||
* @return Breakpoint information or null if not found
|
||||
*/
|
||||
fun getDebugProtocolBreakpoint(id: String, breakpoinId: String): Any?
|
||||
|
||||
/**
|
||||
* Appends text to the debug console output.
|
||||
* @param value The text to append to the console
|
||||
*/
|
||||
fun appendDebugConsole(value: String)
|
||||
|
||||
/**
|
||||
* Registers new breakpoints with the debug service.
|
||||
* @param breakpoints List of breakpoint objects to register
|
||||
* @return Registration result (typically Unit or success indicator)
|
||||
*/
|
||||
fun registerBreakpoints(breakpoints: List<Any>): Any
|
||||
|
||||
/**
|
||||
* Unregisters existing breakpoints.
|
||||
* @param breakpointIds List of regular breakpoint IDs to remove
|
||||
* @param functionBreakpointIds List of function breakpoint IDs to remove
|
||||
* @param dataBreakpointIds List of data breakpoint IDs to remove
|
||||
* @return Unregistration result (typically Unit)
|
||||
*/
|
||||
fun unregisterBreakpoints(
|
||||
breakpointIds: List<String>,
|
||||
functionBreakpointIds: List<String>,
|
||||
dataBreakpointIds: List<String>
|
||||
): Any
|
||||
|
||||
/**
|
||||
* Registers a debug visualizer extension.
|
||||
* @param extensionId The ID of the extension providing the visualizer
|
||||
* @param id The unique ID of the visualizer within the extension
|
||||
*/
|
||||
fun registerDebugVisualizer(extensionId: String, id: String)
|
||||
|
||||
/**
|
||||
* Unregisters a debug visualizer extension.
|
||||
* @param extensionId The ID of the extension providing the visualizer
|
||||
* @param id The unique ID of the visualizer within the extension
|
||||
*/
|
||||
fun unregisterDebugVisualizer(extensionId: String, id: String)
|
||||
|
||||
/**
|
||||
* Registers a debug visualizer tree structure.
|
||||
* @param treeId Unique identifier for the tree
|
||||
* @param canEdit Whether the tree structure can be edited by users
|
||||
*/
|
||||
fun registerDebugVisualizerTree(treeId: String, canEdit: Boolean)
|
||||
|
||||
/**
|
||||
* Unregisters a debug visualizer tree structure.
|
||||
* @param treeId Unique identifier for the tree to unregister
|
||||
*/
|
||||
fun unregisterDebugVisualizerTree(treeId: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread debug service implementation.
|
||||
* This class provides the concrete implementation of the MainThreadDebugServiceShape interface,
|
||||
* handling debug session management, breakpoint operations, and debug adapter communication.
|
||||
* All operations are logged for debugging purposes.
|
||||
*/
|
||||
class MainThreadDebugService : MainThreadDebugServiceShape {
|
||||
private val logger = Logger.getInstance(MainThreadDebugService::class.java)
|
||||
|
||||
override fun registerDebugTypes(debugTypes: List<String>) {
|
||||
logger.info("Registering debug types: $debugTypes")
|
||||
}
|
||||
|
||||
override fun sessionCached(sessionID: String) {
|
||||
logger.info("Session cached: $sessionID")
|
||||
}
|
||||
|
||||
override fun acceptDAMessage(handle: Int, message: Any) {
|
||||
logger.info("Received debug adapter message: handle=$handle, message=$message")
|
||||
}
|
||||
|
||||
override fun acceptDAError(handle: Int, name: String, message: String, stack: String?) {
|
||||
logger.info("Received debug adapter error: handle=$handle, name=$name, message=$message, stack=$stack")
|
||||
}
|
||||
|
||||
override fun acceptDAExit(handle: Int, code: Int?, signal: String?) {
|
||||
logger.info("Received debug adapter exit: handle=$handle, code=$code, signal=$signal")
|
||||
}
|
||||
|
||||
override fun registerDebugConfigurationProvider(
|
||||
type: String,
|
||||
triggerKind: Int,
|
||||
hasProvideMethod: Boolean,
|
||||
hasResolveMethod: Boolean,
|
||||
hasResolve2Method: Boolean,
|
||||
handle: Int
|
||||
): Any {
|
||||
logger.info("Registering debug configuration provider: type=$type, triggerKind=$triggerKind, " +
|
||||
"hasProvideMethod=$hasProvideMethod, hasResolveMethod=$hasResolveMethod, " +
|
||||
"hasResolve2Method=$hasResolve2Method, handle=$handle")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun registerDebugAdapterDescriptorFactory(type: String, handle: Int): Any {
|
||||
logger.info("Registering debug adapter descriptor factory: type=$type, handle=$handle")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun unregisterDebugConfigurationProvider(handle: Int) {
|
||||
logger.info("Unregistering debug configuration provider: handle=$handle")
|
||||
}
|
||||
|
||||
override fun unregisterDebugAdapterDescriptorFactory(handle: Int) {
|
||||
logger.info("Unregistering debug adapter descriptor factory: handle=$handle")
|
||||
}
|
||||
|
||||
override fun startDebugging(folder: URI?, nameOrConfig: Any, options: Any): Any {
|
||||
logger.info("Starting debugging: folder=$folder, nameOrConfig=$nameOrConfig, options=$options")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun stopDebugging(sessionId: String?): Any {
|
||||
logger.info("Stopping debugging: sessionId=$sessionId")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun setDebugSessionName(id: String, name: String) {
|
||||
logger.info("Setting debug session name: id=$id, name=$name")
|
||||
}
|
||||
|
||||
override fun customDebugAdapterRequest(id: String, command: String, args: Any): Any {
|
||||
logger.info("Custom debug adapter request: id=$id, command=$command, args=$args")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun getDebugProtocolBreakpoint(id: String, breakpoinId: String): Any? {
|
||||
logger.info("Getting debug protocol breakpoint: id=$id, breakpoinId=$breakpoinId")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun appendDebugConsole(value: String) {
|
||||
logger.info("Appending to debug console: $value")
|
||||
}
|
||||
|
||||
override fun registerBreakpoints(breakpoints: List<Any>): Any {
|
||||
logger.info("Registering breakpoints: ${breakpoints.size} total")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun unregisterBreakpoints(
|
||||
breakpointIds: List<String>,
|
||||
functionBreakpointIds: List<String>,
|
||||
dataBreakpointIds: List<String>
|
||||
): Any {
|
||||
logger.info("Unregistering breakpoints: ${breakpointIds.size} regular, " +
|
||||
"${functionBreakpointIds.size} function, " +
|
||||
"${dataBreakpointIds.size} data breakpoints")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun registerDebugVisualizer(extensionId: String, id: String) {
|
||||
logger.info("Registering debug visualizer: extensionId=$extensionId, id=$id")
|
||||
}
|
||||
|
||||
override fun unregisterDebugVisualizer(extensionId: String, id: String) {
|
||||
logger.info("Unregistering debug visualizer: extensionId=$extensionId, id=$id")
|
||||
}
|
||||
|
||||
override fun registerDebugVisualizerTree(treeId: String, canEdit: Boolean) {
|
||||
logger.info("Registering debug visualizer tree: treeId=$treeId, canEdit=$canEdit")
|
||||
}
|
||||
|
||||
override fun unregisterDebugVisualizerTree(treeId: String) {
|
||||
logger.info("Unregistering debug visualizer tree: treeId=$treeId")
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadDebugService")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.fileChooser.FileChooser
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||
import ai.roocode.jetbrains.util.URI
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.fileChooser.FileChooserFactory
|
||||
import com.intellij.openapi.fileChooser.FileSaverDescriptor
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/**
|
||||
* Configuration options for the open file dialog.
|
||||
* This data class encapsulates all the parameters needed to customize the file chooser dialog.
|
||||
*
|
||||
* @property defaultUri The default URI/path to start browsing from
|
||||
* @property openLabel Custom label text for the dialog's open button
|
||||
* @property canSelectFiles Whether files can be selected in the dialog
|
||||
* @property canSelectFolders Whether folders can be selected in the dialog
|
||||
* @property canSelectMany Whether multiple items can be selected simultaneously
|
||||
* @property filters File extension filters for filtering displayed files (format: {"Description": ["ext1", "ext2"]})
|
||||
* @property title Custom title for the dialog window
|
||||
* @property allowUIResources Whether to allow UI resources to be selected
|
||||
*/
|
||||
data class MainThreadDialogOpenOptions(
|
||||
val defaultUri: Map<String, String?>?,
|
||||
val openLabel: String?,
|
||||
val canSelectFiles: Boolean?,
|
||||
val canSelectFolders: Boolean?,
|
||||
val canSelectMany: Boolean?,
|
||||
val filters: MutableMap<String, MutableList<String>>?,
|
||||
val title: String?,
|
||||
val allowUIResources: Boolean?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Interface defining the contract for main thread dialog operations.
|
||||
* This interface provides methods for showing file open and save dialogs that must be executed on the main UI thread.
|
||||
*/
|
||||
interface MainThreadDiaglogsShape : Disposable {
|
||||
/**
|
||||
* Shows an open file dialog and returns the selected file URIs.
|
||||
*
|
||||
* @param options Configuration options for customizing the dialog behavior
|
||||
* @return List of selected file URIs, or null if the dialog was cancelled
|
||||
*/
|
||||
suspend fun showOpenDialog(options: Map<String, Any?>?): MutableList<URI>?
|
||||
|
||||
/**
|
||||
* Shows a save file dialog and returns the selected file URI.
|
||||
*
|
||||
* @param options Configuration options for customizing the dialog behavior
|
||||
* @return The selected file URI for saving, or null if the dialog was cancelled
|
||||
*/
|
||||
suspend fun showSaveDialog(options: Map<String, Any?>?): URI?
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of MainThreadDiaglogsShape that provides file dialog functionality
|
||||
* executed on the IntelliJ platform's main UI thread.
|
||||
*
|
||||
* This class handles both file open and save dialogs using IntelliJ's file chooser APIs,
|
||||
* ensuring all UI operations are performed on the main thread as required by the platform.
|
||||
*/
|
||||
class MainThreadDiaglogs : MainThreadDiaglogsShape {
|
||||
private val logger = Logger.getInstance(MainThreadDiaglogs::class.java)
|
||||
|
||||
/**
|
||||
* Shows an open file dialog with the specified options.
|
||||
*
|
||||
* This method creates a file chooser dialog that allows users to select one or more files
|
||||
* based on the provided configuration. The operation is performed on the main UI thread
|
||||
* using IntelliJ's invokeLater mechanism.
|
||||
*
|
||||
* @param map Configuration map containing dialog options
|
||||
* @return Mutable list of selected file URIs, or null if cancelled
|
||||
*/
|
||||
override suspend fun showOpenDialog(map: Map<String, Any?>?): MutableList<URI>? {
|
||||
// Convert the configuration map to typed options
|
||||
val options = create(map)
|
||||
|
||||
// Create file chooser descriptor with default values for unspecified options
|
||||
val descriptor = FileChooserDescriptor(
|
||||
/* chooseFiles = */ true,
|
||||
/* chooseFolders = */ options?.canSelectFolders ?: true,
|
||||
/* chooseJars = */ false,
|
||||
/* chooseJarsAsFiles = */ false,
|
||||
/* chooseMultipleJars = */ false,
|
||||
/* chooseMultiple = */ options?.canSelectMany ?: true
|
||||
)
|
||||
.withTitle(options?.title ?: "Open")
|
||||
.withDescription(options?.openLabel ?: "Select files")
|
||||
|
||||
// Apply file extension filters if provided
|
||||
options?.filters?.forEach { (name, extensions) ->
|
||||
descriptor.withFileFilter { file ->
|
||||
extensions.any { file.extension?.equals(it, true) ?: false }
|
||||
}
|
||||
}
|
||||
|
||||
// Use coroutine to handle the asynchronous file chooser operation
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
try {
|
||||
// Show the file chooser dialog and get selected files
|
||||
val files = FileChooser.chooseFiles(descriptor, null, null)
|
||||
|
||||
// Convert IntelliJ VirtualFile objects to URI objects
|
||||
val result = files.map { file ->
|
||||
URI.file(file.path)
|
||||
}.toMutableList()
|
||||
|
||||
// Resume coroutine with the result
|
||||
continuation.resume(result)
|
||||
} catch (e: Exception) {
|
||||
// Resume coroutine with exception if an error occurs
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}, ModalityState.defaultModalityState())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a save file dialog with the specified options.
|
||||
*
|
||||
* This method creates a file saver dialog that allows users to select a location
|
||||
* and filename for saving a file. The operation is performed on the main UI thread.
|
||||
*
|
||||
* @param map Configuration map containing dialog options
|
||||
* @return URI of the selected save location, or null if cancelled
|
||||
*/
|
||||
override suspend fun showSaveDialog(map: Map<String, Any?>?): URI? {
|
||||
// Convert the configuration map to typed options
|
||||
val options = create(map)
|
||||
|
||||
// Create file saver descriptor with custom title and description
|
||||
val descriptor = FileSaverDescriptor("Save", options?.openLabel ?: "Select save location")
|
||||
|
||||
// Apply file extension filters if provided
|
||||
options?.filters?.forEach { (name, extensions) ->
|
||||
descriptor.withFileFilter { file ->
|
||||
extensions.any { file.extension?.equals(it, true) ?: false }
|
||||
}
|
||||
}
|
||||
|
||||
// Extract default path and filename from options
|
||||
val path = options?.defaultUri?.get("path")
|
||||
var fileName: String? = null
|
||||
|
||||
// Convert the path string to a Path object and extract filename
|
||||
val virtualFile = path?.let { filePath ->
|
||||
val file = File(filePath)
|
||||
fileName = file.name
|
||||
Path.of(file.parentFile.absolutePath)
|
||||
}
|
||||
|
||||
// Use coroutine to handle the asynchronous save dialog operation
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
ApplicationManager.getApplication().invokeLater({
|
||||
try {
|
||||
// Show the save file dialog and get the selected file
|
||||
val file = FileChooserFactory.getInstance()
|
||||
.createSaveFileDialog(descriptor, null)
|
||||
.save(virtualFile, fileName)
|
||||
|
||||
// Convert the result to URI format
|
||||
val result = file?.let { URI.file(it.file.absolutePath) }
|
||||
|
||||
// Resume coroutine with the result
|
||||
continuation.resume(result)
|
||||
} catch (e: Exception) {
|
||||
// Resume coroutine with exception if an error occurs
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}, ModalityState.defaultModalityState())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MainThreadDialogOpenOptions instance from a configuration map.
|
||||
*
|
||||
* This helper method safely extracts typed values from a generic map structure
|
||||
* and constructs a properly typed configuration object.
|
||||
*
|
||||
* @param map Configuration map containing dialog options as key-value pairs
|
||||
* @return MainThreadDialogOpenOptions instance, or null if map is null
|
||||
*/
|
||||
private fun create(map: Map<String, Any?>?): MainThreadDialogOpenOptions? {
|
||||
map?.let {
|
||||
return MainThreadDialogOpenOptions(
|
||||
defaultUri = it["defaultUri"] as? Map<String, String?>,
|
||||
openLabel = it["openLabel"] as? String,
|
||||
canSelectFiles = it["canSelectFiles"] as? Boolean,
|
||||
canSelectFolders = it["canSelectFolders"] as? Boolean,
|
||||
canSelectMany = it["canSelectMany"] as? Boolean,
|
||||
filters = it["filters"] as? MutableMap<String, MutableList<String>>,
|
||||
title = it["title"] as? String,
|
||||
allowUIResources = it["allowUIResources"] as? Boolean
|
||||
)
|
||||
} ?: return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of any resources held by this dialog handler.
|
||||
*
|
||||
* This method is called when the plugin or component is being shut down,
|
||||
* allowing for proper cleanup of resources.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadDiaglogs")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import java.net.URI
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
/**
|
||||
* Interfaces related to document content providers.
|
||||
*/
|
||||
interface MainThreadDocumentContentProvidersShape : Disposable {
|
||||
/**
|
||||
* Registers a text content provider.
|
||||
* @param handle Provider handle
|
||||
* @param scheme URI scheme
|
||||
*/
|
||||
fun registerTextContentProvider(handle: Int, scheme: String)
|
||||
|
||||
/**
|
||||
* Unregisters a text content provider.
|
||||
* @param handle Provider handle
|
||||
*/
|
||||
fun unregisterTextContentProvider(handle: Int)
|
||||
|
||||
/**
|
||||
* Virtual document content change.
|
||||
* @param uri Document URI
|
||||
* @param value New content
|
||||
* @return Execution result
|
||||
*/
|
||||
suspend fun onVirtualDocumentChange(uri: Map<String, Any?>, value: String): Any
|
||||
}
|
||||
|
||||
class MainThreadDocumentContentProviders : MainThreadDocumentContentProvidersShape {
|
||||
private val logger = Logger.getInstance(MainThreadDocumentContentProviders::class.java)
|
||||
|
||||
override fun registerTextContentProvider(handle: Int, scheme: String) {
|
||||
logger.info("Register text content provider: handle=$handle, scheme=$scheme")
|
||||
}
|
||||
|
||||
override fun unregisterTextContentProvider(handle: Int) {
|
||||
logger.info("Unregister text content provider: handle=$handle")
|
||||
}
|
||||
|
||||
override suspend fun onVirtualDocumentChange(uri: Map<String, Any?>, value: String): Any {
|
||||
logger.info("Virtual document content changed: uri=$uri")
|
||||
return CompletableDeferred<Unit>().also { it.complete(Unit) }.await()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadDocumentContentProviders resources")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.google.common.collect.Maps
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.testFramework.utils.vfs.createFile
|
||||
import ai.roocode.jetbrains.editor.EditorAndDocManager
|
||||
import ai.roocode.jetbrains.editor.createURI
|
||||
import ai.roocode.jetbrains.service.DocumentSyncService
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManagerListener
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.util.messages.MessageBusConnection
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import kotlinx.coroutines.cancel
|
||||
import java.io.File
|
||||
|
||||
interface MainThreadDocumentsShape {
|
||||
suspend fun tryCreateDocument(options: Map<String, Any?>?): Map<String, Any?>
|
||||
suspend fun tryOpenDocument(uri: Map<String, Any?>, options: Map<String, Any?>?): Map<String, Any?>
|
||||
suspend fun trySaveDocument(uri: Map<String, Any?>): Boolean
|
||||
suspend fun tryOpenDocument(map: Map<String, Any?>, options: String?): Map<String, Any?>
|
||||
}
|
||||
|
||||
class MainThreadDocuments(var project: Project) : MainThreadDocumentsShape {
|
||||
val logger = Logger.getInstance(MainThreadDocuments::class.java)
|
||||
private var messageBusConnection: MessageBusConnection? = null
|
||||
private val documentSyncService = DocumentSyncService(project)
|
||||
|
||||
/** Coroutine scope tied to this instance, cancelled in [dispose]. */
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
init {
|
||||
setupDocumentSaveListener()
|
||||
}
|
||||
|
||||
private fun setupDocumentSaveListener() {
|
||||
try {
|
||||
// Connect to the message bus
|
||||
messageBusConnection = ApplicationManager.getApplication().messageBus.connect()
|
||||
|
||||
// Listen for document save events
|
||||
messageBusConnection?.subscribe(
|
||||
FileDocumentManagerListener.TOPIC,
|
||||
object : FileDocumentManagerListener {
|
||||
override fun beforeDocumentSaving(document: Document) {
|
||||
handleDocumentSaving(document)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("Document save listener registered successfully")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to setup document save listener", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDocumentSaving(document: Document) {
|
||||
// Get the virtual file associated with the document
|
||||
val virtualFile = FileDocumentManager.getInstance().getFile(document)
|
||||
logger.info("Handle document save event: ${virtualFile?.path}")
|
||||
|
||||
if (virtualFile != null && documentSyncService.shouldHandleFileEvent(virtualFile)) {
|
||||
// Handle in the coroutine scope dedicated to this instance to avoid issues with older IDEs lacking project.coroutineScope
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
// Wait a short time to ensure the save operation is complete
|
||||
delay(50)
|
||||
if (!project.isDisposed) {
|
||||
documentSyncService.syncDocumentStateOnSave(virtualFile, document)
|
||||
}
|
||||
} catch (e: ProcessCanceledException) {
|
||||
// Normal control flow exception, can be ignored
|
||||
logger.debug("Document save cancelled because project is disposed")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error handling document save event", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun tryCreateDocument(options: Map<String, Any?>?): Map<String, Any?> {
|
||||
logger.info("tryCreateDocument$options")
|
||||
return mapOf()
|
||||
}
|
||||
|
||||
override suspend fun tryOpenDocument(map: Map<String, Any?>, options: Map<String, Any?>?): Map<String, Any?> {
|
||||
val uri = createURI(map)
|
||||
logger.info("tryOpenDocument : ${uri.path}")
|
||||
|
||||
val file = File(uri.path)
|
||||
val vfs = LocalFileSystem.getInstance()
|
||||
if (!file.exists()) {
|
||||
file.parentFile.mkdirs()
|
||||
val vf = vfs.findFileByIoFile(file.parentFile)
|
||||
vf?.createFile(file.name)
|
||||
}
|
||||
|
||||
project.getService(EditorAndDocManager::class.java).openDocument(uri)
|
||||
|
||||
logger.info("tryOpenDocument : ${uri.path} execution completed")
|
||||
return map
|
||||
}
|
||||
|
||||
// This function is designed to work around a VS Code type system issue where a string argument may be incorrectly treated as an options: {} object. To prevent this, multiple function overloads are declared.
|
||||
override suspend fun tryOpenDocument(map: Map<String, Any?>, options: String?): Map<String, Any?> {
|
||||
return tryOpenDocument(map, HashMap())
|
||||
}
|
||||
|
||||
override suspend fun trySaveDocument(map: Map<String, Any?>): Boolean {
|
||||
val uri = createURI(map)
|
||||
|
||||
logger.info("trySaveDocument: ${uri.path}")
|
||||
|
||||
project.getService(EditorAndDocManager::class.java).getEditorHandleByUri(uri,true)?.updateDocumentDirty(false) ?: run {
|
||||
logger.info("trySaveDocument: ${uri.path} not found")
|
||||
return false
|
||||
}
|
||||
logger.info("trySaveDocument: ${uri.path} execution completed")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
fun dispose() {
|
||||
try {
|
||||
messageBusConnection?.disconnect()
|
||||
messageBusConnection = null
|
||||
documentSyncService.dispose()
|
||||
coroutineScope.cancel()
|
||||
logger.info("Document save listener disposed")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error disposing document save listener", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ai.roocode.jetbrains.editor.EditorAndDocManager
|
||||
|
||||
|
||||
interface MainThreadEditorTabsShape {
|
||||
fun moveTab(tabId: String, index: Int, viewColumn: Int, preserveFocus: Boolean?)
|
||||
suspend fun closeTab(tabIds: List<String>, preserveFocus: Boolean?): Boolean
|
||||
suspend fun closeGroup(groupIds: List<Int>, preservceFocus: Boolean?): Boolean
|
||||
}
|
||||
|
||||
class MainThreadEditorTabs(val project : Project) : MainThreadEditorTabsShape {
|
||||
private val logger = Logger.getInstance(MainThreadEditorTabs::class.java)
|
||||
override fun moveTab(tabId: String, index: Int, viewColumn: Int, preserveFocus: Boolean?) {
|
||||
logger.info("moveTab $tabId")
|
||||
}
|
||||
|
||||
override suspend fun closeTab(tabIds: List<String>, preserveFocus: Boolean?): Boolean {
|
||||
logger.info("closeTab $tabIds")
|
||||
|
||||
// Iterate all tab IDs and trigger close event
|
||||
var closedAny = true
|
||||
for (tabId in tabIds){
|
||||
val tab = project.getService(EditorAndDocManager::class.java).closeTab(tabId)
|
||||
// closedAny = tab?.triggerClose()?:false
|
||||
// if (closedAny){
|
||||
// project.getService(TabStateManager::class.java).removeTab(tabId)
|
||||
// }
|
||||
}
|
||||
|
||||
return closedAny
|
||||
}
|
||||
|
||||
override suspend fun closeGroup(groupIds: List<Int>, preservceFocus: Boolean?): Boolean {
|
||||
logger.info("closeGroup $groupIds")
|
||||
|
||||
// Iterate all tab group IDs and trigger close event
|
||||
var closedAny = false
|
||||
for (groupId in groupIds){
|
||||
val group = project.getService(EditorAndDocManager::class.java).closeGroup(groupId)
|
||||
// closedAny = group?.triggerClose()?:false
|
||||
}
|
||||
return closedAny
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* Main thread error handling interface.
|
||||
* Corresponds to the MainThreadErrorsShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadErrorsShape : Disposable {
|
||||
/**
|
||||
* Handles unexpected errors.
|
||||
* @param err Error information
|
||||
*/
|
||||
fun onUnexpectedError(err: Any?)
|
||||
|
||||
/**
|
||||
* Releases resources.
|
||||
*/
|
||||
override fun dispose()
|
||||
}
|
||||
|
||||
class MainThreadErrors : MainThreadErrorsShape {
|
||||
private val logger = Logger.getInstance(MainThreadErrors::class.java)
|
||||
|
||||
/**
|
||||
* Handles unexpected errors.
|
||||
* @param err Error information
|
||||
*/
|
||||
override fun onUnexpectedError(err: Any?) {
|
||||
logger.warn("Unexpected error occurred in plugin: $err")
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Dispose MainThreadErrors")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.core.ExtensionManager
|
||||
import ai.roocode.jetbrains.ipc.proxy.IRPCProtocol
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Main thread extension service interface.
|
||||
* Defines the contract for managing extensions in the main thread context.
|
||||
* This interface provides methods for extension lifecycle management,
|
||||
* activation, error handling, and utility operations.
|
||||
*/
|
||||
interface MainThreadExtensionServiceShape : Disposable {
|
||||
/**
|
||||
* Retrieves extension information by extension ID.
|
||||
* @param extensionId Extension identifier, typically provided as a Map with "value" key
|
||||
* @return Extension description object containing metadata about the extension,
|
||||
* or null if the extension is not found
|
||||
*/
|
||||
fun getExtension(extensionId: Any): Any?
|
||||
|
||||
/**
|
||||
* Activates an extension with the specified ID and reason.
|
||||
* This method triggers the extension activation process and waits for completion.
|
||||
* @param extensionId Extension identifier to activate
|
||||
* @param reason Optional activation reason or context information
|
||||
* @return Boolean indicating whether the activation was successful (true) or failed (false)
|
||||
*/
|
||||
fun activateExtension(extensionId: Any, reason: Any?): Any
|
||||
|
||||
/**
|
||||
* Called immediately before an extension is about to be activated.
|
||||
* This provides a hook for pre-activation setup or logging.
|
||||
* @param extensionId Extension identifier that will be activated
|
||||
*/
|
||||
fun onWillActivateExtension(extensionId: Any)
|
||||
|
||||
/**
|
||||
* Called after an extension has been successfully activated.
|
||||
* Provides detailed timing information about the activation process.
|
||||
* @param extensionId Extension identifier that was activated
|
||||
* @param codeLoadingTime Time taken to load extension code (in milliseconds)
|
||||
* @param activateCallTime Time taken for the activation call (in milliseconds)
|
||||
* @param activateResolvedTime Time taken to resolve activation (in milliseconds)
|
||||
* @param activationReason Reason or context for the activation
|
||||
*/
|
||||
fun onDidActivateExtension(
|
||||
extensionId: Any,
|
||||
codeLoadingTime: Double,
|
||||
activateCallTime: Double,
|
||||
activateResolvedTime: Double,
|
||||
activationReason: Any?
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles extension activation errors.
|
||||
* Called when an extension fails to activate due to errors or missing dependencies.
|
||||
* @param extensionId Extension identifier that failed to activate
|
||||
* @param error Error information or exception details
|
||||
* @param missingExtensionDependency Information about missing dependencies, if applicable
|
||||
* @return Unit (void) - the method handles the error internally
|
||||
*/
|
||||
fun onExtensionActivationError(
|
||||
extensionId: Any,
|
||||
error: Any?,
|
||||
missingExtensionDependency: Any?
|
||||
): Any
|
||||
|
||||
/**
|
||||
* Handles runtime errors that occur during extension execution.
|
||||
* Called when an extension encounters errors after successful activation.
|
||||
* @param extensionId Extension identifier that encountered the runtime error
|
||||
* @param error Error information or exception details
|
||||
*/
|
||||
fun onExtensionRuntimeError(extensionId: Any, error: Any?)
|
||||
|
||||
/**
|
||||
* Sets performance marks for extension profiling and monitoring.
|
||||
* Used to track performance metrics across extension lifecycle events.
|
||||
* @param marks List of performance mark objects containing timing information
|
||||
* @return Unit (void) - the method processes the marks internally
|
||||
*/
|
||||
fun setPerformanceMarks(marks: List<Any>)
|
||||
|
||||
/**
|
||||
* Converts a standard URI to a browser-compatible URI format.
|
||||
* This method ensures URIs are properly formatted for web browser contexts.
|
||||
* @param uri The original URI to convert
|
||||
* @return Browser-compatible URI object
|
||||
*/
|
||||
fun asBrowserUri(uri: URI): URI
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread extension service implementation.
|
||||
* Provides concrete implementation for extension management in the main thread,
|
||||
* handling extension lifecycle events, activation, and error management.
|
||||
*
|
||||
* @param extensionManager Core extension manager responsible for extension operations
|
||||
* @param rpcProtocol RPC protocol for inter-process communication with extensions
|
||||
*/
|
||||
class MainThreadExtensionService(
|
||||
private val extensionManager: ExtensionManager,
|
||||
private val rpcProtocol: IRPCProtocol
|
||||
) : MainThreadExtensionServiceShape {
|
||||
private val logger = Logger.getInstance(MainThreadExtensionService::class.java)
|
||||
|
||||
/**
|
||||
* Retrieves extension information by extension ID.
|
||||
* Safely extracts the extension ID from various input formats and queries the extension manager.
|
||||
*
|
||||
* @param extensionId Extension identifier, expected as Map with "value" key or any other type
|
||||
* @return Extension description object containing metadata, or null if not found
|
||||
*/
|
||||
override fun getExtension(extensionId: Any): Any? {
|
||||
// Safely extract extension ID string from input parameter
|
||||
val extensionIdStr = try {
|
||||
(extensionId as? Map<*, *>)?.get("value") as? String
|
||||
} catch (e: Exception) {
|
||||
// Fallback to string representation if extraction fails
|
||||
"$extensionId"
|
||||
}
|
||||
logger.info("Retrieving extension: $extensionIdStr")
|
||||
return extensionManager.getExtensionDescription(extensionIdStr.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates an extension with the specified ID and reason.
|
||||
* Uses asynchronous activation via Future and waits for completion.
|
||||
*
|
||||
* @param extensionId Extension identifier to activate
|
||||
* @param reason Optional activation reason or context information
|
||||
* @return Boolean indicating activation success (true) or failure (false)
|
||||
*/
|
||||
override fun activateExtension(extensionId: Any, reason: Any?): Any {
|
||||
// Safely extract extension ID string from input parameter
|
||||
val extensionIdStr = try {
|
||||
(extensionId as? Map<*, *>)?.get("value") as? String
|
||||
} catch (e: Exception) {
|
||||
// Fallback to string representation if extraction fails
|
||||
"$extensionId"
|
||||
}
|
||||
logger.info("Activating extension: $extensionIdStr, reason: $reason")
|
||||
|
||||
// Use Future to get asynchronous activation result
|
||||
val future = extensionManager.activateExtension(extensionIdStr.toString(), rpcProtocol)
|
||||
|
||||
return try {
|
||||
// Wait for Future completion and return result
|
||||
val result = future.get()
|
||||
logger.info("Extension $extensionIdStr activation ${if (result) "successful" else "failed"}")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logger.error("Extension $extensionIdStr activation exception", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately before extension activation begins.
|
||||
* Provides logging for pre-activation state tracking.
|
||||
*
|
||||
* @param extensionId Extension identifier about to be activated
|
||||
*/
|
||||
override fun onWillActivateExtension(extensionId: Any) {
|
||||
// Safely extract extension ID string from input parameter
|
||||
val extensionIdStr = try {
|
||||
(extensionId as? Map<*, *>)?.get("value") as? String
|
||||
} catch (e: Exception) {
|
||||
// Fallback to string representation if extraction fails
|
||||
"$extensionId"
|
||||
}
|
||||
logger.info("Extension $extensionIdStr is about to be activated")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after extension activation has completed successfully.
|
||||
* Logs activation completion with detailed timing information.
|
||||
*
|
||||
* @param extensionId Extension identifier that was activated
|
||||
* @param codeLoadingTime Time taken to load extension code (milliseconds)
|
||||
* @param activateCallTime Time taken for activation call (milliseconds)
|
||||
* @param activateResolvedTime Time taken to resolve activation (milliseconds)
|
||||
* @param activationReason Reason or context for activation
|
||||
*/
|
||||
override fun onDidActivateExtension(
|
||||
extensionId: Any,
|
||||
codeLoadingTime: Double,
|
||||
activateCallTime: Double,
|
||||
activateResolvedTime: Double,
|
||||
activationReason: Any?
|
||||
) {
|
||||
// Safely extract extension ID string from input parameter
|
||||
val extensionIdStr = try {
|
||||
(extensionId as? Map<*, *>)?.get("value") as? String
|
||||
} catch (e: Exception) {
|
||||
// Fallback to string representation if extraction fails
|
||||
"$extensionId"
|
||||
}
|
||||
logger.info("Extension $extensionIdStr activated, reason: $activationReason")
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles extension activation errors with detailed logging.
|
||||
* Called when extension activation fails due to errors or missing dependencies.
|
||||
*
|
||||
* @param extensionId Extension identifier that failed activation
|
||||
* @param error Error information or exception details
|
||||
* @param missingExtensionDependency Information about missing dependencies
|
||||
* @return Unit (void) - error is handled through logging
|
||||
*/
|
||||
override fun onExtensionActivationError(
|
||||
extensionId: Any,
|
||||
error: Any?,
|
||||
missingExtensionDependency: Any?
|
||||
): Any {
|
||||
// Safely extract extension ID string from input parameter
|
||||
val extensionIdStr = try {
|
||||
(extensionId as? Map<*, *>)?.get("value") as? String
|
||||
} catch (e: Exception) {
|
||||
// Fallback to string representation if extraction fails
|
||||
"$extensionId"
|
||||
}
|
||||
logger.error("Extension $extensionIdStr activation error: $error, missing dependency: $missingExtensionDependency")
|
||||
return Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles runtime errors that occur during extension execution.
|
||||
* Called when an activated extension encounters runtime errors.
|
||||
*
|
||||
* @param extensionId Extension identifier that encountered the error
|
||||
* @param error Error information or exception details
|
||||
*/
|
||||
override fun onExtensionRuntimeError(extensionId: Any, error: Any?) {
|
||||
// Safely extract extension ID string from input parameter
|
||||
val extensionIdStr = try {
|
||||
(extensionId as? Map<*, *>)?.get("value") as? String
|
||||
} catch (e: Exception) {
|
||||
// Fallback to string representation if extraction fails
|
||||
"$extensionId"
|
||||
}
|
||||
logger.warn("Extension $extensionIdStr runtime error: $error")
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets performance marks for extension profiling and monitoring.
|
||||
* Used to track performance metrics across extension operations.
|
||||
*
|
||||
* @param marks List of performance mark objects containing timing information
|
||||
*/
|
||||
override fun setPerformanceMarks(marks: List<Any>) {
|
||||
logger.info("Setting performance marks: $marks")
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a standard URI to browser-compatible format.
|
||||
* Ensures URIs are properly formatted for web browser contexts.
|
||||
*
|
||||
* @param uri The original URI to convert
|
||||
* @return Browser-compatible URI object (currently returns the original URI)
|
||||
*/
|
||||
override fun asBrowserUri(uri: URI): URI {
|
||||
logger.info("Converting to browser URI: $uri")
|
||||
return uri
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of resources when the service is no longer needed.
|
||||
* Called during application shutdown or when the extension service is being replaced.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadExtensionService")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* File system event service interface.
|
||||
* Provides functionality for watching file system changes.
|
||||
* Corresponds to the MainThreadFileSystemEventServiceShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadFileSystemEventServiceShape : Disposable {
|
||||
/**
|
||||
* Watches for file system changes.
|
||||
*
|
||||
* @param extensionId The extension identifier
|
||||
* @param session The session identifier
|
||||
* @param resource The resource URI as a map
|
||||
* @param opts Watch options
|
||||
* @param correlate Whether to correlate events
|
||||
*/
|
||||
fun watch(
|
||||
extensionId: String,
|
||||
session: Int,
|
||||
resource: Map<String, Any?>,
|
||||
opts: Map<String, Any?>,
|
||||
correlate: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Stops watching for file system changes.
|
||||
*
|
||||
* @param session The session identifier to stop watching
|
||||
*/
|
||||
fun unwatch(session: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the file system event service interface.
|
||||
* Handles watching and unwatching file system changes.
|
||||
*/
|
||||
class MainThreadFileSystemEventService : MainThreadFileSystemEventServiceShape {
|
||||
private val logger = Logger.getInstance(MainThreadFileSystemEventService::class.java)
|
||||
|
||||
/**
|
||||
* Starts watching for file system changes.
|
||||
*
|
||||
* @param extensionId The extension identifier
|
||||
* @param session The session identifier
|
||||
* @param resource The resource URI as a map
|
||||
* @param opts Watch options
|
||||
* @param correlate Whether to correlate events
|
||||
*/
|
||||
override fun watch(
|
||||
extensionId: String,
|
||||
session: Int,
|
||||
resource: Map<String, Any?>,
|
||||
opts: Map<String, Any?>,
|
||||
correlate: Boolean
|
||||
) {
|
||||
logger.info("Starting to watch file system changes: extensionId=$extensionId, session=$session, resource=$resource, opts=$opts, correlate=$correlate")
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops watching for file system changes.
|
||||
*
|
||||
* @param session The session identifier to stop watching
|
||||
*/
|
||||
override fun unwatch(session: Int) {
|
||||
logger.info("Stopping file system watch: session=$session")
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources used by this service.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Releasing MainThreadFileSystemEventService resources")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,616 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* File type enumeration.
|
||||
* Defines the possible types of files in the file system.
|
||||
*/
|
||||
enum class FileType {
|
||||
UNKNOWN,
|
||||
FILE,
|
||||
DIRECTORY,
|
||||
SYMBOLIC_LINK
|
||||
}
|
||||
|
||||
/**
|
||||
* File statistics information.
|
||||
* Contains metadata about a file including its type, creation time, modification time, and size.
|
||||
*
|
||||
* @property type The type of the file (file, directory, symbolic link, etc.)
|
||||
* @property ctime The creation time of the file in milliseconds since epoch
|
||||
* @property mtime The last modification time of the file in milliseconds since epoch
|
||||
* @property size The size of the file in bytes
|
||||
*/
|
||||
data class FileStat(
|
||||
val type: FileType,
|
||||
val ctime: Long,
|
||||
val mtime: Long,
|
||||
val size: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* File system provider capabilities.
|
||||
* Defines the capabilities and features supported by a file system provider.
|
||||
*
|
||||
* @property isCaseSensitive Whether the file system is case-sensitive
|
||||
* @property isReadonly Whether the file system is read-only
|
||||
* @property isPathCaseSensitive Whether file paths are case-sensitive
|
||||
* @property canHandleFileUri Whether the provider can handle file URIs
|
||||
* @property hasFileCopy Whether the provider supports file copying
|
||||
* @property hasFolderCopy Whether the provider supports folder copying
|
||||
* @property hasOpenReadWriteCloseCapability Whether the provider supports open/read/write/close operations
|
||||
* @property hasLegacyWatchCapability Whether the provider supports legacy file watching
|
||||
* @property hasDiffCapability Whether the provider supports diff operations
|
||||
* @property hasFileChangeCapability Whether the provider supports file change notifications
|
||||
*/
|
||||
data class FileSystemProviderCapabilities(
|
||||
val isCaseSensitive: Boolean,
|
||||
val isReadonly: Boolean,
|
||||
val isPathCaseSensitive: Boolean,
|
||||
val canHandleFileUri: Boolean,
|
||||
val hasFileCopy: Boolean,
|
||||
val hasFolderCopy: Boolean,
|
||||
val hasOpenReadWriteCloseCapability: Boolean,
|
||||
val hasLegacyWatchCapability: Boolean,
|
||||
val hasDiffCapability: Boolean,
|
||||
val hasFileChangeCapability: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* File overwrite options.
|
||||
* Options for controlling file overwrite behavior during write operations.
|
||||
*
|
||||
* @property overwrite Whether to overwrite existing files
|
||||
*/
|
||||
data class FileOverwriteOptions(
|
||||
val overwrite: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* File delete options.
|
||||
* Options for controlling file deletion behavior.
|
||||
*
|
||||
* @property recursive Whether to delete directories recursively
|
||||
* @property useTrash Whether to move files to trash instead of permanent deletion
|
||||
*/
|
||||
data class FileDeleteOptions(
|
||||
val recursive: Boolean,
|
||||
val useTrash: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* File change data.
|
||||
* Represents a change event in the file system.
|
||||
*
|
||||
* @property type The type of change: 1 for ADDED, 2 for UPDATED, 3 for DELETED
|
||||
* @property resource The resource that was changed, represented as a map of properties
|
||||
*/
|
||||
data class FileChangeDto(
|
||||
val type: Int, // 1: ADDED, 2: UPDATED, 3: DELETED
|
||||
val resource: Map<String, Any?>
|
||||
)
|
||||
|
||||
/**
|
||||
* Markdown string interface.
|
||||
* Represents a string that can contain markdown formatting.
|
||||
*/
|
||||
interface MarkdownString {
|
||||
val value: String
|
||||
val isTrusted: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread file system service interface.
|
||||
* Corresponds to the MainThreadFileSystemShape interface in VSCode.
|
||||
* Provides an abstraction layer for file system operations that can be executed from the main thread.
|
||||
*/
|
||||
interface MainThreadFileSystemShape : Disposable {
|
||||
/**
|
||||
* Registers a file system provider with the given handle and scheme.
|
||||
*
|
||||
* @param handle A unique identifier for the provider
|
||||
* @param scheme The URI scheme this provider handles (e.g., "file", "ftp", etc.)
|
||||
*/
|
||||
fun registerFileSystemProvider(handle: Int, scheme: String)
|
||||
|
||||
/**
|
||||
* Unregisters a file system provider.
|
||||
*
|
||||
* @param handle The handle of the provider to unregister
|
||||
*/
|
||||
fun unregisterProvider(handle: Int)
|
||||
|
||||
/**
|
||||
* Gets file status information for the specified resource.
|
||||
*
|
||||
* @param resource The URI of the file or directory to get information about
|
||||
* @return FileStat object containing file metadata
|
||||
*/
|
||||
fun stat(resource: URI): FileStat
|
||||
|
||||
/**
|
||||
* Reads directory contents.
|
||||
* Returns a list of entries in the specified directory.
|
||||
*
|
||||
* @param resource The URI of the directory to read
|
||||
* @return List of pairs, where each pair contains (filename, fileType)
|
||||
*/
|
||||
fun readdir(resource: URI): List<Pair<String, String>>
|
||||
|
||||
/**
|
||||
* Reads file content.
|
||||
* Returns the raw bytes of the specified file.
|
||||
*
|
||||
* @param uri The URI of the file to read
|
||||
* @return Byte array containing the file content
|
||||
*/
|
||||
fun readFile(uri: URI): ByteArray
|
||||
|
||||
/**
|
||||
* Writes file content.
|
||||
* Writes the provided content to the specified file.
|
||||
*
|
||||
* @param uri The URI of the file to write
|
||||
* @param content The content to write as a byte array
|
||||
* @param overwrite Whether to overwrite if the file already exists
|
||||
* @return The written content as a byte array
|
||||
*/
|
||||
fun writeFile(uri: URI, content: ByteArray, overwrite: Boolean): ByteArray
|
||||
|
||||
/**
|
||||
* Renames a file or directory.
|
||||
* Moves a file or directory from source to target location.
|
||||
*
|
||||
* @param source The URI of the source file/directory
|
||||
* @param target The URI of the target location
|
||||
* @param options Additional options for the rename operation
|
||||
*/
|
||||
fun rename(source: URI, target: URI, options: Map<String, Any>)
|
||||
|
||||
/**
|
||||
* Copies a file or directory.
|
||||
* Creates a copy of the source at the target location.
|
||||
*
|
||||
* @param source The URI of the source file/directory
|
||||
* @param target The URI of the target location
|
||||
* @param options Additional options for the copy operation
|
||||
*/
|
||||
fun copy(source: URI, target: URI, options: Map<String, Any>)
|
||||
|
||||
/**
|
||||
* Creates a directory.
|
||||
* Creates the specified directory and any necessary parent directories.
|
||||
*
|
||||
* @param uri The URI of the directory to create
|
||||
*/
|
||||
fun mkdir(uri: URI)
|
||||
|
||||
/**
|
||||
* Deletes a file or directory.
|
||||
* Removes the specified file or directory from the file system.
|
||||
*
|
||||
* @param uri The URI of the file/directory to delete
|
||||
* @param options Additional options for the delete operation
|
||||
*/
|
||||
fun delete(uri: URI, options: Map<String, Any>)
|
||||
|
||||
/**
|
||||
* Ensures activation.
|
||||
* Ensures that the file system provider for the given scheme is activated.
|
||||
*
|
||||
* @param scheme The URI scheme to ensure is activated
|
||||
*/
|
||||
fun ensureActivation(scheme: String)
|
||||
|
||||
/**
|
||||
* Listens for file system changes.
|
||||
* Processes file system change notifications from providers.
|
||||
*
|
||||
* @param handle The handle of the provider sending the change notification
|
||||
* @param resources List of file changes to process
|
||||
*/
|
||||
fun onFileSystemChange(handle: Int, resources: List<FileChangeDto>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread file system service implementation.
|
||||
* Provides implementation of file system related functionality for the IDEA platform.
|
||||
* This class implements the MainThreadFileSystemShape interface and provides
|
||||
* concrete implementations for all file system operations.
|
||||
*/
|
||||
class MainThreadFileSystem : MainThreadFileSystemShape {
|
||||
private val logger = Logger.getInstance(MainThreadFileSystem::class.java)
|
||||
|
||||
// Registered file system providers mapped by their handles
|
||||
private val providers = ConcurrentHashMap<Int, String>()
|
||||
|
||||
/**
|
||||
* Registers a file system provider with the given handle and scheme.
|
||||
* This method stores the provider information for later use.
|
||||
*
|
||||
* @param handle A unique identifier for the provider
|
||||
* @param scheme The URI scheme this provider handles
|
||||
*/
|
||||
override fun registerFileSystemProvider(handle: Int, scheme: String) {
|
||||
logger.info("Registering file system provider: handle=$handle, scheme=$scheme")
|
||||
|
||||
try {
|
||||
// Store provider information
|
||||
providers[handle] = scheme
|
||||
|
||||
// Actual implementation would need to integrate with IDEA's VFS
|
||||
// based on the scheme
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register file system provider: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a file system provider.
|
||||
* Removes the provider associated with the given handle.
|
||||
*
|
||||
* @param handle The handle of the provider to unregister
|
||||
*/
|
||||
override fun unregisterProvider(handle: Int) {
|
||||
logger.info("Unregistering file system provider: handle=$handle")
|
||||
|
||||
try {
|
||||
// Remove provider information
|
||||
providers.remove(handle)
|
||||
|
||||
// Actual implementation would need to unregister the corresponding file system provider
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to unregister file system provider: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file status information for the specified resource.
|
||||
* Retrieves metadata about a file or directory including type, timestamps, and size.
|
||||
*
|
||||
* @param resource The URI of the file or directory to get information about
|
||||
* @return FileStat object containing file metadata
|
||||
*/
|
||||
override fun stat(resource: URI): FileStat {
|
||||
logger.info("Getting file status information: $resource")
|
||||
|
||||
try {
|
||||
val path = getPathFromUriComponents(resource)
|
||||
val file = File(path)
|
||||
|
||||
if (!file.exists()) {
|
||||
throw Exception("File does not exist: $path")
|
||||
}
|
||||
|
||||
val type = when {
|
||||
file.isDirectory -> FileType.DIRECTORY
|
||||
Files.isSymbolicLink(Paths.get(file.toURI())) -> FileType.SYMBOLIC_LINK
|
||||
else -> FileType.FILE
|
||||
}
|
||||
|
||||
val ctime = file.lastModified()
|
||||
val mtime = file.lastModified()
|
||||
val size = file.length()
|
||||
|
||||
return FileStat(type, ctime, mtime, size)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to get file status information: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads directory contents.
|
||||
* Returns a list of all entries in the specified directory.
|
||||
*
|
||||
* @param resource The URI of the directory to read
|
||||
* @return List of pairs, where each pair contains (filename, fileType)
|
||||
*/
|
||||
override fun readdir(resource: URI): List<Pair<String, String>> {
|
||||
logger.info("Reading directory contents: $resource")
|
||||
|
||||
try {
|
||||
val path = getPathFromUriComponents(resource)
|
||||
val file = File(path)
|
||||
|
||||
if (!file.exists() || !file.isDirectory) {
|
||||
throw Exception("Directory does not exist or is not a directory: $path")
|
||||
}
|
||||
|
||||
// Read directory contents
|
||||
return file.listFiles()?.map {
|
||||
Pair(it.name, if (it.isDirectory) FileType.DIRECTORY.ordinal.toString() else FileType.FILE.ordinal.toString())
|
||||
} ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to read directory contents: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file content.
|
||||
* Returns the raw bytes of the specified file.
|
||||
*
|
||||
* @param uri The URI of the file to read
|
||||
* @return Byte array containing the file content
|
||||
*/
|
||||
override fun readFile(uri: URI): ByteArray {
|
||||
logger.info("Reading file content: $uri")
|
||||
|
||||
try {
|
||||
val path = getPathFromUriComponents(uri)
|
||||
val file = File(path)
|
||||
|
||||
if (!file.exists() || file.isDirectory) {
|
||||
throw Exception("File does not exist or is a directory: $path")
|
||||
}
|
||||
|
||||
// Read file content
|
||||
return file.readBytes()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to read file content: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes file content.
|
||||
* Writes the provided content to the specified file.
|
||||
*
|
||||
* @param uri The URI of the file to write
|
||||
* @param content The content to write as a byte array
|
||||
* @param overwrite Whether to overwrite if the file already exists
|
||||
* @return The written content as a byte array
|
||||
*/
|
||||
override fun writeFile(uri: URI, content: ByteArray, overwrite: Boolean): ByteArray {
|
||||
logger.info("Writing file content: $uri, content size: ${content.size} bytes")
|
||||
|
||||
try {
|
||||
val path = getPathFromUriComponents(uri)
|
||||
val file = File(path)
|
||||
|
||||
// Ensure parent directory exists
|
||||
file.parentFile?.mkdirs()
|
||||
|
||||
// Write file content
|
||||
file.writeBytes(content)
|
||||
return content
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to write file content: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file or directory.
|
||||
* Moves a file or directory from source to target location.
|
||||
*
|
||||
* @param source The URI of the source file/directory
|
||||
* @param target The URI of the target location
|
||||
* @param options Additional options for the rename operation
|
||||
*/
|
||||
override fun rename(source: URI, target: URI, options: Map<String, Any>) {
|
||||
logger.info("Renaming: $source -> $target")
|
||||
|
||||
try {
|
||||
val sourcePath = getPathFromUriComponents(source)
|
||||
val targetPath = getPathFromUriComponents(target)
|
||||
val overwrite = options["overwrite"] as? Boolean ?: false
|
||||
|
||||
val sourceFile = File(sourcePath)
|
||||
val targetFile = File(targetPath)
|
||||
|
||||
if (!sourceFile.exists()) {
|
||||
throw Exception("Source file does not exist: $sourcePath")
|
||||
}
|
||||
|
||||
if (targetFile.exists() && !overwrite) {
|
||||
throw Exception("Target file already exists and overwrite is not allowed: $targetPath")
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
targetFile.parentFile?.mkdirs()
|
||||
|
||||
// Perform rename operation
|
||||
if (!sourceFile.renameTo(targetFile)) {
|
||||
// If simple rename fails, try copy then delete
|
||||
Files.move(
|
||||
Paths.get(sourcePath),
|
||||
Paths.get(targetPath),
|
||||
if (overwrite) StandardCopyOption.REPLACE_EXISTING else StandardCopyOption.ATOMIC_MOVE
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Rename operation failed: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file or directory.
|
||||
* Creates a copy of the source at the target location.
|
||||
*
|
||||
* @param source The URI of the source file/directory
|
||||
* @param target The URI of the target location
|
||||
* @param options Additional options for the copy operation
|
||||
*/
|
||||
override fun copy(source: URI, target: URI, options: Map<String, Any>) {
|
||||
logger.info("Copying: $source -> $target")
|
||||
|
||||
try {
|
||||
val sourcePath = getPathFromUriComponents(source)
|
||||
val targetPath = getPathFromUriComponents(target)
|
||||
val overwrite = options["overwrite"] as? Boolean ?: false
|
||||
|
||||
val sourceFile = File(sourcePath)
|
||||
val targetFile = File(targetPath)
|
||||
|
||||
if (!sourceFile.exists()) {
|
||||
throw Exception("Source file does not exist: $sourcePath")
|
||||
}
|
||||
|
||||
if (targetFile.exists() && !overwrite) {
|
||||
throw Exception("Target file already exists and overwrite is not allowed: $targetPath")
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
targetFile.parentFile?.mkdirs()
|
||||
|
||||
if (sourceFile.isDirectory) {
|
||||
// Copy directory recursively
|
||||
sourceFile.copyRecursively(targetFile, overwrite)
|
||||
} else {
|
||||
// Copy file
|
||||
Files.copy(
|
||||
Paths.get(sourcePath),
|
||||
Paths.get(targetPath),
|
||||
if (overwrite) StandardCopyOption.REPLACE_EXISTING else StandardCopyOption.COPY_ATTRIBUTES
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Copy operation failed: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory.
|
||||
* Creates the specified directory and any necessary parent directories.
|
||||
*
|
||||
* @param uri The URI of the directory to create
|
||||
*/
|
||||
override fun mkdir(uri: URI) {
|
||||
logger.info("Creating directory: $uri")
|
||||
|
||||
try {
|
||||
val path = getPathFromUriComponents(uri)
|
||||
val file = File(path)
|
||||
|
||||
if (file.exists()) {
|
||||
throw Exception("File or directory already exists: $path")
|
||||
}
|
||||
|
||||
// Create directory
|
||||
if (!file.mkdirs()) {
|
||||
throw Exception("Failed to create directory: $path")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to create directory: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file or directory.
|
||||
* Removes the specified file or directory from the file system.
|
||||
*
|
||||
* @param uri The URI of the file/directory to delete
|
||||
* @param options Additional options for the delete operation
|
||||
*/
|
||||
override fun delete(uri: URI, options: Map<String, Any>) {
|
||||
logger.info("Deleting: $uri, options: $options")
|
||||
|
||||
try {
|
||||
val path = getPathFromUriComponents(uri)
|
||||
val file = File(path)
|
||||
val recursive = options["recursive"] as? Boolean ?: false
|
||||
val useTrash = options["useTrash"] as? Boolean ?: false
|
||||
|
||||
if (!file.exists()) {
|
||||
// If file doesn't exist, consider deletion successful
|
||||
return
|
||||
}
|
||||
|
||||
if (useTrash) {
|
||||
// TODO: Implement trash deletion based on platform
|
||||
// Currently performs direct deletion, should move to trash in actual implementation
|
||||
logger.warn("Trash deletion not implemented, performing direct deletion")
|
||||
}
|
||||
|
||||
if (file.isDirectory && recursive) {
|
||||
// Recursively delete directory
|
||||
file.deleteRecursively()
|
||||
} else if (file.isDirectory && !recursive) {
|
||||
throw Exception("Cannot delete non-empty directory unless recursive=true: $path")
|
||||
} else {
|
||||
// Delete file
|
||||
file.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Delete operation failed: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures activation.
|
||||
* Ensures that the file system provider for the given scheme is activated.
|
||||
*
|
||||
* @param scheme The URI scheme to ensure is activated
|
||||
*/
|
||||
override fun ensureActivation(scheme: String) {
|
||||
logger.info("Ensuring activation: $scheme")
|
||||
|
||||
try {
|
||||
// This should handle file system activation
|
||||
// Actual implementation may need to notify IDEA's VFS to refresh
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to ensure activation: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for file system changes.
|
||||
* Processes file system change notifications from providers.
|
||||
*
|
||||
* @param handle The handle of the provider sending the change notification
|
||||
* @param resources List of file changes to process
|
||||
*/
|
||||
override fun onFileSystemChange(handle: Int, resources: List<FileChangeDto>) {
|
||||
logger.info("File system change notification: handle=$handle, resources=${resources.joinToString { it.resource.toString() }}")
|
||||
|
||||
try {
|
||||
// This should handle file system change notifications
|
||||
// Actual implementation may need to notify IDEA's VFS to refresh
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to process file system change notification: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file system path from URI components.
|
||||
* Converts a URI to a local file system path.
|
||||
*
|
||||
* @param uri The URI to convert
|
||||
* @return The corresponding file system path
|
||||
*/
|
||||
private fun getPathFromUriComponents(uri: URI): String {
|
||||
return File(uri).path
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of resources.
|
||||
* Cleans up resources when this service is no longer needed.
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadFileSystem resources")
|
||||
providers.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,713 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.core.ExtensionIdentifier
|
||||
|
||||
/**
|
||||
* Language features related interface.
|
||||
* Corresponds to the MainThreadLanguageFeaturesShape interface in VSCode.
|
||||
* This interface defines the contract for language feature providers that run on the main thread.
|
||||
* It provides methods to register various language intelligence features like code completion,
|
||||
* hover information, symbol navigation, and more.
|
||||
*/
|
||||
interface MainThreadLanguageFeaturesShape : Disposable {
|
||||
/**
|
||||
* Unregisters service.
|
||||
* @param handle Provider handle
|
||||
*/
|
||||
fun unregister(handle: Int)
|
||||
|
||||
/**
|
||||
* Registers document symbol provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param label Label
|
||||
*/
|
||||
fun registerDocumentSymbolProvider(handle: Int, selector: List<Map<String, Any?>>, label: String)
|
||||
|
||||
/**
|
||||
* Registers code lens support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param eventHandle Event handle
|
||||
*/
|
||||
fun registerCodeLensSupport(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?)
|
||||
|
||||
/**
|
||||
* Emits code lens event.
|
||||
* @param eventHandle Event handle
|
||||
* @param event Event content
|
||||
*/
|
||||
fun emitCodeLensEvent(eventHandle: Int, event: Any?)
|
||||
|
||||
/**
|
||||
* Registers definition support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers declaration support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerDeclarationSupport(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers implementation support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerImplementationSupport(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers type definition support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerTypeDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers hover provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerHoverProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers evaluatable expression provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerEvaluatableExpressionProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers inline values provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param eventHandle Event handle
|
||||
*/
|
||||
fun registerInlineValuesProvider(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?)
|
||||
|
||||
/**
|
||||
* Emits inline values event.
|
||||
* @param eventHandle Event handle
|
||||
* @param event Event content
|
||||
*/
|
||||
fun emitInlineValuesEvent(eventHandle: Int, event: Any?)
|
||||
|
||||
/**
|
||||
* Registers document highlight provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers multi-document highlight provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerMultiDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers linked editing range provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerLinkedEditingRangeProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers reference support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerReferenceSupport(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers code action support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param metadata Metadata
|
||||
* @param displayName Display name
|
||||
* @param extensionID Extension ID
|
||||
* @param supportsResolve Whether to support resolve
|
||||
*/
|
||||
fun registerCodeActionSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>,
|
||||
displayName: String,
|
||||
extensionID: String,
|
||||
supportsResolve: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers paste edit provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param metadata Metadata
|
||||
*/
|
||||
fun registerPasteEditProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers document formatting support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param extensionId Extension ID
|
||||
* @param displayName Display name
|
||||
*/
|
||||
fun registerDocumentFormattingSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
displayName: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers range formatting support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param extensionId Extension ID
|
||||
* @param displayName Display name
|
||||
* @param supportRanges Whether to support ranges
|
||||
*/
|
||||
fun registerRangeFormattingSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
displayName: String,
|
||||
supportRanges: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers on-type formatting support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param autoFormatTriggerCharacters Auto-format trigger characters
|
||||
* @param extensionId Extension ID
|
||||
*/
|
||||
fun registerOnTypeFormattingSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
autoFormatTriggerCharacters: List<String>,
|
||||
extensionId: ExtensionIdentifier
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers navigate type support.
|
||||
* @param handle Provider handle
|
||||
* @param supportsResolve Whether to support resolve
|
||||
*/
|
||||
fun registerNavigateTypeSupport(handle: Int, supportsResolve: Boolean)
|
||||
|
||||
/**
|
||||
* Registers rename support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param supportsResolveInitialValues Whether to support resolve initial values
|
||||
*/
|
||||
fun registerRenameSupport(handle: Int, selector: List<Map<String, Any?>>, supportsResolveInitialValues: Boolean)
|
||||
|
||||
/**
|
||||
* Registers new symbol names provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerNewSymbolNamesProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers document semantic tokens provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param legend Legend
|
||||
* @param eventHandle Event handle
|
||||
*/
|
||||
fun registerDocumentSemanticTokensProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
legend: Map<String, Any?>,
|
||||
eventHandle: Int?
|
||||
)
|
||||
|
||||
/**
|
||||
* Emits document semantic tokens event.
|
||||
* @param eventHandle Event handle
|
||||
*/
|
||||
fun emitDocumentSemanticTokensEvent(eventHandle: Int)
|
||||
|
||||
/**
|
||||
* Registers document range semantic tokens provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param legend Legend
|
||||
*/
|
||||
fun registerDocumentRangeSemanticTokensProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
legend: Map<String, Any?>
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers completions provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param triggerCharacters Trigger characters
|
||||
* @param supportsResolveDetails Whether to support resolve details
|
||||
* @param extensionId Extension ID
|
||||
*/
|
||||
fun registerCompletionsProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
triggerCharacters: List<String>,
|
||||
supportsResolveDetails: Boolean,
|
||||
extensionId: ExtensionIdentifier
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers inline completions support.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param supportsHandleDidShowCompletionItem Whether to support handle did show completion item
|
||||
* @param extensionId Extension ID
|
||||
* @param yieldsToExtensionIds Yields to extension IDs
|
||||
* @param displayName Display name
|
||||
* @param debounceDelayMs Debounce delay in milliseconds
|
||||
*/
|
||||
fun registerInlineCompletionsSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
supportsHandleDidShowCompletionItem: Boolean,
|
||||
extensionId: String,
|
||||
yieldsToExtensionIds: List<String>,
|
||||
displayName: String?,
|
||||
debounceDelayMs: Int?
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers inline edit provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param extensionId Extension ID
|
||||
* @param displayName Display name
|
||||
*/
|
||||
fun registerInlineEditProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
displayName: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers signature help provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param metadata Metadata
|
||||
*/
|
||||
fun registerSignatureHelpProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers inlay hints provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param supportsResolve Whether to support resolve
|
||||
* @param eventHandle Event handle
|
||||
* @param displayName Display name
|
||||
*/
|
||||
fun registerInlayHintsProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
supportsResolve: Boolean,
|
||||
eventHandle: Int?,
|
||||
displayName: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* Emits inlay hints event.
|
||||
* @param eventHandle Event handle
|
||||
*/
|
||||
fun emitInlayHintsEvent(eventHandle: Int)
|
||||
|
||||
/**
|
||||
* Registers document link provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param supportsResolve Whether to support resolve
|
||||
*/
|
||||
fun registerDocumentLinkProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
supportsResolve: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Registers document color provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerDocumentColorProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers folding range provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param extensionId Extension ID
|
||||
* @param eventHandle Event handle
|
||||
*/
|
||||
fun registerFoldingRangeProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
eventHandle: Int?
|
||||
)
|
||||
|
||||
/**
|
||||
* Emits folding range event.
|
||||
* @param eventHandle Event handle
|
||||
* @param event Event content
|
||||
*/
|
||||
fun emitFoldingRangeEvent(eventHandle: Int, event: Any?)
|
||||
|
||||
/**
|
||||
* Registers selection range provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerSelectionRangeProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers call hierarchy provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerCallHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers type hierarchy provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
*/
|
||||
fun registerTypeHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Registers document on drop edit provider.
|
||||
* @param handle Provider handle
|
||||
* @param selector Document selector
|
||||
* @param metadata Metadata
|
||||
*/
|
||||
fun registerDocumentOnDropEditProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>?
|
||||
)
|
||||
|
||||
/**
|
||||
* Resolves paste file data.
|
||||
* @param handle Provider handle
|
||||
* @param requestId Request ID
|
||||
* @param dataId Data ID
|
||||
* @return File data
|
||||
*/
|
||||
fun resolvePasteFileData(handle: Int, requestId: Int, dataId: String): ByteArray
|
||||
|
||||
/**
|
||||
* Resolves document on drop file data.
|
||||
* @param handle Provider handle
|
||||
* @param requestId Request ID
|
||||
* @param dataId Data ID
|
||||
* @return File data
|
||||
*/
|
||||
fun resolveDocumentOnDropFileData(handle: Int, requestId: Int, dataId: String): ByteArray
|
||||
|
||||
/**
|
||||
* Sets language configuration.
|
||||
* @param handle Provider handle
|
||||
* @param languageId Language ID
|
||||
* @param configuration Configuration
|
||||
*/
|
||||
fun setLanguageConfiguration(handle: Int, languageId: String, configuration: Map<String, Any?>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Language features related implementation class.
|
||||
* This class implements the MainThreadLanguageFeaturesShape interface and provides
|
||||
* concrete implementations for all language feature registration methods.
|
||||
* It acts as a bridge between the extension host and the IDE's language services.
|
||||
*/
|
||||
class MainThreadLanguageFeatures : MainThreadLanguageFeaturesShape {
|
||||
private val logger = Logger.getInstance(MainThreadLanguageFeatures::class.java)
|
||||
|
||||
override fun unregister(handle: Int) {
|
||||
logger.info("Unregistering service: handle=$handle")
|
||||
}
|
||||
|
||||
override fun registerDocumentSymbolProvider(handle: Int, selector: List<Map<String, Any?>>, label: String) {
|
||||
logger.info("Registering document symbol provider: handle=$handle, selector=$selector, label=$label")
|
||||
}
|
||||
|
||||
override fun registerCodeLensSupport(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?) {
|
||||
logger.info("Registering code lens support: handle=$handle, selector=$selector, eventHandle=$eventHandle")
|
||||
}
|
||||
|
||||
override fun emitCodeLensEvent(eventHandle: Int, event: Any?) {
|
||||
logger.info("Emitting code lens event: eventHandle=$eventHandle, event=$event")
|
||||
}
|
||||
|
||||
override fun registerDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering definition support: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerDeclarationSupport(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering declaration support: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerImplementationSupport(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering implementation support: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerTypeDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering type definition support: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerHoverProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering hover provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerEvaluatableExpressionProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering evaluatable expression provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerInlineValuesProvider(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?) {
|
||||
logger.info("Registering inline values provider: handle=$handle, selector=$selector, eventHandle=$eventHandle")
|
||||
}
|
||||
|
||||
override fun emitInlineValuesEvent(eventHandle: Int, event: Any?) {
|
||||
logger.info("Emitting inline values event: eventHandle=$eventHandle, event=$event")
|
||||
}
|
||||
|
||||
override fun registerDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering document highlight provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerMultiDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering multi-document highlight provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerLinkedEditingRangeProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering linked editing range provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerReferenceSupport(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering reference support: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerCodeActionSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>,
|
||||
displayName: String,
|
||||
extensionID: String,
|
||||
supportsResolve: Boolean
|
||||
) {
|
||||
logger.info("Registering code action support: handle=$handle, selector=$selector, metadata=$metadata, displayName=$displayName, extensionID=$extensionID, supportsResolve=$supportsResolve")
|
||||
}
|
||||
|
||||
override fun registerPasteEditProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>
|
||||
) {
|
||||
logger.info("Registering paste edit provider: handle=$handle, selector=$selector, metadata=$metadata")
|
||||
}
|
||||
|
||||
override fun registerDocumentFormattingSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
displayName: String
|
||||
) {
|
||||
logger.info("Registering document formatting support: handle=$handle, selector=$selector, extensionId=${extensionId.value}, displayName=$displayName")
|
||||
}
|
||||
|
||||
override fun registerRangeFormattingSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
displayName: String,
|
||||
supportRanges: Boolean
|
||||
) {
|
||||
logger.info("Registering range formatting support: handle=$handle, selector=$selector, extensionId=${extensionId.value}, displayName=$displayName, supportRanges=$supportRanges")
|
||||
}
|
||||
|
||||
override fun registerOnTypeFormattingSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
autoFormatTriggerCharacters: List<String>,
|
||||
extensionId: ExtensionIdentifier
|
||||
) {
|
||||
logger.info("Registering on-type formatting support: handle=$handle, selector=$selector, autoFormatTriggerCharacters=$autoFormatTriggerCharacters, extensionId=${extensionId.value}")
|
||||
}
|
||||
|
||||
override fun registerNavigateTypeSupport(handle: Int, supportsResolve: Boolean) {
|
||||
logger.info("Registering navigate type support: handle=$handle, supportsResolve=$supportsResolve")
|
||||
}
|
||||
|
||||
override fun registerRenameSupport(handle: Int, selector: List<Map<String, Any?>>, supportsResolveInitialValues: Boolean) {
|
||||
logger.info("Registering rename support: handle=$handle, selector=$selector, supportsResolveInitialValues=$supportsResolveInitialValues")
|
||||
}
|
||||
|
||||
override fun registerNewSymbolNamesProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering new symbol names provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerDocumentSemanticTokensProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
legend: Map<String, Any?>,
|
||||
eventHandle: Int?
|
||||
) {
|
||||
logger.info("Registering document semantic tokens provider: handle=$handle, selector=$selector, legend=$legend, eventHandle=$eventHandle")
|
||||
}
|
||||
|
||||
override fun emitDocumentSemanticTokensEvent(eventHandle: Int) {
|
||||
logger.info("Emitting document semantic tokens event: eventHandle=$eventHandle")
|
||||
}
|
||||
|
||||
override fun registerDocumentRangeSemanticTokensProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
legend: Map<String, Any?>
|
||||
) {
|
||||
logger.info("Registering document range semantic tokens provider: handle=$handle, selector=$selector, legend=$legend")
|
||||
}
|
||||
|
||||
override fun registerCompletionsProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
triggerCharacters: List<String>,
|
||||
supportsResolveDetails: Boolean,
|
||||
extensionId: ExtensionIdentifier
|
||||
) {
|
||||
logger.info("Registering completions provider: handle=$handle, selector=$selector, triggerCharacters=$triggerCharacters, supportsResolveDetails=$supportsResolveDetails, extensionId=${extensionId.value}")
|
||||
}
|
||||
|
||||
override fun registerInlineCompletionsSupport(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
supportsHandleDidShowCompletionItem: Boolean,
|
||||
extensionId: String,
|
||||
yieldsToExtensionIds: List<String>,
|
||||
displayName: String?,
|
||||
debounceDelayMs: Int?
|
||||
) {
|
||||
logger.info("Registering inline completions support: handle=$handle, selector=$selector, supportsHandleDidShowCompletionItem=$supportsHandleDidShowCompletionItem, extensionId=$extensionId, yieldsToExtensionIds=$yieldsToExtensionIds, displayName=$displayName, debounceDelayMs=$debounceDelayMs")
|
||||
}
|
||||
|
||||
override fun registerInlineEditProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
displayName: String
|
||||
) {
|
||||
logger.info("Registering inline edit provider: handle=$handle, selector=$selector, extensionId=${extensionId.value}, displayName=$displayName")
|
||||
}
|
||||
|
||||
override fun registerSignatureHelpProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>
|
||||
) {
|
||||
logger.info("Registering signature help provider: handle=$handle, selector=$selector, metadata=$metadata")
|
||||
}
|
||||
|
||||
override fun registerInlayHintsProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
supportsResolve: Boolean,
|
||||
eventHandle: Int?,
|
||||
displayName: String?
|
||||
) {
|
||||
logger.info("Registering inlay hints provider: handle=$handle, selector=$selector, supportsResolve=$supportsResolve, eventHandle=$eventHandle, displayName=$displayName")
|
||||
}
|
||||
|
||||
override fun emitInlayHintsEvent(eventHandle: Int) {
|
||||
logger.info("Emitting inlay hints event: eventHandle=$eventHandle")
|
||||
}
|
||||
|
||||
override fun registerDocumentLinkProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
supportsResolve: Boolean
|
||||
) {
|
||||
logger.info("Registering document link provider: handle=$handle, selector=$selector, supportsResolve=$supportsResolve")
|
||||
}
|
||||
|
||||
override fun registerDocumentColorProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering document color provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerFoldingRangeProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
extensionId: ExtensionIdentifier,
|
||||
eventHandle: Int?
|
||||
) {
|
||||
logger.info("Registering folding range provider: handle=$handle, selector=$selector, extensionId=${extensionId.value}, eventHandle=$eventHandle")
|
||||
}
|
||||
|
||||
override fun emitFoldingRangeEvent(eventHandle: Int, event: Any?) {
|
||||
logger.info("Emitting folding range event: eventHandle=$eventHandle, event=$event")
|
||||
}
|
||||
|
||||
override fun registerSelectionRangeProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering selection range provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerCallHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering call hierarchy provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerTypeHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>) {
|
||||
logger.info("Registering type hierarchy provider: handle=$handle, selector=$selector")
|
||||
}
|
||||
|
||||
override fun registerDocumentOnDropEditProvider(
|
||||
handle: Int,
|
||||
selector: List<Map<String, Any?>>,
|
||||
metadata: Map<String, Any?>?
|
||||
) {
|
||||
logger.info("Registering document on drop edit provider: handle=$handle, selector=$selector, metadata=$metadata")
|
||||
}
|
||||
|
||||
override fun resolvePasteFileData(handle: Int, requestId: Int, dataId: String): ByteArray {
|
||||
logger.info("Resolving paste file data: handle=$handle, requestId=$requestId, dataId=$dataId")
|
||||
return ByteArray(0) // Return empty array, actual implementation needs to handle real file data
|
||||
}
|
||||
|
||||
override fun resolveDocumentOnDropFileData(handle: Int, requestId: Int, dataId: String): ByteArray {
|
||||
logger.info("Resolving document on drop file data: handle=$handle, requestId=$requestId, dataId=$dataId")
|
||||
return ByteArray(0) // Return empty array, actual implementation needs to handle real file data
|
||||
}
|
||||
|
||||
override fun setLanguageConfiguration(handle: Int, languageId: String, configuration: Map<String, Any?>) {
|
||||
logger.info("Setting language configuration: handle=$handle, languageId=$languageId, configuration=$configuration")
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadLanguageFeatures resources")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.plugin.SystemObjectProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Language model tools service interface.
|
||||
* Corresponds to the MainThreadLanguageModelTools interface in VSCode.
|
||||
*/
|
||||
interface MainThreadLanguageModelToolsShape : Disposable {
|
||||
/**
|
||||
* Gets all available tool list.
|
||||
*/
|
||||
fun getTools(): List<Map<String, Any?>>
|
||||
|
||||
/**
|
||||
* Invokes the specified tool.
|
||||
* @param dto Tool invocation parameters
|
||||
* @param token Cancellation token
|
||||
*/
|
||||
fun invokeTool(dto: Map<String, Any?>, token: Any? = null): Map<String, Any?>
|
||||
|
||||
/**
|
||||
* Calculates the number of tokens for the given input.
|
||||
* @param callId Call ID
|
||||
* @param input Input content
|
||||
* @param token Cancellation token
|
||||
*/
|
||||
fun countTokensForInvocation(callId: String, input: String, token: Any?): Int
|
||||
|
||||
/**
|
||||
* Registers a tool.
|
||||
* @param id Tool ID
|
||||
*/
|
||||
fun registerTool(id: String)
|
||||
|
||||
/**
|
||||
* Unregisters a tool.
|
||||
* @param name Tool name
|
||||
*/
|
||||
fun unregisterTool(name: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the language model tools service.
|
||||
*/
|
||||
class MainThreadLanguageModelTools : MainThreadLanguageModelToolsShape {
|
||||
|
||||
private val logger = logger<MainThreadLanguageModelTools>()
|
||||
private val tools = ConcurrentHashMap<String, ToolInfo>()
|
||||
|
||||
/**
|
||||
* Tool information
|
||||
*/
|
||||
private data class ToolInfo(
|
||||
val id: String,
|
||||
val registered: Boolean = true
|
||||
)
|
||||
|
||||
override fun getTools(): List<Map<String, Any?>> {
|
||||
logger.info("Get available language model tool list")
|
||||
// Return the list of registered tools
|
||||
return tools.values.filter { it.registered }.map {
|
||||
mapOf("id" to it.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun invokeTool(dto: Map<String, Any?>, token: Any?): Map<String, Any?> {
|
||||
val toolId = dto["id"] as? String ?: throw IllegalArgumentException("Tool ID cannot be empty")
|
||||
val params = dto["params"] ?: emptyMap<String, Any?>()
|
||||
|
||||
logger.info("Invoke language model tool: $toolId")
|
||||
val toolInfo = tools[toolId] ?: throw IllegalArgumentException("Tool with ID $toolId not found")
|
||||
|
||||
if (!toolInfo.registered) {
|
||||
throw IllegalStateException("Tool $toolId is not registered")
|
||||
}
|
||||
|
||||
// The actual tool should be invoked here. Currently returns a mock result.
|
||||
// In the actual implementation, it may need to call the real tool in the extension process via RPC.
|
||||
return mapOf(
|
||||
"result" to "Tool $toolId invoked successfully",
|
||||
"id" to toolId
|
||||
)
|
||||
}
|
||||
|
||||
override fun countTokensForInvocation(callId: String, input: String, token: Any?): Int {
|
||||
logger.info("Calculate token count for tool invocation $callId")
|
||||
|
||||
// The actual token count should be calculated here. Currently returns a mock result.
|
||||
// In the actual implementation, it may need to use a specific algorithm or service to calculate the token count.
|
||||
return input.length / 4 + 1 // Simple mock token calculation
|
||||
}
|
||||
|
||||
override fun registerTool(id: String) {
|
||||
logger.info("Register language model tool: $id")
|
||||
|
||||
tools[id] = ToolInfo(id, true)
|
||||
}
|
||||
|
||||
override fun unregisterTool(name: String) {
|
||||
logger.info("Unregister language model tool: $name")
|
||||
|
||||
if (tools.containsKey(name)) {
|
||||
tools[name] = tools[name]!!.copy(registered = false)
|
||||
} else {
|
||||
logger.warn("Attempting to unregister non-existent tool: $name")
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Dispose MainThreadLanguageModelTools resources")
|
||||
tools.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.util.URI
|
||||
|
||||
/**
|
||||
* Main thread logger interface.
|
||||
*/
|
||||
interface MainThreadLoggerShape : Disposable {
|
||||
/**
|
||||
* Logs messages.
|
||||
* @param file Log file URI
|
||||
* @param messages List of log messages
|
||||
*/
|
||||
fun log(file: URI, messages: List<String>)
|
||||
|
||||
/**
|
||||
* Flushes log.
|
||||
* @param file Log file URI
|
||||
*/
|
||||
fun flush(file: URI)
|
||||
|
||||
/**
|
||||
* Creates logger.
|
||||
* @param file Log file URI
|
||||
* @param options Log options
|
||||
* @return Creation result
|
||||
*/
|
||||
fun createLogger(file: URI, options: Map<String, Any?>): Any
|
||||
|
||||
/**
|
||||
* Registers logger.
|
||||
* @param logger Logger information
|
||||
* @return Registration result
|
||||
*/
|
||||
fun registerLogger(logger: Map<String, Any?>): Any
|
||||
|
||||
/**
|
||||
* Deregisters logger.
|
||||
* @param resource Resource URI
|
||||
* @return Deregistration result
|
||||
*/
|
||||
fun deregisterLogger(resource: String): Any
|
||||
|
||||
/**
|
||||
* Sets logger visibility.
|
||||
* @param resource Resource URI
|
||||
* @param visible Whether visible
|
||||
* @return Setting result
|
||||
*/
|
||||
fun setVisibility(resource: String, visible: Boolean): Any
|
||||
|
||||
}
|
||||
|
||||
class MainThreadLogger : MainThreadLoggerShape {
|
||||
private val logger = Logger.getInstance(MainThreadLogger::class.java)
|
||||
|
||||
override fun log(file: URI, messages: List<String>) {
|
||||
logger.info("Logging to file: $file")
|
||||
}
|
||||
|
||||
override fun flush(file: URI) {
|
||||
logger.info("Flushing log file: $file")
|
||||
}
|
||||
|
||||
override fun createLogger(file: URI, options: Map<String, Any?>): Any {
|
||||
logger.info("Creating logger for file: $file with options: $options")
|
||||
return Unit // Placeholder for actual logger object
|
||||
}
|
||||
|
||||
override fun registerLogger(log: Map<String, Any?>): Any {
|
||||
logger.info("Registering logger: $log")
|
||||
return Unit // Placeholder for actual registration result
|
||||
}
|
||||
|
||||
override fun deregisterLogger(resource: String): Any {
|
||||
logger.info("Deregistering logger for resource: $resource")
|
||||
return Unit // Placeholder for actual deregistration result
|
||||
}
|
||||
|
||||
override fun setVisibility(resource: String, visible: Boolean): Any {
|
||||
logger.info("Setting visibility for resource: $resource to $visible")
|
||||
return Unit // Placeholder for actual visibility result
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadLogger")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
interface MainThreadMessageServiceShape : Disposable {
|
||||
// $showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number }[]): Promise<number | undefined>;
|
||||
fun showMessage(severity: Int, message: String, options: Map<String, Any>, commands: List<Map<String, Any>>): Int?
|
||||
}
|
||||
|
||||
class MainThreadMessageService : MainThreadMessageServiceShape {
|
||||
private val logger = Logger.getInstance(MainThreadMessageService::class.java)
|
||||
|
||||
override fun showMessage(
|
||||
severity: Int,
|
||||
message: String,
|
||||
options: Map<String, Any>,
|
||||
commands: List<Map<String, Any>>
|
||||
): Int? {
|
||||
logger.info("showMessage - severity: $severity, message: $message, options: $options, commands: $commands")
|
||||
|
||||
val project = ProjectManager.getInstance().defaultProject
|
||||
val isModal = options["modal"] as? Boolean ?: false
|
||||
val detail = options["detail"] as? String
|
||||
return if (isModal) {
|
||||
showModalMessage(project, severity, message, detail, options, commands)
|
||||
} else {
|
||||
showNotificationMessage(project, severity, message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showModalMessage(
|
||||
project: com.intellij.openapi.project.Project,
|
||||
severity: Int,
|
||||
message: String,
|
||||
detail: String?,
|
||||
options: Map<String, Any>,
|
||||
commands: List<Map<String, Any>>
|
||||
): Int? {
|
||||
// Find if there's a button with isCloseAffordance=true as cancel button
|
||||
var cancelIdx = commands.indexOfFirst { it["isCloseAffordance"] == true }
|
||||
// If no cancel button, automatically add a "Cancel" button at the end
|
||||
val commandsWithCancel = if (cancelIdx < 0) {
|
||||
val cancelHandle = commands.size
|
||||
commands + mapOf("title" to "Cancel", "handle" to cancelHandle, "isCloseAffordance" to true)
|
||||
} else {
|
||||
commands
|
||||
}
|
||||
// Button title array for dialog buttons
|
||||
val buttonTitles = commandsWithCancel.map { it["title"].toString() }
|
||||
// Establish mapping from button index to handle for returning handle later
|
||||
val handleMap = commandsWithCancel.mapIndexed { idx, cmd -> idx to (cmd["handle"] as? Number)?.toInt() }.toMap()
|
||||
// Re-find the index of cancel button
|
||||
val cancelIdxFinal = commandsWithCancel.indexOfFirst { it["isCloseAffordance"] == true }
|
||||
// Assemble dialog main message and subtitle
|
||||
val dialogMessage = if (detail.isNullOrBlank()) message else "$message\n\n$detail"
|
||||
// For thread-safe retrieval of user-selected button index
|
||||
val selectedIdxRef = AtomicReference<Int>()
|
||||
// Ensure UI operations execute on EDT thread, show modal dialog
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
val selectedIdx = Messages.showDialog(
|
||||
project,
|
||||
dialogMessage,
|
||||
options["source"]?.let { (it as? Map<*, *>)?.get("label")?.toString() } ?: "roo-code",
|
||||
buttonTitles.toTypedArray(),
|
||||
if (cancelIdxFinal >= 0) cancelIdxFinal else 0,
|
||||
// Choose different icons based on severity
|
||||
when (severity) {
|
||||
1 -> Messages.getInformationIcon()
|
||||
2 -> Messages.getWarningIcon()
|
||||
3 -> Messages.getErrorIcon()
|
||||
else -> Messages.getInformationIcon()
|
||||
}
|
||||
)
|
||||
selectedIdxRef.set(selectedIdx)
|
||||
}
|
||||
// Get user-clicked button index and return corresponding handle
|
||||
val selectedIdx = selectedIdxRef.get()
|
||||
return if (selectedIdx != null && selectedIdx >= 0) handleMap[selectedIdx] else null
|
||||
}
|
||||
|
||||
private fun showNotificationMessage(
|
||||
project: com.intellij.openapi.project.Project,
|
||||
severity: Int,
|
||||
message: String
|
||||
) {
|
||||
val notificationType = when (severity) {
|
||||
1 -> NotificationType.INFORMATION
|
||||
2 -> NotificationType.WARNING
|
||||
3 -> NotificationType.ERROR
|
||||
else -> NotificationType.INFORMATION
|
||||
}
|
||||
val notification = NotificationGroupManager.getInstance().getNotificationGroup("roo-code").createNotification(
|
||||
message,
|
||||
notificationType
|
||||
)
|
||||
notification.notify(project)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("dispose")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.util.URI
|
||||
|
||||
/**
|
||||
* Main thread output service interface.
|
||||
* Corresponds to the MainThreadOutputServiceShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadOutputServiceShape : Disposable {
|
||||
/**
|
||||
* Registers output channel.
|
||||
* @param label Label
|
||||
* @param file File URI components
|
||||
* @param languageId Language ID
|
||||
* @param extensionId Extension ID
|
||||
* @return Channel ID
|
||||
*/
|
||||
suspend fun register(label: String, file: Map<String, Any>, languageId: String?, extensionId: String): String
|
||||
|
||||
/**
|
||||
* Updates output channel.
|
||||
* @param channelId Channel ID
|
||||
* @param mode Update mode
|
||||
* @param till Update to specified position
|
||||
*/
|
||||
suspend fun update(channelId: String, mode: Int, till: Int? = null)
|
||||
|
||||
/**
|
||||
* Reveals output channel.
|
||||
* @param channelId Channel ID
|
||||
* @param preserveFocus Whether to preserve focus
|
||||
*/
|
||||
suspend fun reveal(channelId: String, preserveFocus: Boolean)
|
||||
|
||||
/**
|
||||
* Closes output channel.
|
||||
* @param channelId Channel ID
|
||||
*/
|
||||
suspend fun close(channelId: String)
|
||||
|
||||
/**
|
||||
* Disposes output channel.
|
||||
* @param channelId Channel ID
|
||||
*/
|
||||
suspend fun dispose(channelId: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the main thread output service.
|
||||
*/
|
||||
class MainThreadOutputService : MainThreadOutputServiceShape {
|
||||
private val logger = Logger.getInstance(MainThreadOutputService::class.java)
|
||||
|
||||
/**
|
||||
* Registers output channel.
|
||||
* @param label Label
|
||||
* @param file File URI components
|
||||
* @param languageId Language ID
|
||||
* @param extensionId Extension ID
|
||||
* @return Channel ID
|
||||
*/
|
||||
override suspend fun register(label: String, file: Map<String, Any>, languageId: String?, extensionId: String): String {
|
||||
logger.info("Register output channel: label=$label, file=$file, extensionId=$extensionId")
|
||||
return label // Use label as channel ID
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates output channel.
|
||||
* @param channelId Channel ID
|
||||
* @param mode Update mode
|
||||
* @param till Update to specified position
|
||||
*/
|
||||
override suspend fun update(channelId: String, mode: Int, till: Int?) {
|
||||
logger.info("Update output channel: channelId=$channelId, mode=$mode, till=$till")
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals output channel.
|
||||
* @param channelId Channel ID
|
||||
* @param preserveFocus Whether to preserve focus
|
||||
*/
|
||||
override suspend fun reveal(channelId: String, preserveFocus: Boolean) {
|
||||
logger.info("Reveal output channel: channelId=$channelId, preserveFocus=$preserveFocus")
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes output channel.
|
||||
* @param channelId Channel ID
|
||||
*/
|
||||
override suspend fun close(channelId: String) {
|
||||
logger.info("Close output channel: channelId=$channelId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes output channel.
|
||||
* @param channelId Channel ID
|
||||
*/
|
||||
override suspend fun dispose(channelId: String) {
|
||||
logger.info("Disposing output channel: channelId=$channelId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all resources
|
||||
*/
|
||||
override fun dispose() {
|
||||
logger.info("Disposing all output channel resources")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Main thread search service interface.
|
||||
* Corresponds to the MainThreadSearchShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadSearchShape : Disposable {
|
||||
/**
|
||||
* Registers file search provider.
|
||||
* @param handle Provider ID
|
||||
* @param scheme Scheme
|
||||
*/
|
||||
fun registerFileSearchProvider(handle: Int, scheme: String)
|
||||
|
||||
/**
|
||||
* Registers AI text search provider.
|
||||
* @param handle Provider ID
|
||||
* @param scheme Scheme
|
||||
*/
|
||||
fun registerAITextSearchProvider(handle: Int, scheme: String)
|
||||
|
||||
/**
|
||||
* Registers text search provider.
|
||||
* @param handle Provider ID
|
||||
* @param scheme Scheme
|
||||
*/
|
||||
fun registerTextSearchProvider(handle: Int, scheme: String)
|
||||
|
||||
/**
|
||||
* Unregisters provider.
|
||||
* @param handle Provider ID
|
||||
*/
|
||||
fun unregisterProvider(handle: Int)
|
||||
|
||||
/**
|
||||
* Handles file match.
|
||||
* @param handle Provider ID
|
||||
* @param session Session ID
|
||||
* @param data List of URI components
|
||||
*/
|
||||
fun handleFileMatch(handle: Int, session: Int, data: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Handles text match.
|
||||
* @param handle Provider ID
|
||||
* @param session Session ID
|
||||
* @param data Raw file match data
|
||||
*/
|
||||
fun handleTextMatch(handle: Int, session: Int, data: List<Map<String, Any?>>)
|
||||
|
||||
/**
|
||||
* Handles telemetry data.
|
||||
* @param eventName Event name
|
||||
* @param data Telemetry data
|
||||
*/
|
||||
fun handleTelemetry(eventName: String, data: Any?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the main thread search service.
|
||||
* Provides search-related functionality for the IDEA platform.
|
||||
*/
|
||||
class MainThreadSearch : MainThreadSearchShape {
|
||||
private val logger = Logger.getInstance(MainThreadSearch::class.java)
|
||||
private val searchProviders = mutableMapOf<Int, String>()
|
||||
private val fileSessions = mutableMapOf<Int, MutableList<URI>>()
|
||||
private val textSessions = mutableMapOf<Int, MutableList<Map<String, Any?>>>()
|
||||
|
||||
override fun registerFileSearchProvider(handle: Int, scheme: String) {
|
||||
try {
|
||||
logger.info("Registering file search provider: handle=$handle, scheme=$scheme")
|
||||
searchProviders[handle] = "file:$scheme"
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register file search provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerAITextSearchProvider(handle: Int, scheme: String) {
|
||||
try {
|
||||
logger.info("Registering AI text search provider: handle=$handle, scheme=$scheme")
|
||||
searchProviders[handle] = "aitext:$scheme"
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register AI text search provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerTextSearchProvider(handle: Int, scheme: String) {
|
||||
try {
|
||||
logger.info("Registering text search provider: handle=$handle, scheme=$scheme")
|
||||
searchProviders[handle] = "text:$scheme"
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register text search provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregisterProvider(handle: Int) {
|
||||
try {
|
||||
logger.info("Unregistering provider: handle=$handle")
|
||||
searchProviders.remove(handle)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to unregister search provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleFileMatch(handle: Int, session: Int, data: List<Map<String, Any?>>) {
|
||||
try {
|
||||
logger.info("Handling file match: handle=$handle, session=$session, matches=${data.size}")
|
||||
|
||||
// Convert URI components to URI
|
||||
val uris = data.mapNotNull { uriComponents ->
|
||||
try {
|
||||
val scheme = uriComponents["scheme"] as? String ?: return@mapNotNull null
|
||||
val authority = uriComponents["authority"] as? String ?: ""
|
||||
val path = uriComponents["path"] as? String ?: return@mapNotNull null
|
||||
val query = uriComponents["query"] as? String ?: ""
|
||||
val fragment = uriComponents["fragment"] as? String ?: ""
|
||||
|
||||
URI(scheme, authority, path, query, fragment)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Failed to convert URI components: $uriComponents", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Store match results
|
||||
fileSessions.getOrPut(session) { mutableListOf() }.addAll(uris)
|
||||
|
||||
// TODO: Actual implementation should display these results in IDEA's search results panel
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to handle file match", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleTextMatch(handle: Int, session: Int, data: List<Map<String, Any?>>) {
|
||||
try {
|
||||
logger.info("Handling text match: handle=$handle, session=$session, matches=${data.size}")
|
||||
|
||||
// Store match results
|
||||
textSessions.getOrPut(session) { mutableListOf() }.addAll(data)
|
||||
|
||||
// TODO: Actual implementation should display these results in IDEA's search results panel, including highlighting matched text
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to handle text match", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleTelemetry(eventName: String, data: Any?) {
|
||||
try {
|
||||
logger.info("Handling telemetry: event=$eventName, data=$data")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to handle telemetry data", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadSearch")
|
||||
searchProviders.clear()
|
||||
fileSessions.clear()
|
||||
textSessions.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
/**
|
||||
* Secret state management service interface.
|
||||
*/
|
||||
interface MainThreadSecretStateShape : Disposable {
|
||||
/**
|
||||
* Gets the secret.
|
||||
* @param extensionId Extension ID
|
||||
* @param key Secret key identifier
|
||||
* @return Secret value, returns null if not exists
|
||||
*/
|
||||
suspend fun getPassword(extensionId: String, key: String): String?
|
||||
|
||||
/**
|
||||
* Sets the secret.
|
||||
* @param extensionId Extension ID
|
||||
* @param key Secret key identifier
|
||||
* @param value Secret value
|
||||
*/
|
||||
suspend fun setPassword(extensionId: String, key: String, value: String)
|
||||
|
||||
/**
|
||||
* Deletes the secret.
|
||||
* @param extensionId Extension ID
|
||||
* @param key Secret key identifier
|
||||
*/
|
||||
suspend fun deletePassword(extensionId: String, key: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the secret state management service.
|
||||
* Stores secrets in ~/.roo-cline/secrets.json file.
|
||||
*/
|
||||
class MainThreadSecretState : MainThreadSecretStateShape {
|
||||
private val logger = Logger.getInstance(MainThreadSecretState::class.java)
|
||||
private val gson = GsonBuilder().setPrettyPrinting().create()
|
||||
private val mutex = Mutex()
|
||||
|
||||
// Configuration file path
|
||||
private val secretsDir = File(System.getProperty("user.home"), ".roo-cline")
|
||||
private val secretsFile = File(secretsDir, "secrets.json")
|
||||
|
||||
init {
|
||||
// Ensure the directory exists
|
||||
if (!secretsDir.exists()) {
|
||||
secretsDir.mkdirs()
|
||||
logger.info("Create secret storage directory: ${secretsDir.absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPassword(extensionId: String, key: String): String? = mutex.withLock {
|
||||
try {
|
||||
if (!secretsFile.exists()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val jsonContent = secretsFile.readText()
|
||||
if (jsonContent.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val jsonObject = JsonParser.parseString(jsonContent).asJsonObject
|
||||
val extensionObject = jsonObject.getAsJsonObject(extensionId) ?: return null
|
||||
val passwordElement = extensionObject.get(key) ?: return null
|
||||
|
||||
return passwordElement.asString
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Failed to get secret: extensionId=$extensionId, key=$key", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setPassword(extensionId: String, key: String, value: String) = mutex.withLock {
|
||||
try {
|
||||
val jsonObject = if (secretsFile.exists() && secretsFile.readText().isNotBlank()) {
|
||||
JsonParser.parseString(secretsFile.readText()).asJsonObject
|
||||
} else {
|
||||
JsonObject()
|
||||
}
|
||||
|
||||
val extensionObject = jsonObject.getAsJsonObject(extensionId) ?: JsonObject().also {
|
||||
jsonObject.add(extensionId, it)
|
||||
}
|
||||
|
||||
extensionObject.addProperty(key, value)
|
||||
|
||||
val jsonString = gson.toJson(jsonObject)
|
||||
secretsFile.writeText(jsonString)
|
||||
|
||||
logger.info("Successfully set secret: extensionId=$extensionId, key=$key")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to set secret: extensionId=$extensionId, key=$key", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deletePassword(extensionId: String, key: String) = mutex.withLock {
|
||||
try {
|
||||
if (!secretsFile.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
val jsonContent = secretsFile.readText()
|
||||
if (jsonContent.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
val jsonObject = JsonParser.parseString(jsonContent).asJsonObject
|
||||
val extensionObject = jsonObject.getAsJsonObject(extensionId) ?: return
|
||||
|
||||
extensionObject.remove(key)
|
||||
|
||||
// If extension object is empty, delete the entire extension
|
||||
if (extensionObject.size() == 0) {
|
||||
jsonObject.remove(extensionId)
|
||||
}
|
||||
|
||||
val jsonString = gson.toJson(jsonObject)
|
||||
secretsFile.writeText(jsonString)
|
||||
|
||||
logger.info("Successfully deleted secret: extensionId=$extensionId, key=$key")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to delete secret: extensionId=$extensionId, key=$key", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadSecretState resources")
|
||||
// JSON file storage doesn't require special resource disposal
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.service.ExtensionStorageService
|
||||
|
||||
/**
|
||||
* Main thread storage service interface.
|
||||
*/
|
||||
interface MainThreadStorageShape : Disposable {
|
||||
/**
|
||||
* Initializes extension storage.
|
||||
* @param shared Whether shared
|
||||
* @param extensionId Extension ID
|
||||
* @return Initialization result
|
||||
*/
|
||||
fun initializeExtensionStorage(shared: Boolean, extensionId: String): Any?
|
||||
|
||||
/**
|
||||
* Sets value.
|
||||
* @param shared Whether shared
|
||||
* @param extensionId Extension ID
|
||||
* @param value Value object
|
||||
* @return Set result
|
||||
*/
|
||||
fun setValue(shared: Boolean, extensionId: String, value: Any)
|
||||
|
||||
/**
|
||||
* Registers extension storage keys for synchronization.
|
||||
* @param extension Extension ID and version
|
||||
* @param keys List of keys
|
||||
*/
|
||||
fun registerExtensionStorageKeysToSync(extension: Any, keys: List<String>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the main thread storage service.
|
||||
*/
|
||||
class MainThreadStorage : MainThreadStorageShape {
|
||||
private val logger = Logger.getInstance(MainThreadStorage::class.java)
|
||||
|
||||
override fun initializeExtensionStorage(shared: Boolean, extensionId: String): Any? {
|
||||
logger.info("Initializing extension storage: shared=$shared, extensionId=$extensionId")
|
||||
val storage = service<ExtensionStorageService>()
|
||||
return storage.getValue(extensionId)
|
||||
}
|
||||
|
||||
override fun setValue(shared: Boolean, extensionId: String, value: Any) {
|
||||
// logger.info("Setting value: shared=$shared, extensionId=$extensionId, value=$value")
|
||||
val storage = service<ExtensionStorageService>()
|
||||
storage.setValue(extensionId, value)
|
||||
}
|
||||
|
||||
override fun registerExtensionStorageKeysToSync(extension: Any, keys: List<String>) {
|
||||
val extensionId = if (extension is Map<*, *>) {
|
||||
"${extension["id"]}_${extension["version"]}"
|
||||
} else {
|
||||
"$extension"
|
||||
}
|
||||
logger.info("Registering extension storage keys for sync: extension=$extensionId, keys=$keys")
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Dispose MainThreadStorage")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import ai.roocode.jetbrains.plugin.SystemObjectProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.future.future
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Main thread task service interface.
|
||||
* Corresponds to the MainThreadTaskShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadTaskShape : Disposable {
|
||||
/**
|
||||
* Creates task ID.
|
||||
* @param task Task DTO
|
||||
* @return Task ID
|
||||
*/
|
||||
fun createTaskId(task: Map<String, Any?>): String
|
||||
|
||||
/**
|
||||
* Registers task provider.
|
||||
* @param handle Provider ID
|
||||
* @param type Task type
|
||||
*/
|
||||
fun registerTaskProvider(handle: Int, type: String)
|
||||
|
||||
/**
|
||||
* Unregisters task provider.
|
||||
* @param handle Provider ID
|
||||
*/
|
||||
fun unregisterTaskProvider(handle: Int)
|
||||
|
||||
/**
|
||||
* Fetches task list.
|
||||
* @param filter Task filter
|
||||
* @return Task list
|
||||
*/
|
||||
fun fetchTasks(filter: Map<String, Any?>?): List<Map<String, Any?>>
|
||||
|
||||
/**
|
||||
* Gets task execution instance.
|
||||
* @param value Task handle or task DTO
|
||||
* @return Task execution DTO
|
||||
*/
|
||||
fun getTaskExecution(value: Map<String, Any?>): Map<String, Any?>
|
||||
|
||||
/**
|
||||
* Executes task.
|
||||
* @param task Task handle or task DTO
|
||||
* @return Task execution DTO
|
||||
*/
|
||||
fun executeTask(task: Map<String, Any?>): Map<String, Any?>
|
||||
|
||||
/**
|
||||
* Terminates task.
|
||||
* @param id Task ID
|
||||
*/
|
||||
fun terminateTask(id: String)
|
||||
|
||||
/**
|
||||
* Registers task system.
|
||||
* @param scheme Scheme
|
||||
* @param info Task system information
|
||||
*/
|
||||
fun registerTaskSystem(scheme: String, info: Map<String, Any?>)
|
||||
|
||||
/**
|
||||
* Custom execution complete.
|
||||
* @param id Task ID
|
||||
* @param result Execution result
|
||||
*/
|
||||
fun customExecutionComplete(id: String, result: Int?)
|
||||
|
||||
/**
|
||||
* Registers supported execution types.
|
||||
* @param custom Whether supports custom execution
|
||||
* @param shell Whether supports shell execution
|
||||
* @param process Whether supports process execution
|
||||
*/
|
||||
fun registerSupportedExecutions(custom: Boolean?, shell: Boolean?, process: Boolean?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the main thread task service.
|
||||
* Provides task-related functionality for the IDEA platform.
|
||||
*/
|
||||
class MainThreadTask : MainThreadTaskShape {
|
||||
private val logger = Logger.getInstance(MainThreadTask::class.java)
|
||||
private val taskProviders = mutableMapOf<Int, String>()
|
||||
private val taskExecutions = mutableMapOf<String, Map<String, Any?>>()
|
||||
|
||||
override fun createTaskId(task: Map<String, Any?>):String {
|
||||
try {
|
||||
logger.info("Creating task ID for task: $task")
|
||||
val id = "task-${System.currentTimeMillis()}-${task.hashCode()}"
|
||||
logger.debug("Generated task ID: $id")
|
||||
return id
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to create task ID", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerTaskProvider(handle: Int, type: String) {
|
||||
try {
|
||||
logger.info("Registering task provider: handle=$handle, type=$type")
|
||||
taskProviders[handle] = type
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register task provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregisterTaskProvider(handle: Int) {
|
||||
try {
|
||||
logger.info("Unregistering task provider: handle=$handle")
|
||||
taskProviders.remove(handle)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to unregister task provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchTasks(filter: Map<String, Any?>?): List<Map<String, Any?>> {
|
||||
try {
|
||||
logger.info("Fetching tasks with filter: $filter")
|
||||
// TODO: Actual implementation should query IDEA's task system
|
||||
return emptyList()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to get tasks", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTaskExecution(value: Map<String, Any?>): Map<String, Any?> {
|
||||
try {
|
||||
val taskId = value["id"] as? String ?: value["taskId"] as? String
|
||||
logger.info("Getting task execution for task: $taskId")
|
||||
|
||||
// Create a simple task execution DTO
|
||||
return mapOf(
|
||||
"id" to (taskId ?: "unknown-task"),
|
||||
"task" to value,
|
||||
"active" to false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to get task execution", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun executeTask(task: Map<String, Any?>):Map<String, Any?> {
|
||||
try {
|
||||
val taskId = task["id"] as? String ?: task["taskId"] as? String ?: "unknown-task"
|
||||
logger.info("Executing task: $taskId")
|
||||
|
||||
// Create an executing task execution DTO
|
||||
val execution = mapOf(
|
||||
"id" to taskId,
|
||||
"task" to task,
|
||||
"active" to true
|
||||
)
|
||||
|
||||
// Store task execution information
|
||||
taskExecutions[taskId] = execution
|
||||
return execution
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to execute task", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun terminateTask(id: String) {
|
||||
try {
|
||||
logger.info("Terminating task: $id")
|
||||
taskExecutions.remove(id)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to terminate task", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerTaskSystem(scheme: String, info: Map<String, Any?>) {
|
||||
try {
|
||||
logger.info("Registering task system: scheme=$scheme, info=$info")
|
||||
// Register task system
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register task system", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun customExecutionComplete(id: String, result: Int?) {
|
||||
try {
|
||||
logger.info("Custom execution complete for task: $id with result: $result")
|
||||
// Update task execution status
|
||||
taskExecutions[id]?.let { execution ->
|
||||
taskExecutions[id] = execution + ("active" to false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to update custom execution completion status", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerSupportedExecutions(custom: Boolean?, shell: Boolean?, process: Boolean?) {
|
||||
try {
|
||||
logger.info("Registering supported executions: custom=$custom, shell=$shell, process=$process")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to register supported execution types", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadTask")
|
||||
taskProviders.clear()
|
||||
taskExecutions.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* Main thread telemetry service interface.
|
||||
*/
|
||||
interface MainThreadTelemetryShape : Disposable {
|
||||
/**
|
||||
* Logs public event.
|
||||
* @param eventName Event name
|
||||
* @param data Event data
|
||||
*/
|
||||
fun publicLog(eventName: String, data: Any?)
|
||||
|
||||
/**
|
||||
* Logs public event (supports categorized events).
|
||||
* @param eventName Event name
|
||||
* @param data Event data
|
||||
*/
|
||||
fun publicLog2(eventName: String, data: Any?)
|
||||
}
|
||||
|
||||
class MainThreadTelemetry : MainThreadTelemetryShape {
|
||||
private val logger = Logger.getInstance(MainThreadTelemetry::class.java)
|
||||
|
||||
override fun publicLog(eventName: String, data: Any?) {
|
||||
logger.info("[Telemetry] $eventName: $data")
|
||||
}
|
||||
|
||||
override fun publicLog2(eventName: String, data: Any?) {
|
||||
logger.info("[Telemetry] $eventName: $data")
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Dispose MainThreadTelemetry")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ai.roocode.jetbrains.core.PluginContext
|
||||
import ai.roocode.jetbrains.terminal.TerminalInstance
|
||||
import ai.roocode.jetbrains.terminal.TerminalInstanceManager
|
||||
import ai.roocode.jetbrains.terminal.TerminalConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
|
||||
/**
|
||||
* Main thread terminal service interface.
|
||||
* Corresponds to the MainThreadTerminalServiceShape interface in VSCode.
|
||||
*/
|
||||
interface MainThreadTerminalServiceShape : Disposable {
|
||||
/**
|
||||
* Creates terminal.
|
||||
* @param extHostTerminalId Extension host terminal ID
|
||||
* @param config Terminal launch configuration
|
||||
*/
|
||||
suspend fun createTerminal(extHostTerminalId: String, config: Map<String, Any?>)
|
||||
|
||||
/**
|
||||
* Disposes terminal resources.
|
||||
* @param id Terminal identifier (can be String or Number)
|
||||
*/
|
||||
fun dispose(id: Any)
|
||||
|
||||
/**
|
||||
* Hides terminal.
|
||||
* @param id Terminal identifier (can be String or Number)
|
||||
*/
|
||||
fun hide(id: Any)
|
||||
|
||||
/**
|
||||
* Sends text to terminal.
|
||||
* @param id Terminal identifier (can be String or Number)
|
||||
* @param text Text to send
|
||||
* @param shouldExecute Whether to execute
|
||||
*/
|
||||
fun sendText(id: Any, text: String, shouldExecute: Boolean?)
|
||||
|
||||
/**
|
||||
* Shows terminal.
|
||||
* @param id Terminal identifier (can be String or Number)
|
||||
* @param preserveFocus Whether to preserve focus
|
||||
*/
|
||||
fun show(id: Any, preserveFocus: Boolean?)
|
||||
|
||||
/**
|
||||
* Registers process support.
|
||||
* @param isSupported Whether supported
|
||||
*/
|
||||
fun registerProcessSupport(isSupported: Boolean)
|
||||
|
||||
/**
|
||||
* Registers profile provider.
|
||||
* @param id Profile provider ID
|
||||
* @param extensionIdentifier Extension identifier
|
||||
*/
|
||||
fun registerProfileProvider(id: String, extensionIdentifier: String)
|
||||
|
||||
/**
|
||||
* Unregisters profile provider.
|
||||
* @param id Profile provider ID
|
||||
*/
|
||||
fun unregisterProfileProvider(id: String)
|
||||
|
||||
/**
|
||||
* Registers completion provider.
|
||||
* @param id Completion provider ID
|
||||
* @param extensionIdentifier Extension identifier
|
||||
* @param triggerCharacters List of trigger characters
|
||||
*/
|
||||
fun registerCompletionProvider(id: String, extensionIdentifier: String, vararg triggerCharacters: String)
|
||||
|
||||
/**
|
||||
* Unregisters completion provider.
|
||||
* @param id Completion provider ID
|
||||
*/
|
||||
fun unregisterCompletionProvider(id: String)
|
||||
|
||||
/**
|
||||
* Registers quick fix provider.
|
||||
* @param id Quick fix provider ID
|
||||
* @param extensionIdentifier Extension identifier
|
||||
*/
|
||||
fun registerQuickFixProvider(id: String, extensionIdentifier: String)
|
||||
|
||||
/**
|
||||
* Unregisters quick fix provider.
|
||||
* @param id Quick fix provider ID
|
||||
*/
|
||||
fun unregisterQuickFixProvider(id: String)
|
||||
|
||||
/**
|
||||
* Set environment variable collection
|
||||
* @param extensionIdentifier Extension identifier
|
||||
* @param persistent Whether to persist
|
||||
* @param collection Serializable environment variable collection
|
||||
* @param descriptionMap Serializable environment description mapping
|
||||
*/
|
||||
fun setEnvironmentVariableCollection(
|
||||
extensionIdentifier: String,
|
||||
persistent: Boolean,
|
||||
collection: Map<String, Any?>?,
|
||||
descriptionMap: Map<String, Any?>
|
||||
)
|
||||
|
||||
/**
|
||||
* Start sending data events
|
||||
*/
|
||||
fun startSendingDataEvents()
|
||||
|
||||
/**
|
||||
* Stop sending data events
|
||||
*/
|
||||
fun stopSendingDataEvents()
|
||||
|
||||
/**
|
||||
* Start sending command events
|
||||
*/
|
||||
fun startSendingCommandEvents()
|
||||
|
||||
/**
|
||||
* Stop sending command events
|
||||
*/
|
||||
fun stopSendingCommandEvents()
|
||||
|
||||
/**
|
||||
* Start link provider
|
||||
*/
|
||||
fun startLinkProvider()
|
||||
|
||||
/**
|
||||
* Stop link provider
|
||||
*/
|
||||
fun stopLinkProvider()
|
||||
|
||||
/**
|
||||
* Send process data
|
||||
* @param terminalId Terminal ID
|
||||
* @param data Data
|
||||
*/
|
||||
fun sendProcessData(terminalId: Int, data: String)
|
||||
|
||||
/**
|
||||
* Send process ready
|
||||
* @param terminalId Terminal ID
|
||||
* @param pid Process ID
|
||||
* @param cwd Current working directory
|
||||
* @param windowsPty Windows PTY information
|
||||
*/
|
||||
fun sendProcessReady(
|
||||
terminalId: Int,
|
||||
pid: Int,
|
||||
cwd: String,
|
||||
windowsPty: Map<String, Any?>?
|
||||
)
|
||||
|
||||
/**
|
||||
* Send process property
|
||||
* @param terminalId Terminal ID
|
||||
* @param property Process property
|
||||
*/
|
||||
fun sendProcessProperty(terminalId: Int, property: Map<String, Any?>)
|
||||
|
||||
/**
|
||||
* Send process exit
|
||||
* @param terminalId Terminal ID
|
||||
* @param exitCode Exit code
|
||||
*/
|
||||
fun sendProcessExit(terminalId: Int, exitCode: Int?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread terminal service implementation class
|
||||
* Provides implementation of IDEA platform terminal-related functionality
|
||||
*/
|
||||
class MainThreadTerminalService(private val project: Project) : MainThreadTerminalServiceShape {
|
||||
private val logger = Logger.getInstance(MainThreadTerminalService::class.java)
|
||||
|
||||
// Use terminal instance manager
|
||||
private val terminalManager = project.service<TerminalInstanceManager>()
|
||||
|
||||
// Coroutine scope - use IO dispatcher to avoid Main Dispatcher issues
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override suspend fun createTerminal(extHostTerminalId: String, config: Map<String, Any?>) {
|
||||
logger.info("🚀 Creating terminal: $extHostTerminalId, config: $config")
|
||||
|
||||
try {
|
||||
// Check if terminal already exists
|
||||
if (terminalManager.containsTerminal(extHostTerminalId)) {
|
||||
logger.warn("Terminal already exists: $extHostTerminalId")
|
||||
return
|
||||
}
|
||||
|
||||
// Get RPC protocol instance
|
||||
val pluginContext = PluginContext.getInstance(project)
|
||||
val rpcProtocol = pluginContext.getRPCProtocol()
|
||||
if (rpcProtocol == null) {
|
||||
logger.error("❌ Unable to get RPC protocol instance, terminal creation failed: $extHostTerminalId")
|
||||
throw IllegalStateException("RPC protocol not initialized")
|
||||
}
|
||||
logger.info("✅ Got RPC protocol instance: ${rpcProtocol.javaClass.simpleName}")
|
||||
|
||||
// Allocate numeric ID
|
||||
val numericId = terminalManager.allocateNumericId()
|
||||
logger.info("🔢 Allocated terminal numeric ID: $numericId")
|
||||
|
||||
// Create terminal instance
|
||||
val terminalConfig = TerminalConfig.fromMap(config)
|
||||
val terminalInstance = TerminalInstance(extHostTerminalId, numericId, project, terminalConfig, rpcProtocol)
|
||||
|
||||
// Initialize terminal
|
||||
terminalInstance.initialize()
|
||||
|
||||
// Register to manager
|
||||
terminalManager.registerTerminal(extHostTerminalId, terminalInstance)
|
||||
|
||||
logger.info("✅ Terminal created successfully: $extHostTerminalId (numericId: $numericId)")
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to create terminal: $extHostTerminalId", e)
|
||||
// Clean up possibly created resources
|
||||
terminalManager.unregisterTerminal(extHostTerminalId)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose(id: Any) {
|
||||
try {
|
||||
logger.info("🧹 Destroying terminal: $id")
|
||||
|
||||
val terminalInstance = terminalManager.unregisterTerminal(id.toString())
|
||||
if (terminalInstance != null) {
|
||||
terminalInstance.dispose()
|
||||
logger.info("✅ Terminal destroyed: $id")
|
||||
} else {
|
||||
logger.warn("Terminal does not exist: $id")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to destroy terminal: $id", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hide(id: Any) {
|
||||
try {
|
||||
logger.info("🙈 Hiding terminal: $id")
|
||||
|
||||
val terminalInstance = getTerminalInstance(id)
|
||||
if (terminalInstance != null) {
|
||||
terminalInstance.hide()
|
||||
logger.info("✅ Terminal hidden: $id")
|
||||
} else {
|
||||
logger.warn("Terminal does not exist: $id")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to hide terminal: $id", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendText(id: Any, text: String, shouldExecute: Boolean?) {
|
||||
try {
|
||||
logger.debug("📤 Sending text to terminal $id: $text (execute: $shouldExecute)")
|
||||
|
||||
val terminalInstance = getTerminalInstance(id)
|
||||
if (terminalInstance != null) {
|
||||
terminalInstance.sendText(text, shouldExecute ?: false)
|
||||
logger.debug("✅ Text sent to terminal: $id")
|
||||
} else {
|
||||
logger.warn("Terminal does not exist: $id")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to send text to terminal: $id", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun show(id: Any, preserveFocus: Boolean?) {
|
||||
try {
|
||||
logger.info("👁️ Showing terminal: $id (preserve focus: $preserveFocus)")
|
||||
|
||||
val terminalInstance = getTerminalInstance(id)
|
||||
if (terminalInstance != null) {
|
||||
terminalInstance.show(preserveFocus ?: true)
|
||||
logger.info("✅ Terminal shown: $id")
|
||||
} else {
|
||||
logger.warn("Terminal does not exist: $id")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to show terminal: $id", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerProcessSupport(isSupported: Boolean) {
|
||||
logger.info("📋 Registering process support: $isSupported")
|
||||
// In IDEA, process support is built-in, mainly used for logging state here
|
||||
}
|
||||
|
||||
override fun registerProfileProvider(id: String, extensionIdentifier: String) {
|
||||
logger.info("📋 Registering profile provider: $id (extension: $extensionIdentifier)")
|
||||
// TODO: Implement profile provider registration logic
|
||||
}
|
||||
|
||||
override fun unregisterProfileProvider(id: String) {
|
||||
logger.info("📋 Unregistering profile provider: $id")
|
||||
// TODO: Implement profile provider unregistration logic
|
||||
}
|
||||
|
||||
override fun registerCompletionProvider(id: String, extensionIdentifier: String, vararg triggerCharacters: String) {
|
||||
logger.info("📋 Registering completion provider: $id (extension: $extensionIdentifier, trigger characters: ${triggerCharacters.joinToString()})")
|
||||
// TODO: Implement completion provider registration logic
|
||||
}
|
||||
|
||||
override fun unregisterCompletionProvider(id: String) {
|
||||
logger.info("📋 Unregistering completion provider: $id")
|
||||
// TODO: Implement completion provider unregistration logic
|
||||
}
|
||||
|
||||
override fun registerQuickFixProvider(id: String, extensionIdentifier: String) {
|
||||
logger.info("📋 Registering quick fix provider: $id (extension: $extensionIdentifier)")
|
||||
// TODO: Implement quick fix provider registration logic
|
||||
}
|
||||
|
||||
override fun unregisterQuickFixProvider(id: String) {
|
||||
logger.info("📋 Unregistering quick fix provider: $id")
|
||||
// TODO: Implement quick fix provider unregistration logic
|
||||
}
|
||||
|
||||
override fun setEnvironmentVariableCollection(
|
||||
extensionIdentifier: String,
|
||||
persistent: Boolean,
|
||||
collection: Map<String, Any?>?,
|
||||
descriptionMap: Map<String, Any?>
|
||||
) {
|
||||
logger.info("📋 Setting environment variable collection: $extensionIdentifier (persistent: $persistent)")
|
||||
// TODO: Implement environment variable collection setting logic
|
||||
}
|
||||
|
||||
override fun startSendingDataEvents() {
|
||||
logger.info("📋 Starting to send data events")
|
||||
// TODO: Implement data event sending logic
|
||||
}
|
||||
|
||||
override fun stopSendingDataEvents() {
|
||||
logger.info("📋 Stopping data event sending")
|
||||
// TODO: Implement stopping data event sending logic
|
||||
}
|
||||
|
||||
override fun startSendingCommandEvents() {
|
||||
logger.info("📋 Starting to send command events")
|
||||
// TODO: Implement command event sending logic
|
||||
}
|
||||
|
||||
override fun stopSendingCommandEvents() {
|
||||
logger.info("📋 Stopping command event sending")
|
||||
// TODO: Implement stopping command event sending logic
|
||||
}
|
||||
|
||||
override fun startLinkProvider() {
|
||||
logger.info("📋 Starting link provider")
|
||||
// TODO: Implement link provider startup logic
|
||||
}
|
||||
|
||||
override fun stopLinkProvider() {
|
||||
logger.info("📋 Stopping link provider")
|
||||
// TODO: Implement link provider stopping logic
|
||||
}
|
||||
|
||||
override fun sendProcessData(terminalId: Int, data: String) {
|
||||
logger.debug("Send process data to terminal $terminalId")
|
||||
// Send process data to terminal
|
||||
}
|
||||
|
||||
override fun sendProcessReady(terminalId: Int, pid: Int, cwd: String, windowsPty: Map<String, Any?>?) {
|
||||
logger.info("Send process ready: terminal=$terminalId, pid=$pid, cwd=$cwd")
|
||||
// Send process ready information
|
||||
}
|
||||
|
||||
override fun sendProcessProperty(terminalId: Int, property: Map<String, Any?>) {
|
||||
logger.debug("📋 Sending process property: terminal=$terminalId")
|
||||
// TODO: Notify extension host of process property changes
|
||||
}
|
||||
|
||||
override fun sendProcessExit(terminalId: Int, exitCode: Int?) {
|
||||
logger.info("📋 Sending process exit: terminal=$terminalId, exit code=$exitCode")
|
||||
// TODO: Notify extension host of process exit
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terminal instance (by string ID or numeric ID)
|
||||
*/
|
||||
fun getTerminalInstance(id: Any): TerminalInstance? {
|
||||
return when (id) {
|
||||
is String -> terminalManager.getTerminalInstance(id)
|
||||
is Number -> terminalManager.getTerminalInstance(id.toInt())
|
||||
else -> {
|
||||
logger.warn("Unsupported ID type: ${id.javaClass.name}, attempting to convert to string")
|
||||
terminalManager.getTerminalInstance(id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all terminal instances
|
||||
*/
|
||||
fun getAllTerminals(): Collection<TerminalInstance> {
|
||||
return terminalManager.getAllTerminals()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("🧹 Disposing main thread terminal service")
|
||||
|
||||
try {
|
||||
// Cancel coroutine scope
|
||||
scope.cancel()
|
||||
|
||||
// Terminal instance manager will automatically handle cleanup of all terminals
|
||||
// No manual cleanup needed here as TerminalInstanceManager is project-level service
|
||||
|
||||
logger.info("✅ Main thread terminal service disposed")
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to dispose main thread terminal service", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ai.roocode.jetbrains.terminal.TerminalInstanceManager
|
||||
|
||||
interface MainThreadTerminalShellIntegrationShape : Disposable {
|
||||
fun executeCommand(terminalId: Int, commandLine: String)
|
||||
}
|
||||
|
||||
class MainThreadTerminalShellIntegration(
|
||||
private val project: Project
|
||||
) : MainThreadTerminalShellIntegrationShape {
|
||||
private val logger = Logger.getInstance(MainThreadTerminalShellIntegration::class.java)
|
||||
|
||||
private val terminalManager = project.service<TerminalInstanceManager>()
|
||||
|
||||
override fun executeCommand(terminalId: Int, commandLine: String) {
|
||||
logger.info("🚀 Executing Shell Integration command: terminalId=$terminalId, commandLine='$commandLine'")
|
||||
|
||||
try {
|
||||
// Get terminal instance by numeric ID
|
||||
val terminalInstance = terminalManager.getTerminalInstance(terminalId)
|
||||
|
||||
if (terminalInstance == null) {
|
||||
logger.warn("❌ Terminal instance not found: terminalId=$terminalId")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("✅ Found terminal instance: ${terminalInstance.extHostTerminalId}")
|
||||
|
||||
// Execute command in terminal
|
||||
terminalInstance.sendText(commandLine, shouldExecute = true)
|
||||
|
||||
logger.info("✅ Command sent to terminal: terminalId=$terminalId, command='$commandLine'")
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("❌ Failed to execute Shell Integration command: terminalId=$terminalId, command='$commandLine'", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("🧹 Destroying MainThreadTerminalShellIntegration")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import ai.roocode.jetbrains.editor.EditorAndDocManager
|
||||
import ai.roocode.jetbrains.editor.Range
|
||||
import ai.roocode.jetbrains.editor.createURI
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Main thread text editor interface
|
||||
*/
|
||||
interface MainThreadTextEditorsShape : Disposable {
|
||||
/**
|
||||
* Try to show text document
|
||||
* @param resource Resource URI
|
||||
* @param options Display options
|
||||
* @return Editor ID or null
|
||||
*/
|
||||
suspend fun tryShowTextDocument(resource: Map<String,Any?>, options: Any?): Any?
|
||||
|
||||
/**
|
||||
* Register text editor decoration type
|
||||
* @param extensionId Extension ID
|
||||
* @param key Decoration type key
|
||||
* @param options Decoration rendering options
|
||||
*/
|
||||
fun registerTextEditorDecorationType(extensionId: Map<String, String>, key: String, options: Any)
|
||||
|
||||
/**
|
||||
* Remove text editor decoration type
|
||||
* @param key Decoration type key
|
||||
*/
|
||||
fun removeTextEditorDecorationType(key: String)
|
||||
|
||||
/**
|
||||
* Try to show editor
|
||||
* @param id Editor ID
|
||||
* @param position Position
|
||||
* @return Operation result
|
||||
*/
|
||||
fun tryShowEditor(id: String, position: Any?): Any
|
||||
|
||||
/**
|
||||
* Try to hide editor
|
||||
* @param id Editor ID
|
||||
* @return Operation result
|
||||
*/
|
||||
fun tryHideEditor(id: String): Any
|
||||
|
||||
/**
|
||||
* Try to set options
|
||||
* @param id Editor ID
|
||||
* @param options Configuration updates
|
||||
* @return Operation result
|
||||
*/
|
||||
fun trySetOptions(id: String, options: Any): Any
|
||||
|
||||
/**
|
||||
* Try to set decorations
|
||||
* @param id Editor ID
|
||||
* @param key Decoration type key
|
||||
* @param ranges Decoration ranges
|
||||
* @return Operation result
|
||||
*/
|
||||
fun trySetDecorations(id: String, key: String, ranges: List<Any>): Any
|
||||
|
||||
/**
|
||||
* Try to quickly set decorations
|
||||
* @param id Editor ID
|
||||
* @param key Decoration type key
|
||||
* @param ranges Decoration ranges array
|
||||
* @return Operation result
|
||||
*/
|
||||
fun trySetDecorationsFast(id: String, key: String, ranges: List<Any>): Any
|
||||
|
||||
/**
|
||||
* Try to reveal range
|
||||
* @param id Editor ID
|
||||
* @param range Display range
|
||||
* @param revealType Display type
|
||||
* @return Operation result
|
||||
*/
|
||||
fun tryRevealRange(id: String, range: Map<String,Any?>, revealType: Int): Any
|
||||
|
||||
/**
|
||||
* Try to set selections
|
||||
* @param id Editor ID
|
||||
* @param selections Selections array
|
||||
* @return Operation result
|
||||
*/
|
||||
fun trySetSelections(id: String, selections: List<Any>): Any
|
||||
|
||||
/**
|
||||
* Try to apply edits
|
||||
* @param id Editor ID
|
||||
* @param modelVersionId Model version ID
|
||||
* @param edits Edit operations
|
||||
* @param opts Apply options
|
||||
* @return Whether successful
|
||||
*/
|
||||
fun tryApplyEdits(id: String, modelVersionId: Int, edits: List<Any>, opts: Any?): Boolean
|
||||
|
||||
/**
|
||||
* Try to insert snippet
|
||||
* @param id Editor ID
|
||||
* @param modelVersionId Model version ID
|
||||
* @param template Code snippet template
|
||||
* @param selections Selection ranges
|
||||
* @param opts Undo options
|
||||
* @return Whether successful
|
||||
*/
|
||||
fun tryInsertSnippet(id: String, modelVersionId: Int, template: String, selections: List<Any>, opts: Any?): Boolean
|
||||
|
||||
/**
|
||||
* Get diff information
|
||||
* @param id Editor ID
|
||||
* @return Diff information
|
||||
*/
|
||||
fun getDiffInformation(id: String): Any?
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread text editor implementation
|
||||
*/
|
||||
class MainThreadTextEditors(var project: Project) : MainThreadTextEditorsShape {
|
||||
private val logger = Logger.getInstance(MainThreadTextEditors::class.java)
|
||||
|
||||
override suspend fun tryShowTextDocument(resource: Map<String, Any?>, options: Any?): Any? {
|
||||
logger.info("Trying to show text document: resource=$resource, options=$options")
|
||||
val path = resource["path"] as String? ?: ""
|
||||
|
||||
val vfs = LocalFileSystem.getInstance()
|
||||
vfs.refreshIoFiles(listOf(File(path)))
|
||||
val resourceURI = createURI(resource)
|
||||
val editorHandle = project.getService(EditorAndDocManager::class.java).openEditor(resourceURI)
|
||||
logger.info("Trying to show text document: resource=$resource execution completed" )
|
||||
return editorHandle.id
|
||||
}
|
||||
|
||||
override fun registerTextEditorDecorationType(extensionId: Map<String, String>, key: String, options: Any) {
|
||||
logger.info("Registering text editor decoration type: extensionId=$extensionId, key=$key, options=$options")
|
||||
}
|
||||
|
||||
override fun removeTextEditorDecorationType(key: String) {
|
||||
logger.info("Removing text editor decoration type: $key")
|
||||
}
|
||||
|
||||
override fun tryShowEditor(id: String, position: Any?): Any {
|
||||
logger.info("Trying to show editor: id=$id, position=$position")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun tryHideEditor(id: String): Any {
|
||||
logger.info("Trying to hide editor: $id")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun trySetOptions(id: String, options: Any): Any {
|
||||
logger.info("Try to set options: id=$id, options=$options")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun trySetDecorations(id: String, key: String, ranges: List<Any>): Any {
|
||||
logger.info("Try to set decorations: id=$id, key=$key, ranges=${ranges.size}")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun trySetDecorationsFast(id: String, key: String, ranges: List<Any>): Any {
|
||||
logger.info("Try to quickly set decorations: id=$id, key=$key, ranges=${ranges.size}")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun tryRevealRange(id: String, range: Map<String,Any?>, revealType: Int): Any {
|
||||
logger.info("Try to reveal range: id=$id, range=$range, revealType=$revealType")
|
||||
val handle = project.getService(EditorAndDocManager::class.java).getEditorHandleById(id)
|
||||
handle?.let {
|
||||
val rang = createRanges(range)
|
||||
handle.revealRange(rang)
|
||||
}
|
||||
return Unit
|
||||
}
|
||||
|
||||
private fun createRanges(range: Map<String,Any?>): Range {
|
||||
val startLineNumber = (range["startLineNumber"] as? Number)?.toInt() ?: 0
|
||||
val startColumn = (range["startColumn"] as? Number)?.toInt() ?: 0
|
||||
val endLineNumber = (range["endLineNumber"] as? Number)?.toInt() ?: startLineNumber
|
||||
val endColumn = (range["endColumn"] as? Number)?.toInt() ?: startColumn
|
||||
return Range(startLineNumber, startColumn, endLineNumber, endColumn)
|
||||
}
|
||||
|
||||
override fun trySetSelections(id: String, selections: List<Any>): Any {
|
||||
logger.info("Try to set selections: id=$id, selections=$selections")
|
||||
return Unit
|
||||
}
|
||||
|
||||
override fun tryApplyEdits(id: String, modelVersionId: Int, edits: List<Any>, opts: Any?): Boolean {
|
||||
logger.info("Try to apply edits: id=$id, modelVersionId=$modelVersionId, edits=$edits, opts=$opts")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun tryInsertSnippet(id: String, modelVersionId: Int, template: String, selections: List<Any>, opts: Any?): Boolean {
|
||||
logger.info("Try to insert snippet: id=$id, modelVersionId=$modelVersionId, template=$template, selections=$selections, opts=$opts")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getDiffInformation(id: String): Any? {
|
||||
logger.info("Get diff information: $id")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Dispose MainThreadTextEditors")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
/**
|
||||
* URL handling related interface
|
||||
*/
|
||||
interface MainThreadUrlsShape : Disposable {
|
||||
/**
|
||||
* Register URI handler
|
||||
* @param handle Handler identifier
|
||||
* @param extensionId Extension ID
|
||||
* @param extensionDisplayName Extension display name
|
||||
* @return Execution result
|
||||
*/
|
||||
suspend fun registerUriHandler(handle: Int, extensionId: Map<String, String>, extensionDisplayName: String): Any
|
||||
|
||||
/**
|
||||
* Unregister URI handler
|
||||
* @param handle Handler identifier
|
||||
* @return Execution result
|
||||
*/
|
||||
suspend fun unregisterUriHandler(handle: Int): Any
|
||||
|
||||
/**
|
||||
* Create application URI
|
||||
* @param uri URI components
|
||||
* @return Created URI components
|
||||
*/
|
||||
suspend fun createAppUri(uri: Map<String, Any?>): Map<String, Any?>
|
||||
}
|
||||
|
||||
class MainThreadUrls : MainThreadUrlsShape {
|
||||
private val logger = Logger.getInstance(MainThreadUrls::class.java)
|
||||
|
||||
override suspend fun registerUriHandler(handle: Int, extensionId: Map<String, String>, extensionDisplayName: String): Any {
|
||||
logger.info("Registering URI handler: handle=$handle, extensionId=$extensionId, displayName=$extensionDisplayName")
|
||||
return CompletableDeferred<Unit>().also { it.complete(Unit) }.await()
|
||||
}
|
||||
|
||||
override suspend fun unregisterUriHandler(handle: Int): Any {
|
||||
logger.info("Unregistering URI handler: handle=$handle")
|
||||
return CompletableDeferred<Unit>().also { it.complete(Unit) }.await()
|
||||
}
|
||||
|
||||
override suspend fun createAppUri(uri: Map<String, Any?>): Map<String, Any?> {
|
||||
logger.info("Creating application URI: uri=$uri")
|
||||
// Simply return original URI
|
||||
return uri
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadUrls resources")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import ai.roocode.jetbrains.events.WebviewViewProviderData
|
||||
import ai.roocode.jetbrains.webview.WebViewManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Webview view related interface
|
||||
*/
|
||||
interface MainThreadWebviewViewsShape : Disposable {
|
||||
/**
|
||||
* Register Webview view provider
|
||||
* @param extension Webview extension description
|
||||
* @param viewType View type
|
||||
* @param options Option configuration
|
||||
*/
|
||||
fun registerWebviewViewProvider(
|
||||
extension: Map<String, Any?>,
|
||||
viewType: String,
|
||||
options: Map<String, Any?>
|
||||
)
|
||||
|
||||
/**
|
||||
* Unregister Webview view provider
|
||||
* @param viewType View type
|
||||
*/
|
||||
fun unregisterWebviewViewProvider(viewType: String)
|
||||
|
||||
/**
|
||||
* Set Webview view title
|
||||
* @param handle Webview handle
|
||||
* @param value Title value
|
||||
*/
|
||||
fun setWebviewViewTitle(handle: String, value: String?)
|
||||
|
||||
/**
|
||||
* Set Webview view description
|
||||
* @param handle Webview handle
|
||||
* @param value Description content
|
||||
*/
|
||||
fun setWebviewViewDescription(handle: String, value: String?)
|
||||
|
||||
/**
|
||||
* Set Webview view badge
|
||||
* @param handle Webview handle
|
||||
* @param badge Badge information
|
||||
*/
|
||||
fun setWebviewViewBadge(handle: String, badge: Map<String, Any?>?)
|
||||
|
||||
/**
|
||||
* Show Webview view
|
||||
* @param handle Webview handle
|
||||
* @param preserveFocus Whether to preserve focus
|
||||
*/
|
||||
fun show(handle: String, preserveFocus: Boolean)
|
||||
}
|
||||
|
||||
class MainThreadWebviewViews(val project: Project) : MainThreadWebviewViewsShape {
|
||||
private val logger = Logger.getInstance(MainThreadWebviewViews::class.java)
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
override fun registerWebviewViewProvider(
|
||||
extension: Map<String, Any?>,
|
||||
viewType: String,
|
||||
options: Map<String, Any?>
|
||||
) {
|
||||
logger.info("Registering Webview view provider: viewType=$viewType, options=$options")
|
||||
|
||||
// Use EventBus to send WebView view provider registration event, using IntelliJ platform compatible method
|
||||
// project.getService(ProjectEventBus::class.java).emitInApplication(
|
||||
// WebviewViewProviderRegisterEvent,
|
||||
// WebviewViewProviderData(extension, viewType, options)
|
||||
// )
|
||||
project.getService(WebViewManager::class.java).registerProvider(WebviewViewProviderData(extension, viewType, options))
|
||||
}
|
||||
|
||||
override fun unregisterWebviewViewProvider(viewType: String) {
|
||||
logger.info("Unregistering Webview view provider: viewType=$viewType")
|
||||
}
|
||||
|
||||
override fun setWebviewViewTitle(handle: String, value: String?) {
|
||||
logger.info("Setting Webview view title: handle=$handle, title=$value")
|
||||
}
|
||||
|
||||
override fun setWebviewViewDescription(handle: String, value: String?) {
|
||||
logger.info("Setting Webview view description: handle=$handle, description=$value")
|
||||
}
|
||||
|
||||
override fun setWebviewViewBadge(handle: String, badge: Map<String, Any?>?) {
|
||||
logger.info("Setting Webview view badge: handle=$handle, badge=$badge")
|
||||
}
|
||||
|
||||
override fun show(handle: String, preserveFocus: Boolean) {
|
||||
logger.info("Showing Webview view: handle=$handle, preserveFocus=$preserveFocus")
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadWebviewViews resources")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import ai.roocode.jetbrains.events.EventBus
|
||||
import ai.roocode.jetbrains.events.ProjectEventBus
|
||||
import ai.roocode.jetbrains.events.WebviewHtmlUpdateData
|
||||
import ai.roocode.jetbrains.events.WebviewHtmlUpdateEvent
|
||||
import ai.roocode.jetbrains.webview.WebViewManager
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Webview handle type
|
||||
* Corresponds to WebviewHandle type in TypeScript
|
||||
*/
|
||||
typealias WebviewHandle = String
|
||||
|
||||
/**
|
||||
* Main thread Webviews service interface
|
||||
* Corresponds to MainThreadWebviewsShape interface in VSCode
|
||||
*/
|
||||
interface MainThreadWebviewsShape : Disposable {
|
||||
/**
|
||||
* Set HTML content
|
||||
* Corresponds to $setHtml method in TypeScript interface
|
||||
* @param handle Webview handle
|
||||
* @param value HTML content
|
||||
*/
|
||||
fun setHtml(handle: WebviewHandle, value: String)
|
||||
|
||||
/**
|
||||
* Set Webview options
|
||||
* Corresponds to $setOptions method in TypeScript interface
|
||||
* @param handle Webview handle
|
||||
* @param options Webview content options
|
||||
*/
|
||||
fun setOptions(handle: WebviewHandle, options: Map<String, Any?>)
|
||||
|
||||
/**
|
||||
* Send message to Webview
|
||||
* Corresponds to $postMessage method in TypeScript interface
|
||||
* @param handle Webview handle
|
||||
* @param value Message content
|
||||
* @param buffers Binary buffer array
|
||||
* @return Whether operation succeeded
|
||||
*/
|
||||
fun postMessage(handle: WebviewHandle, value: String): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread Webviews service implementation class
|
||||
*/
|
||||
class MainThreadWebviews(val project: Project) : MainThreadWebviewsShape {
|
||||
private val logger = Logger.getInstance(MainThreadWebviews::class.java)
|
||||
|
||||
// Store registered Webviews
|
||||
private val webviews = ConcurrentHashMap<WebviewHandle, Any?>()
|
||||
private var webviewHandle : WebviewHandle = ""
|
||||
|
||||
override fun setHtml(handle: WebviewHandle, value: String) {
|
||||
logger.info("Setting Webview HTML: handle=$handle, length=${value.length}")
|
||||
webviewHandle = handle
|
||||
try {
|
||||
// Replace vscode-file protocol format, using regex to match from vscode-file:/ to /roocode/ part
|
||||
val modifiedHtml = value.replace(Regex("vscode-file:/.*?/roocode/"), "/")
|
||||
logger.info("Replaced vscode-file protocol path format")
|
||||
|
||||
// Send HTML content update event through EventBus
|
||||
val data = WebviewHtmlUpdateData(handle, modifiedHtml)
|
||||
// project.getService(ProjectEventBus::class.java).emitInApplication(WebviewHtmlUpdateEvent, data)
|
||||
project.getService(WebViewManager::class.java).updateWebViewHtml(data)
|
||||
logger.info("Sent HTML content update event: handle=$handle")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to set Webview HTML", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setOptions(handle: WebviewHandle, options: Map<String, Any?>) {
|
||||
logger.info("Setting Webview options: handle=$handle, options=$options")
|
||||
webviewHandle = handle
|
||||
try {
|
||||
// Actual implementation should set options for Webview component on IDEA platform
|
||||
// Here we just log
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to set Webview options: $e")
|
||||
}
|
||||
}
|
||||
|
||||
override fun postMessage(handle: WebviewHandle, value: String): Boolean {
|
||||
// logger.info("Sending message to Webview: handle=$handle")
|
||||
if(value.contains("theme")) {
|
||||
logger.info("Sending theme message to Webview")
|
||||
}
|
||||
|
||||
return try {
|
||||
val mangler = project.getService(WebViewManager::class.java)
|
||||
|
||||
// mangler.getWebView(handle)?.postMessageToWebView(value)
|
||||
mangler.getLatestWebView()?.postMessageToWebView(value)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to send message to Webview: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadWebviews resources")
|
||||
webviews.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.actors
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.WindowManager
|
||||
import ai.roocode.jetbrains.plugin.SystemObjectProvider
|
||||
import ai.roocode.jetbrains.plugin.WecoderPluginService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.future.future
|
||||
import java.awt.Desktop
|
||||
import java.net.URI
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Main thread window service interface
|
||||
* Corresponds to MainThreadWindowShape interface in VSCode
|
||||
*/
|
||||
interface MainThreadWindowShape : Disposable {
|
||||
/**
|
||||
* Get initial state
|
||||
* @return Initial window state including focus and active status
|
||||
*/
|
||||
fun getInitialState(): Map<String, Boolean>
|
||||
|
||||
/**
|
||||
* Open URI
|
||||
* @param uri URI component
|
||||
* @param uriString URI string
|
||||
* @param options Open options
|
||||
* @return Whether successfully opened
|
||||
*/
|
||||
fun openUri(uri: Map<String, Any?>, uriString: String?, options: Map<String, Any?>): Boolean
|
||||
|
||||
/**
|
||||
* Convert to external URI
|
||||
* @param uri URI component
|
||||
* @param options Open options
|
||||
* @return External URI component
|
||||
*/
|
||||
fun asExternalUri(uri: Map<String, Any?>, options: Map<String, Any?>): Map<String, Any?>
|
||||
}
|
||||
|
||||
/**
|
||||
* Main thread window service implementation
|
||||
* Provides IDEA platform window related functionality
|
||||
*/
|
||||
class MainThreadWindow(val project: Project) : MainThreadWindowShape {
|
||||
private val logger = Logger.getInstance(MainThreadWindow::class.java)
|
||||
|
||||
override fun getInitialState(): Map<String, Boolean> {
|
||||
try {
|
||||
logger.info("Getting window initial state")
|
||||
|
||||
if (project != null) {
|
||||
// Get current project window state
|
||||
val frame = WindowManager.getInstance().getFrame(project)
|
||||
val isFocused = frame?.isFocused ?: false
|
||||
val isActive = frame?.isActive ?: false
|
||||
|
||||
return mapOf(
|
||||
"isFocused" to isFocused,
|
||||
"isActive" to isActive
|
||||
)
|
||||
} else {
|
||||
logger.warn("Cannot get current project, returning default window state")
|
||||
return mapOf(
|
||||
"isFocused" to false,
|
||||
"isActive" to false
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to get window initial state", e)
|
||||
return mapOf(
|
||||
"isFocused" to false,
|
||||
"isActive" to false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openUri(uri: Map<String, Any?>, uriString: String?, options: Map<String, Any?>): Boolean {
|
||||
try {
|
||||
logger.info("Opening URI: $uriString")
|
||||
|
||||
// Try to get URI
|
||||
val actualUri = if (uriString != null) {
|
||||
try {
|
||||
URI(uriString)
|
||||
} catch (e: Exception) {
|
||||
// If URI string is invalid, try to build from URI components
|
||||
createUriFromComponents(uri)
|
||||
}
|
||||
} else {
|
||||
createUriFromComponents(uri)
|
||||
}
|
||||
|
||||
return if (actualUri != null) {
|
||||
// Check if Desktop operation is supported
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().browse(actualUri)
|
||||
true
|
||||
} else {
|
||||
logger.warn("System does not support opening URI")
|
||||
false
|
||||
}
|
||||
} else {
|
||||
logger.warn("Cannot create valid URI")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to open URI", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun asExternalUri(uri: Map<String, Any?>, options: Map<String, Any?>): Map<String, Any?> {
|
||||
return try {
|
||||
logger.info("Converting to external URI: $uri")
|
||||
|
||||
// For most cases, we directly return the same URI components
|
||||
// Actual implementation may need to handle specific protocol conversion
|
||||
uri
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to convert to external URI", e)
|
||||
uri // Return original URI on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create URI from URI components
|
||||
* @param components URI components
|
||||
* @return Created URI or null
|
||||
*/
|
||||
private fun createUriFromComponents(components: Map<String, Any?>): URI? {
|
||||
return try {
|
||||
val scheme = components["scheme"] as? String ?: return null
|
||||
val authority = components["authority"] as? String ?: ""
|
||||
val path = components["path"] as? String ?: ""
|
||||
val query = components["query"] as? String ?: ""
|
||||
val fragment = components["fragment"] as? String ?: ""
|
||||
|
||||
URI(scheme, authority, path, query, fragment)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Failed to create URI from components: $components", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
logger.info("Disposing MainThreadWindow")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.commands
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
/**
|
||||
* Interface representing a command in the system.
|
||||
* Commands are used to define executable actions that can be registered and invoked.
|
||||
*/
|
||||
interface ICommand {
|
||||
/**
|
||||
* Gets the unique identifier for this command.
|
||||
* @return The command ID as a string
|
||||
*/
|
||||
fun getId(): String
|
||||
|
||||
/**
|
||||
* Gets the method name that should be invoked when this command is executed.
|
||||
* @return The method name as a string
|
||||
*/
|
||||
fun getMethod(): String
|
||||
|
||||
/**
|
||||
* Gets the handler object that contains the method to be invoked.
|
||||
* @return The handler object
|
||||
*/
|
||||
fun handler(): Any
|
||||
|
||||
/**
|
||||
* Gets the return type of the command, if any.
|
||||
* @return The return type as a string, or null if the command doesn't return a value
|
||||
*/
|
||||
fun returns(): String?
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a registry that manages commands.
|
||||
* Provides functionality to register, retrieve, and manage commands in the system.
|
||||
*/
|
||||
interface ICommandRegistry {
|
||||
/**
|
||||
* Called when a command is registered.
|
||||
* @param id The ID of the registered command
|
||||
*/
|
||||
fun onDidRegisterCommand(id: String)
|
||||
|
||||
/**
|
||||
* Registers a command in the registry.
|
||||
* @param command The command to register
|
||||
*/
|
||||
fun registerCommand(command: ICommand)
|
||||
|
||||
/**
|
||||
* Registers an alias for an existing command.
|
||||
* @param oldId The ID of the existing command
|
||||
* @param newId The new alias ID for the command
|
||||
*/
|
||||
fun registerCommandAlias(oldId: String, newId: String)
|
||||
|
||||
/**
|
||||
* Gets a command by its ID.
|
||||
* @param id The ID of the command to retrieve
|
||||
* @return The command, or null if not found
|
||||
*/
|
||||
fun getCommand(id: String): ICommand?
|
||||
|
||||
/**
|
||||
* Gets all registered commands.
|
||||
* @return A map of command IDs to commands
|
||||
*/
|
||||
fun getCommands(): Map<String, ICommand>
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the ICommandRegistry interface.
|
||||
* Manages commands for a specific project.
|
||||
*
|
||||
* @property project The project context for this command registry
|
||||
*/
|
||||
class CommandRegistry(val project: Project) : ICommandRegistry {
|
||||
|
||||
/**
|
||||
* Map of command IDs to lists of commands.
|
||||
* Using a list allows for potential command overloading in the future.
|
||||
*/
|
||||
private val commands = mutableMapOf<String, MutableList<ICommand>>()
|
||||
|
||||
/**
|
||||
* Called when a command is registered.
|
||||
* Currently not implemented.
|
||||
*
|
||||
* @param id The ID of the registered command
|
||||
*/
|
||||
override fun onDidRegisterCommand(id: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a command in the registry.
|
||||
*
|
||||
* @param command The command to register
|
||||
*/
|
||||
override fun registerCommand(command: ICommand) {
|
||||
commands.put(command.getId(), mutableListOf(command))
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an alias for an existing command.
|
||||
* If the original command exists, creates a new entry with the new ID pointing to the same command.
|
||||
*
|
||||
* @param oldId The ID of the existing command
|
||||
* @param newId The new alias ID for the command
|
||||
*/
|
||||
override fun registerCommandAlias(oldId: String, newId: String) {
|
||||
getCommand(oldId)?.let {
|
||||
commands.put(newId, mutableListOf(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a command by its ID.
|
||||
* Returns the first command registered with the given ID, or null if not found.
|
||||
*
|
||||
* @param id The ID of the command to retrieve
|
||||
* @return The command, or null if not found
|
||||
*/
|
||||
override fun getCommand(id: String): ICommand? {
|
||||
return commands[id]?.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all registered commands.
|
||||
* Returns a map of command IDs to the first command registered with each ID.
|
||||
*
|
||||
* @return A map of command IDs to commands
|
||||
*/
|
||||
override fun getCommands(): Map<String, ICommand> {
|
||||
return commands.mapValues { it.value.first() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.commands
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.JBProtocolCommand
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import ai.roocode.jetbrains.core.PluginContext
|
||||
import ai.roocode.jetbrains.core.ServiceProxyRegistry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* JetBrains Protocol Command for handling Roo Code authentication URLs
|
||||
*
|
||||
* Handles URLs in the format: jetbrains://idea/ai.roocode.jetbrains.auth?token=HERE
|
||||
* and forwards them to the VSCode extension via RPC protocol
|
||||
*/
|
||||
class KiloCodeAuthProtocolCommand : JBProtocolCommand("ai.roocode.jetbrains.auth") {
|
||||
private val logger = Logger.getInstance(KiloCodeAuthProtocolCommand::class.java)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
companion object {
|
||||
const val COMMAND_ID = "ai.roocode.jetbrains.auth"
|
||||
const val TOKEN_PARAM = "token"
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method for testing the protocol command execution
|
||||
* @param target The target parameter from the URL
|
||||
* @param parameters Map of URL parameters
|
||||
* @param fragment The URL fragment
|
||||
* @return null on success, error message on failure
|
||||
*/
|
||||
suspend fun executeForTesting(target: String?, parameters: Map<String, String>, fragment: String?): String? {
|
||||
return execute(target, parameters, fragment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the protocol command
|
||||
* @param target The target parameter from the URL
|
||||
* @param parameters Map of URL parameters
|
||||
* @param fragment The URL fragment
|
||||
* @return null on success, error message on failure
|
||||
*/
|
||||
override suspend fun execute(target: String?, parameters: Map<String, String>, fragment: String?): String? {
|
||||
logger.info("Handling Roo Code auth protocol command: target=$target, parameters=$parameters")
|
||||
|
||||
return try {
|
||||
// Extract token from parameters
|
||||
val token = parameters[TOKEN_PARAM]
|
||||
if (token.isNullOrBlank()) {
|
||||
val errorMsg = "No token found in parameters: $parameters"
|
||||
logger.warn(errorMsg)
|
||||
return errorMsg
|
||||
}
|
||||
|
||||
logger.info("Extracted token from parameters, forwarding to VSCode extension")
|
||||
|
||||
// Forward to VSCode extension via RPC
|
||||
forwardTokenToVSCodeExtension(token)
|
||||
|
||||
null // Success
|
||||
} catch (e: Exception) {
|
||||
val errorMsg = "Error handling Roo Code auth protocol command: ${e.message}"
|
||||
logger.error(errorMsg, e)
|
||||
errorMsg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Forward the token to the VSCode extension by simulating a VSCode URL handler call
|
||||
*/
|
||||
private fun forwardTokenToVSCodeExtension(token: String) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
// Get the current project (or default project if none is open)
|
||||
val project = getCurrentProject()
|
||||
|
||||
if (project == null) {
|
||||
logger.warn("No project available to forward token")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Get RPC protocol instance
|
||||
val protocol = project.getService(PluginContext::class.java)?.getRPCProtocol()
|
||||
|
||||
if (protocol == null) {
|
||||
logger.error("Cannot get RPC protocol instance, cannot forward token")
|
||||
return@launch
|
||||
}
|
||||
|
||||
logger.info("Forwarding token to VSCode extension via RPC")
|
||||
|
||||
// Use ExtHostCommands to execute a command that handles the URL
|
||||
val extHostCommands = protocol.getProxy(ServiceProxyRegistry.ExtHostContext.ExtHostCommands)
|
||||
|
||||
// Create the VSCode URI string that would normally be handled by handleUri
|
||||
val vscodeUriString = "vscode://roocode.kilo-code/roocode?token=${token}"
|
||||
|
||||
// Execute a command to handle the URI - this simulates what happens when VSCode receives a URL
|
||||
// We'll use a special command that the VSCode extension can handle
|
||||
extHostCommands.executeContributedCommand(
|
||||
"kilo-code.handleExternalUri",
|
||||
listOf(vscodeUriString)
|
||||
)
|
||||
|
||||
logger.info("Successfully forwarded token to VSCode extension via command execution")
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error forwarding token to VSCode extension", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current project, preferring the focused project
|
||||
*/
|
||||
private fun getCurrentProject(): Project? {
|
||||
return try {
|
||||
val projectManager = ProjectManager.getInstance()
|
||||
|
||||
// Try to get the default project first
|
||||
val openProjects = projectManager.openProjects
|
||||
|
||||
if (openProjects.isNotEmpty()) {
|
||||
// Return the first open project
|
||||
openProjects[0]
|
||||
} else {
|
||||
// Fallback to default project
|
||||
projectManager.defaultProject
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Error getting current project", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
// Copyright 2009-2025 Weibo, Inc.
|
||||
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ai.roocode.jetbrains.core
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import ai.roocode.jetbrains.editor.EditorAndDocManager
|
||||
import ai.roocode.jetbrains.ipc.NodeSocket
|
||||
import ai.roocode.jetbrains.ipc.PersistentProtocol
|
||||
import ai.roocode.jetbrains.ipc.proxy.ResponsiveState
|
||||
import ai.roocode.jetbrains.util.PluginConstants
|
||||
import ai.roocode.jetbrains.util.PluginResourceUtil
|
||||
import ai.roocode.jetbrains.util.URI
|
||||
import ai.roocode.jetbrains.workspace.WorkspaceFileChangeManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import java.net.Socket
|
||||
import java.nio.channels.SocketChannel
|
||||
import java.nio.file.Paths
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
|
||||
/**
|
||||
* Extension host manager, responsible for communication with extension processes.
|
||||
* Handles Ready and Initialized messages from extension processes.
|
||||
*/
|
||||
class ExtensionHostManager : Disposable {
|
||||
companion object {
|
||||
val LOG = Logger.getInstance(ExtensionHostManager::class.java)
|
||||
}
|
||||
|
||||
private val project: Project
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// Communication protocol
|
||||
private var nodeSocket: NodeSocket
|
||||
private var protocol: PersistentProtocol? = null
|
||||
|
||||
// RPC manager
|
||||
private var rpcManager: RPCManager? = null
|
||||
|
||||
// Extension manager
|
||||
private var extensionManager: ExtensionManager? = null
|
||||
|
||||
// Plugin identifier
|
||||
private var rooCodeIdentifier: String? = null
|
||||
|
||||
// JSON serialization
|
||||
private val gson = Gson()
|
||||
|
||||
// Last diagnostic log time
|
||||
private var lastDiagnosticLogTime = 0L
|
||||
|
||||
private var projectPath: String? = null
|
||||
|
||||
// Support Socket constructor
|
||||
constructor(clientSocket: Socket, projectPath: String,project: Project) {
|
||||
clientSocket.tcpNoDelay = true
|
||||
this.nodeSocket = NodeSocket(clientSocket, "extension-host")
|
||||
this.projectPath = projectPath
|
||||
this.project = project
|
||||
}
|
||||
// Support SocketChannel constructor
|
||||
constructor(clientChannel: SocketChannel, projectPath: String , project: Project) {
|
||||
this.nodeSocket = NodeSocket(clientChannel, "extension-host")
|
||||
this.projectPath = projectPath
|
||||
this.project = project
|
||||
}
|
||||
|
||||
/**
|
||||
* Start communication with the extension process.
|
||||
*/
|
||||
fun start() {
|
||||
try {
|
||||
// Initialize extension manager
|
||||
extensionManager = ExtensionManager()
|
||||
val extensionPath = PluginResourceUtil.getResourcePath(PluginConstants.PLUGIN_ID, PluginConstants.PLUGIN_CODE_DIR)
|
||||
rooCodeIdentifier = extensionPath?.let { extensionManager!!.registerExtension(it).identifier.value }
|
||||
// Create protocol
|
||||
protocol = PersistentProtocol(
|
||||
PersistentProtocol.PersistentProtocolOptions(
|
||||
socket = nodeSocket,
|
||||
initialChunk = null,
|
||||
loadEstimator = null,
|
||||
sendKeepAlive = true
|
||||
),
|
||||
this::handleMessage
|
||||
)
|
||||
|
||||
LOG.info("ExtensionHostManager started successfully")
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to start ExtensionHostManager", e)
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get RPC responsive state.
|
||||
* @return Responsive state, or null if RPC manager is not initialized.
|
||||
*/
|
||||
fun getResponsiveState(): ResponsiveState? {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
// Limit diagnostic log frequency, at most once every 60 seconds
|
||||
val shouldLogDiagnostics = currentTime - lastDiagnosticLogTime > 60000
|
||||
if (rpcManager == null) {
|
||||
if (shouldLogDiagnostics) {
|
||||
LOG.debug("Unable to get responsive state: RPC manager is not initialized")
|
||||
lastDiagnosticLogTime = currentTime
|
||||
}
|
||||
return null
|
||||
}
|
||||
// Log connection diagnostic information
|
||||
if (shouldLogDiagnostics) {
|
||||
val socketInfo = buildString {
|
||||
append("NodeSocket: ")
|
||||
append(if (nodeSocket.isClosed()) "closed" else "active")
|
||||
append(", input stream: ")
|
||||
append(if (nodeSocket.isInputClosed()) "closed" else "normal")
|
||||
append(", output stream: ")
|
||||
append(if (nodeSocket.isOutputClosed()) "closed" else "normal")
|
||||
append(", disposed=")
|
||||
append(nodeSocket.isDisposed())
|
||||
}
|
||||
|
||||
val protocolInfo = protocol?.let { proto ->
|
||||
"Protocol: ${if (proto.isDisposed()) "disposed" else "active"}"
|
||||
} ?: "Protocol is null"
|
||||
LOG.debug("Connection diagnostics: $socketInfo, $protocolInfo")
|
||||
lastDiagnosticLogTime = currentTime
|
||||
}
|
||||
return rpcManager?.getRPCProtocol()?.responsiveState
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from the extension process.
|
||||
*/
|
||||
private fun handleMessage(data: ByteArray) {
|
||||
// Check if data is a single-byte message (extension host protocol message)
|
||||
if (data.size == 1) {
|
||||
// Try to parse as extension host message type
|
||||
|
||||
when (ExtensionHostMessageType.fromData(data)) {
|
||||
ExtensionHostMessageType.Ready -> handleReadyMessage()
|
||||
ExtensionHostMessageType.Initialized -> handleInitializedMessage()
|
||||
ExtensionHostMessageType.Terminate -> LOG.info("Received Terminate message")
|
||||
null -> LOG.debug("Received unknown message type: ${data.contentToString()}")
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Received message with length ${data.size}, not handling as extension host message")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Ready message, send initialization data.
|
||||
*/
|
||||
private fun handleReadyMessage() {
|
||||
LOG.info("Received Ready message from extension host")
|
||||
|
||||
try {
|
||||
// Build initialization data
|
||||
val initData = createInitData()
|
||||
|
||||
// Send initialization data
|
||||
val jsonData = gson.toJson(initData).toByteArray()
|
||||
|
||||
protocol?.send(jsonData)
|
||||
LOG.info("Sent initialization data to extension host")
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to handle Ready message", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Initialized message, create RPC manager and activate plugin.
|
||||
*/
|
||||
private fun handleInitializedMessage() {
|
||||
LOG.info("Received Initialized message from extension host")
|
||||
|
||||
try {
|
||||
// Get protocol
|
||||
val protocol = this.protocol ?: throw IllegalStateException("Protocol is not initialized")
|
||||
val extensionManager = this.extensionManager ?: throw IllegalStateException("ExtensionManager is not initialized")
|
||||
|
||||
// Create RPC manager
|
||||
rpcManager = RPCManager(protocol, extensionManager,null, project)
|
||||
|
||||
// Start initialization process
|
||||
rpcManager?.startInitialize()
|
||||
|
||||
// Start file monitoring
|
||||
project.getService(WorkspaceFileChangeManager::class.java)
|
||||
// WorkspaceFileChangeManager.getInstance()
|
||||
project.getService(EditorAndDocManager::class.java).initCurrentIdeaEditor()
|
||||
// Activate RooCode plugin
|
||||
val rooCodeId = rooCodeIdentifier ?: throw IllegalStateException("RooCode identifier is not initialized")
|
||||
extensionManager.activateExtension(rooCodeId, rpcManager!!.getRPCProtocol())
|
||||
.whenComplete { _, error ->
|
||||
if (error != null) {
|
||||
LOG.error("Failed to activate RooCode plugin", error)
|
||||
} else {
|
||||
LOG.info("RooCode plugin activated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Initialized extension host")
|
||||
} catch (e: Exception) {
|
||||
LOG.error("Failed to handle Initialized message", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initialization data.
|
||||
* Corresponds to the initData object in main.js.
|
||||
*/
|
||||
private fun createInitData(): Map<String, Any?> {
|
||||
val pluginDir = getPluginDir()
|
||||
val basePath = projectPath
|
||||
|
||||
return mapOf(
|
||||
"commit" to "development",
|
||||
"version" to getIDEVersion(),
|
||||
"quality" to null,
|
||||
"parentPid" to ProcessHandle.current().pid(),
|
||||
"environment" to mapOf(
|
||||
"isExtensionDevelopmentDebug" to false,
|
||||
"appName" to getCurrentIDEName(),
|
||||
"appHost" to "node",
|
||||
"appLanguage" to "en",
|
||||
"appUriScheme" to "vscode",
|
||||
"appRoot" to uriFromPath(pluginDir),
|
||||
"globalStorageHome" to uriFromPath(Paths.get(System.getProperty("user.home"),".roocode", "globalStorage").toString()),
|
||||
"workspaceStorageHome" to uriFromPath(Paths.get(System.getProperty("user.home"),".roocode", "workspaceStorage").toString()),
|
||||
"extensionDevelopmentLocationURI" to null,
|
||||
"extensionTestsLocationURI" to null,
|
||||
"useHostProxy" to false,
|
||||
"skipWorkspaceStorageLock" to false,
|
||||
"isExtensionTelemetryLoggingOnly" to false,
|
||||
),
|
||||
"workspace" to mapOf(
|
||||
"id" to "intellij-workspace",
|
||||
"name" to "IntelliJ Workspace",
|
||||
"transient" to false,
|
||||
"configuration" to null,
|
||||
"isUntitled" to false
|
||||
),
|
||||
"remote" to mapOf(
|
||||
"authority" to null,
|
||||
"connectionData" to null,
|
||||
"isRemote" to false
|
||||
),
|
||||
"extensions" to mapOf<String, Any>(
|
||||
"versionId" to 1,
|
||||
"allExtensions" to (extensionManager?.getAllExtensionDescriptions() ?: emptyList<Any>()),
|
||||
"myExtensions" to (extensionManager?.getAllExtensionDescriptions()?.map { it.identifier } ?: emptyList<Any>()),
|
||||
"activationEvents" to (extensionManager?.getAllExtensionDescriptions()?.associate { ext ->
|
||||
ext.identifier.value to (ext.activationEvents ?: emptyList<String>())
|
||||
} ?: emptyMap())
|
||||
),
|
||||
"telemetryInfo" to mapOf(
|
||||
"sessionId" to "intellij-session",
|
||||
"machineId" to "intellij-machine",
|
||||
"sqmId" to "",
|
||||
"devDeviceId" to "",
|
||||
"firstSessionDate" to java.time.Instant.now().toString(),
|
||||
"msftInternal" to false
|
||||
),
|
||||
"logLevel" to 0, // Info level
|
||||
"loggers" to emptyList<Any>(),
|
||||
"logsLocation" to uriFromPath(Paths.get(pluginDir, "logs").toString()),
|
||||
"autoStart" to true,
|
||||
"consoleForward" to mapOf(
|
||||
"includeStack" to false,
|
||||
"logNative" to false
|
||||
),
|
||||
"uiKind" to 1 // Desktop
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current IDE name.
|
||||
*/
|
||||
private fun getCurrentIDEName(): String {
|
||||
val applicationInfo = ApplicationInfo.getInstance()
|
||||
val productCode = applicationInfo.build.productCode
|
||||
val version = applicationInfo.shortVersion ?: "1.0.0"
|
||||
|
||||
// Return in the format: wrapper|jetbrains|productCode
|
||||
val result = "wrapper|jetbrains|$productCode|$version"
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current IDE version.
|
||||
*/
|
||||
private fun getIDEVersion(): String {
|
||||
val applicationInfo = ApplicationInfo.getInstance()
|
||||
val version = applicationInfo.shortVersion ?: "1.0.0"
|
||||
LOG.info("Get IDE version: $version")
|
||||
|
||||
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(PluginConstants.PLUGIN_ID))?.version
|
||||
if (pluginVersion != null) {
|
||||
val fullVersion = "$version, $pluginVersion"
|
||||
LOG.info("Get IDE version and plugin version: $fullVersion")
|
||||
return fullVersion
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin directory.
|
||||
*/
|
||||
private fun getPluginDir(): String {
|
||||
return PluginResourceUtil.getResourcePath(PluginConstants.PLUGIN_ID, "")
|
||||
?: throw IllegalStateException("Unable to get plugin directory")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create URI object.
|
||||
*/
|
||||
private fun uriFromPath(path: String): URI {
|
||||
return URI.file(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource disposal.
|
||||
*/
|
||||
override fun dispose() {
|
||||
LOG.info("Disposing ExtensionHostManager")
|
||||
|
||||
// Cancel coroutines
|
||||
coroutineScope.cancel()
|
||||
|
||||
// Release RPC manager
|
||||
rpcManager = null
|
||||
|
||||
// Release protocol
|
||||
protocol?.dispose()
|
||||
protocol = null
|
||||
|
||||
// Release socket
|
||||
nodeSocket.dispose()
|
||||
|
||||
LOG.info("ExtensionHostManager disposed")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue