├── assets ├── devchat.png ├── devchat_icon.svg └── devchat_apply.svg ├── src ├── contributes │ ├── codecomplete │ │ ├── README.md │ │ ├── symbols.ts │ │ ├── astIndex │ │ │ ├── types.ts │ │ │ ├── createIdentifierSet.ts │ │ │ └── indexStore.ts │ │ ├── modelConfig.ts │ │ ├── llm │ │ │ └── constants.ts │ │ ├── debouncer.ts │ │ ├── astTest.ts │ │ ├── cache.ts │ │ ├── recentEdits.ts │ │ ├── utils.ts │ │ └── ast │ │ │ ├── findIdentifiers.ts │ │ │ ├── treeSitter.ts │ │ │ ├── ast.ts │ │ │ └── findFunctions.ts │ ├── context.ts │ ├── views.ts │ ├── util.ts │ ├── commandsBase.ts │ └── quickFixProvider.ts ├── handler │ ├── contextDetail.ts │ ├── focusHandler.ts │ ├── listMessages.ts │ ├── chatHandler.ts │ ├── openlinkHandler.ts │ ├── vscodeCommandHandler.ts │ ├── commitHandler.ts │ ├── historyMessagesHandler.ts │ ├── contextHandler.ts │ ├── historyMessagesBase.ts │ ├── workflowCommandHandler.ts │ ├── topicHandler.ts │ ├── fileHandler.ts │ ├── messageHandler.ts │ ├── configHandler.ts │ ├── codeBlockHandler.ts │ └── handlerRegister.ts ├── ide_services │ ├── README.md │ ├── endpoints │ │ ├── getToolsPath.ts │ │ ├── getCurrentFileInfo.ts │ │ ├── ideLanguage.ts │ │ ├── updateSlashCommands.ts │ │ ├── ideLogging.ts │ │ ├── getServicePort.ts │ │ ├── getCollapsedCode.ts │ │ ├── documentRangeDiagnostics.ts │ │ ├── legacy.ts │ │ ├── getDocumentSymbols.ts │ │ ├── findDefs.ts │ │ ├── legacy_bridge │ │ │ └── feature │ │ │ │ ├── find-refs.ts │ │ │ │ └── find-defs.ts │ │ ├── findTypeDefs.ts │ │ ├── unofficial.ts │ │ └── installPythonEnv.ts │ └── types.ts ├── types │ └── globle.d.ts ├── util │ ├── check.ts │ ├── constants.ts │ ├── reg_messages.ts │ ├── python_installer │ │ ├── readme.md │ │ ├── pip_package_version.ts │ │ ├── https_download.ts │ │ ├── package_install.ts │ │ ├── conda_url.ts │ │ ├── install_devchat.ts │ │ └── app_install.ts │ ├── logger.ts │ ├── extensionContext.ts │ ├── diffFilePairs.ts │ ├── findServicePort.ts │ ├── logger_vscode.ts │ ├── progressBar.ts │ ├── apiKey.ts │ ├── askCodeUtil.ts │ ├── uiUtil.ts │ ├── localService.ts │ ├── config.ts │ └── uiUtil_vscode.ts ├── init │ └── chatConfig.ts ├── context │ ├── contextCodeSelected.ts │ ├── contextManager.ts │ ├── contextFileSelected.ts │ └── contextDefRefs.ts ├── panel │ ├── webviewManager.ts │ ├── devchatView.ts │ ├── statusBarViewBase.ts │ └── statusBarView.ts └── toolwrapper │ └── dtm.ts ├── bin └── darwin │ └── arm64 │ └── dtm ├── jest.config.js ├── .mocharc.json ├── instructions ├── default │ ├── instLangPython.txt │ └── instCode.txt └── commit_message │ └── instCommitMessage.txt ├── tsconfig.test.json ├── .vscode ├── extensions.json ├── tasks.json └── launch.json ├── .env.example ├── .vscodeignore ├── CHANGELOG.md ├── .gitignore ├── workflows └── default │ └── context │ └── git_log_for_releasenote │ └── _setting_.json ├── .gitmodules ├── test ├── util │ ├── utils.test.ts │ ├── findServicePort.test.ts │ ├── messageHistory.test.ts │ ├── filePairManager.test.ts │ ├── logger.test.ts │ ├── config.test.ts │ ├── localService.test.ts │ └── commonUtil.test.ts ├── mocks │ └── vscode.js ├── context │ └── contextCodeSelected.test.ts ├── workflows │ ├── workflow_test.py │ └── ui_parser.py └── handler │ └── codeBlockHandler.test.ts ├── tsconfig.json ├── .eslintrc.json ├── docs └── publish.md ├── prebuild.js ├── webpack.config.js ├── .circleci └── config.yml └── vsc-extension-quickstart.md /assets/devchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devchat-ai/devchat-vscode/HEAD/assets/devchat.png -------------------------------------------------------------------------------- /src/contributes/codecomplete/README.md: -------------------------------------------------------------------------------- 1 | 2 | status.ts: 代码补全状态表达接口。预期有三种状态:未就绪、就绪、代码补全中。 3 | 4 | -------------------------------------------------------------------------------- /bin/darwin/arm64/dtm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devchat-ai/devchat-vscode/HEAD/bin/darwin/arm64/dtm -------------------------------------------------------------------------------- /src/handler/contextDetail.ts: -------------------------------------------------------------------------------- 1 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/ide_services/README.md: -------------------------------------------------------------------------------- 1 | # IDE Service 2 | 3 | This module is the VSCode implementation of IDE Service Protocol. -------------------------------------------------------------------------------- /src/types/globle.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | [key: string]: any; 4 | } 5 | } 6 | export {}; 7 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/symbols.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 获取符号名称、符号类型(函数、变量、类等)、符号所在文件路径、符号所在行数等信息 3 | 猜想这些信息会有助于代码补全功能的准确率提升 4 | */ 5 | 6 | -------------------------------------------------------------------------------- /src/util/check.ts: -------------------------------------------------------------------------------- 1 | 2 | export function assertValue(value: any, message: string) { 3 | if (value) { 4 | throw new Error(message); 5 | } 6 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testMatch: ["/test/**/*.test.ts"], 5 | }; -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "test/**/*.test.ts", 4 | "require": "ts-node/register", 5 | "project": "tsconfig.test.json" 6 | } -------------------------------------------------------------------------------- /instructions/default/instLangPython.txt: -------------------------------------------------------------------------------- 1 | When writing Python code, include type hints where appropriate and maintain compliance with PEP-8 guidelines, such as providing docstrings for modules, classes, and functions. -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "out-test", 6 | "skipLibCheck": true 7 | }, 8 | "include": ["src/**/*.ts", "test/**/*.ts"] 9 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/getToolsPath.ts: -------------------------------------------------------------------------------- 1 | import { UiUtilWrapper } from "../../util/uiUtil"; 2 | 3 | 4 | export async function getExtensionToolsPath(): Promise { 5 | return await UiUtilWrapper.extensionPath() + "/tools/"; 6 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | EXTENSION_NAME=devchat 2 | PUBLISHER=merico 3 | ASSISTANT_NAME_EN=DevChat 4 | ASSISTANT_NAME_ZH=DevChat 5 | EXTENSION_ICON=/path/to/extension_icon.png 6 | SIDEBAR_ICON=/path/to/sidebar_icon.svg 7 | DIFF_APPLY_ICON=/path/to/diff_apply_icon.svg 8 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | gui/** 15 | test/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "devchat" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .env 7 | .chatconfig.json 8 | 9 | # DevChat 10 | .chat/prompts.graphml 11 | .chat/prompts.db 12 | 13 | .vscode/settings.json 14 | actions_backup/ 15 | .DS_Store 16 | 17 | __pycache__/ 18 | *.log -------------------------------------------------------------------------------- /src/ide_services/endpoints/getCurrentFileInfo.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function getCurrentFileInfo() { 4 | const fileUri = vscode.window.activeTextEditor?.document.uri; 5 | const filePath = fileUri?.fsPath; 6 | return {"path": filePath ?? ""}; 7 | } 8 | -------------------------------------------------------------------------------- /workflows/default/context/git_log_for_releasenote/_setting_.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git log for release note", 3 | "description": "formatted commit history since the specified commit", 4 | "edit": true, 5 | "command": ["git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:\"%h - %B\""] 6 | } -------------------------------------------------------------------------------- /src/util/constants.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContextHolder } from "./extensionContext"; 2 | 3 | export let ASSISTANT_NAME_EN = "DevChat"; 4 | export let ASSISTANT_NAME_ZH = "DevChat"; 5 | 6 | export function updateNames(nameEN, nameZH) { 7 | ASSISTANT_NAME_EN = nameEN; 8 | ASSISTANT_NAME_ZH = nameZH; 9 | } -------------------------------------------------------------------------------- /src/ide_services/endpoints/ideLanguage.ts: -------------------------------------------------------------------------------- 1 | import { DevChatConfig } from "../../util/config"; 2 | 3 | export async function ideLanguage() { 4 | const language = DevChatConfig.getInstance().get('language'); 5 | // 'en' stands for English, 'zh' stands for Simplified Chinese 6 | return language; 7 | } 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tools"] 2 | path = tools 3 | url = https://github.com/devchat-ai/devchat-vscode-tools.git 4 | [submodule "gui"] 5 | path = gui 6 | url = https://github.com/devchat-ai/devchat-gui.git 7 | [submodule "workflowsCommands"] 8 | path = workflowsCommands 9 | url = https://github.com/devchat-ai/workflows.git 10 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/updateSlashCommands.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../../util/logger'; 2 | import * as vscode from 'vscode'; 3 | 4 | 5 | export async function updateSlashCommands() { 6 | logger.channel()?.debug('Updating slash commands...'); 7 | vscode.commands.executeCommand('DevChat.InstallCommands'); 8 | return true; 9 | } -------------------------------------------------------------------------------- /src/ide_services/endpoints/ideLogging.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../../util/logger"; 2 | 3 | export async function ideLogging(level: string, message: string) { 4 | if (typeof logger.channel()?.[level] === "function") { 5 | // level is one of "info", "warn", "error", "debug" 6 | logger.channel()?.[level](message); 7 | return true; 8 | } 9 | return false; 10 | } 11 | -------------------------------------------------------------------------------- /src/contributes/context.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function regLanguageContext() { 4 | const currentLocale = vscode.env.language; 5 | if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') { 6 | vscode.commands.executeCommand('setContext', 'isChineseLocale', true); 7 | } else { 8 | vscode.commands.executeCommand('setContext', 'isChineseLocale', false); 9 | } 10 | } -------------------------------------------------------------------------------- /src/handler/focusHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { MessageHandler } from './messageHandler'; 3 | 4 | 5 | export async function focusDevChatInput(panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 6 | const inputFocusMessage = {"command": "focusDevChatInput"}; 7 | if (panel) { 8 | MessageHandler.sendMessage(panel, inputFocusMessage); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/util/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | describe('yourFunction', () => { 5 | it('should return the correct result for input 1', () => { 6 | const input = 1; 7 | const expectedResult = 'expectedResult'; 8 | const result = 'expectedResult'; 9 | expect(result).to.equal(expectedResult); 10 | }); 11 | 12 | // Add more test cases here 13 | }); -------------------------------------------------------------------------------- /src/contributes/codecomplete/astIndex/types.ts: -------------------------------------------------------------------------------- 1 | // 代码分为多个block, 每个block包含一个变量集合 2 | export interface BlockInfo { 3 | file: string; 4 | startLine: number; 5 | endLine: number; 6 | lastTime: number; 7 | identifiers: string[]; 8 | } 9 | 10 | // 每个代码文件包含多个block 11 | export interface FileBlockInfo { 12 | path: string; 13 | lastTime: number; 14 | blocks: BlockInfo[]; 15 | hashKey: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/util/reg_messages.ts: -------------------------------------------------------------------------------- 1 | 2 | let inMessages: object[] = []; 3 | let outMessages: object[] = []; 4 | 5 | export function regInMessage(message: object) { 6 | inMessages.push(message); 7 | } 8 | 9 | export function regOutMessage(message: object) { 10 | outMessages.push(message); 11 | } 12 | 13 | export function getInMessages(): object[] { 14 | return inMessages; 15 | } 16 | 17 | export function getOutMessages(): object[] { 18 | return outMessages; 19 | } -------------------------------------------------------------------------------- /src/util/python_installer/readme.md: -------------------------------------------------------------------------------- 1 | # Why conda? 2 | Devchat-vscode support custom extension. Different extension may need different python version. 3 | 4 | pyenv is also a python version manager, but it always download source of python, then build it to binary. 5 | 6 | conda is a package manager, it can download binary of python, and install packages. 7 | 8 | # Where to install? 9 | Install conda to $USER_PROFILE/.chat 10 | 11 | Python will install inside $USER_PROFILE/.chat/conda. 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020", 7 | "es6", 8 | "dom" 9 | ], 10 | "outDir": "./dist", 11 | "sourceMap": true, 12 | "rootDir": "src", 13 | "strict": true, 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "noImplicitAny": false, 17 | "paths": { 18 | "@/*": [ 19 | "./src/*" 20 | ] 21 | }, 22 | "experimentalDecorators": true 23 | }, 24 | "exclude": [ 25 | "test","gui","tools" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/contributes/views.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DevChatViewProvider } from '../panel/devchatView'; 3 | import { ExtensionContextHolder } from '../util/extensionContext'; 4 | 5 | 6 | export function regDevChatView(context: vscode.ExtensionContext) { 7 | ExtensionContextHolder.provider = new DevChatViewProvider(context); 8 | context.subscriptions.push( 9 | vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, { 10 | webviewOptions: { retainContextWhenHidden: true } 11 | }) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/util/python_installer/pip_package_version.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | export function getPackageVersion(pythonPath: string, packageName: string): string | undefined { 4 | try { 5 | const stdout = execSync(`${pythonPath} -m pip show ${packageName}`).toString(); 6 | const versionLine = stdout.split('\n').find(line => line.startsWith('Version')); 7 | return versionLine ? versionLine.split(': ')[1] : undefined; 8 | } catch (error) { 9 | console.error(`exec error: ${error}`); 10 | return undefined; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/handler/listMessages.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { getInMessages, getOutMessages } from '../util/reg_messages'; 3 | import { MessageHandler } from './messageHandler'; 4 | 5 | export async function listAllMessages(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 6 | const inMessages = getInMessages(); 7 | const outMessages = getOutMessages(); 8 | 9 | MessageHandler.sendMessage(panel, { command: 'InMessage', result: inMessages }); 10 | MessageHandler.sendMessage(panel, { command: 'OutMessage', result: outMessages }); 11 | return; 12 | } 13 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/getServicePort.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../../util/logger"; 2 | 3 | export async function getServicePort() { 4 | logger 5 | .channel() 6 | ?.info(`get lsp bridge port: ${process.env.DEVCHAT_IDE_SERVICE_PORT}`); 7 | // return await UiUtilWrapper.getLSPBrigePort(); 8 | return process.env.DEVCHAT_IDE_SERVICE_PORT; 9 | } 10 | 11 | export async function getLocalServicePort() { 12 | logger 13 | .channel() 14 | ?.info(`get local service port: ${process.env.DC_LOCALSERVICE_PORT}`); 15 | return process.env.DC_LOCALSERVICE_PORT; 16 | } -------------------------------------------------------------------------------- /src/handler/chatHandler.ts: -------------------------------------------------------------------------------- 1 | import { ASSISTANT_NAME_EN } from '../util/constants'; 2 | import { UiUtilWrapper } from '../util/uiUtil'; 3 | import { MessageHandler } from './messageHandler'; 4 | import { isSending } from './sendMessage'; 5 | 6 | 7 | export async function chatWithDevChat(panel, message: string) { 8 | if (isSending()) { 9 | // already sending, show error 10 | UiUtilWrapper.showErrorMessage(`${ASSISTANT_NAME_EN}: A command is already being sent, please try again later.`); 11 | return; 12 | } 13 | MessageHandler.sendMessage(panel!, { command: 'chatWithDevChat', 'message': message }); 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface LogChannel { 3 | info(message: string, ...args: any[]): void; 4 | warn(message: string, ...args: any[]): void; 5 | error(message: string | Error, ...args: any[]): void; 6 | debug(message: string, ...args: any[]): void; 7 | trace(message: string, ...args: any[]): void; 8 | show(): void; 9 | } 10 | 11 | // eslint-disable-next-line @typescript-eslint/naming-convention 12 | export class logger { 13 | private static _channel: LogChannel | undefined; 14 | public static init(channel: LogChannel): void { 15 | this._channel = channel; 16 | } 17 | 18 | public static channel(): LogChannel | undefined { 19 | return this._channel; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/ide_services/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions for IDE Service Protocol 3 | */ 4 | export namespace IDEService { 5 | export type LogLevel = "info" | "warn" | "error" | "debug"; 6 | 7 | export interface Position { 8 | line: number; // 0-based 9 | character: number; // 0-based 10 | } 11 | 12 | export interface Range { 13 | start: Position; 14 | end: Position; 15 | } 16 | 17 | export interface Location { 18 | abspath: string; 19 | range: Range; 20 | } 21 | 22 | export interface SymbolNode { 23 | name: string; 24 | kind: string; 25 | range: Range; 26 | children: SymbolNode[]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/getCollapsedCode.ts: -------------------------------------------------------------------------------- 1 | import { collapseFileExculdeSelectRange } from '../../contributes/codecomplete/ast/collapseBlock'; 2 | import * as vscode from 'vscode'; 3 | 4 | export async function getCollapsedCode(fileName: string, startLine: number, endLine: number): Promise { 5 | const document = await vscode.workspace.openTextDocument(fileName); 6 | const startPosition = new vscode.Position(startLine, 0); 7 | const endPosition = new vscode.Position(endLine, Number.MAX_VALUE); 8 | const range = new vscode.Range(startPosition, endPosition); 9 | const code = await collapseFileExculdeSelectRange(document.uri.fsPath, document.getText(), range.start.line, range.end.line); 10 | return code; 11 | } -------------------------------------------------------------------------------- /src/contributes/codecomplete/modelConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface ModelConfigTemplate { 4 | template: string; 5 | stop: string[]; 6 | } 7 | 8 | const stableCodeTemplate: ModelConfigTemplate = { 9 | template: "{{{prefix}}}{{{suffix}}}", 10 | stop: ["", "", "", "<|endoftext|>"], 11 | }; 12 | 13 | const MODLE_COMPLETE_CONFIG = { 14 | 'starcoder': stableCodeTemplate, 15 | 'starcoder2': stableCodeTemplate, 16 | }; 17 | 18 | export function getModelConfigTemplate(modelName: string): ModelConfigTemplate | undefined { 19 | if (modelName in MODLE_COMPLETE_CONFIG) { 20 | return MODLE_COMPLETE_CONFIG[modelName]; 21 | } 22 | return undefined; 23 | } -------------------------------------------------------------------------------- /src/util/extensionContext.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DevChatViewProvider } from '../panel/devchatView'; 3 | 4 | export class ExtensionContextHolder { 5 | private static _context: vscode.ExtensionContext | undefined; 6 | private static _provider: DevChatViewProvider | undefined; 7 | 8 | static set context(context: vscode.ExtensionContext | undefined) { 9 | this._context = context; 10 | } 11 | 12 | static get context(): vscode.ExtensionContext | undefined { 13 | return this._context; 14 | } 15 | 16 | static set provider(provider: DevChatViewProvider | undefined) { 17 | this._provider = provider; 18 | } 19 | 20 | static get provider(): DevChatViewProvider | undefined { 21 | return this._provider; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/documentRangeDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function getDiagnosticsInRange(fileName: string, startLine: number, endLine: number): Promise { 4 | const document = await vscode.workspace.openTextDocument(fileName); 5 | const startPosition = new vscode.Position(startLine, 0); 6 | const endPosition = new vscode.Position(endLine, Number.MAX_VALUE); 7 | const range = new vscode.Range(startPosition, endPosition); 8 | const diagnosticsAll = vscode.languages.getDiagnostics(document.uri); 9 | 10 | const diagnostics = diagnosticsAll.filter(diag => range.contains(diag.range)); 11 | return diagnostics.map(diag => { return `${diag.message} <<${diag.source??""}:${diag.code??""}>>`; }); 12 | } -------------------------------------------------------------------------------- /src/handler/openlinkHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Commands for handling configuration read and write 3 | */ 4 | 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import * as vscode from 'vscode'; 8 | import yaml from 'yaml'; 9 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 10 | import { MessageHandler } from './messageHandler'; 11 | import { DevChatConfig } from '../util/config'; 12 | import { logger } from '../util/logger'; 13 | 14 | 15 | regInMessage({command: 'openLink', url: 'http://...'}); // when key is "", it will rewrite all config values 16 | export async function openLink(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 17 | const url = message.url; 18 | vscode.env.openExternal(vscode.Uri.parse(url)); 19 | } -------------------------------------------------------------------------------- /src/util/diffFilePairs.ts: -------------------------------------------------------------------------------- 1 | 2 | export class FilePairManager { 3 | private static instance: FilePairManager; 4 | private filePairs: Map; 5 | 6 | private constructor() { 7 | this.filePairs = new Map(); 8 | } 9 | 10 | static getInstance(): FilePairManager { 11 | if (!FilePairManager.instance) { 12 | FilePairManager.instance = new FilePairManager(); 13 | } 14 | return FilePairManager.instance; 15 | } 16 | 17 | addFilePair(file1: string, file2: string): void { 18 | this.filePairs.set(file1.toLowerCase(), [file1, file2]); 19 | this.filePairs.set(file2.toLowerCase(), [file1, file2]); 20 | } 21 | 22 | findPair(file: string): [string, string] | undefined { 23 | const fileLower = file.toLowerCase(); 24 | return this.filePairs.get(fileLower); 25 | } 26 | } -------------------------------------------------------------------------------- /src/handler/vscodeCommandHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | execute vscode command 3 | */ 4 | 5 | import * as vscode from 'vscode'; 6 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 7 | import { logger } from '../util/logger'; 8 | 9 | regInMessage({command: 'doCommand', content: ['command', 'arg1', 'arg2']}); 10 | export async function doVscodeCommand(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 11 | // execute vscode command 12 | // message.content[0]: vscode command 13 | // message.content[1:]: args for command 14 | try { 15 | await vscode.commands.executeCommand(message.content[0], ...message.content.slice(1)); 16 | } catch (error) { 17 | logger.channel()?.error(`Failed to execute command ${message.content[0]}: ${error}`); 18 | logger.channel()?.show(); 19 | } 20 | return; 21 | } -------------------------------------------------------------------------------- /src/util/findServicePort.ts: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | 3 | export async function findAvailablePort(): Promise { 4 | return new Promise((resolve, reject) => { 5 | const server = net.createServer().listen(); 6 | 7 | server.on('listening', () => { 8 | const address = server.address(); 9 | if (typeof address !== 'object' || !address?.port) { 10 | server.close(); 11 | reject(new Error('Failed to get port from server')); 12 | return; 13 | } 14 | server.close(() => resolve(address.port)); 15 | }); 16 | 17 | server.on('error', (err) => { 18 | const errWithCode = err as NodeJS.ErrnoException; 19 | if (errWithCode.code === 'EADDRINUSE') { 20 | reject(new Error('Port already in use')); 21 | } else { 22 | reject(err); 23 | } 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /instructions/commit_message/instCommitMessage.txt: -------------------------------------------------------------------------------- 1 | As a software developer assistant, your task is to provide clear and concise responses and write commit messages based on given code, requirements, or conversations. Follow these guidelines: 2 | 3 | 1. A commit message should include a title and multiple body lines. 4 | 2. Adhere to best practices, such as keeping titles under 50 characters and limiting body lines to under 72 characters. 5 | 3. Enclose messages in code blocks using triple backticks (```). 6 | 4. Utilize the , if provided, to create the summary. 7 | 5. Utilize the previous messages, if provided in the end of this prompt, to create the summary. Note that not all previous messages are necessarily relevant. 8 | 6. Please output commit message in a markdown code block, flag as commitmsg type. 9 | 10 | If you need more information, feel free to ask. -------------------------------------------------------------------------------- /test/util/findServicePort.test.ts: -------------------------------------------------------------------------------- 1 | // test/util/findServicePort.test.ts 2 | 3 | import { expect } from 'chai'; 4 | import net from 'net'; 5 | import { findAvailablePort } from '../../src/util/findServicePort'; 6 | 7 | describe('findAvailablePort', () => { 8 | it('should return an available port when successful', async () => { 9 | // Arrange 10 | const expectedPort = await findAvailablePort(); 11 | 12 | // Act 13 | const server = net.createServer(); 14 | const isAvailable = await new Promise((resolve) => { 15 | server.listen(expectedPort, () => { 16 | server.close(); 17 | resolve(true); 18 | }); 19 | server.on('error', () => { 20 | resolve(false); 21 | }); 22 | }); 23 | 24 | // Assert 25 | expect(isAvailable).to.be.true; 26 | expect(expectedPort).to.be.a('number'); 27 | expect(expectedPort).to.be.greaterThan(0); 28 | }); 29 | }); -------------------------------------------------------------------------------- /src/init/chatConfig.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import { logger } from '../util/logger'; 6 | import { UiUtilWrapper } from '../util/uiUtil'; 7 | 8 | 9 | function copyFileSync(source: string, target: string) { 10 | const data = fs.readFileSync(source); 11 | if (!fs.existsSync(target)) { 12 | fs.writeFileSync(target, data); 13 | } 14 | } 15 | 16 | function copyDirSync(source: string, target: string) { 17 | // 创建目标目录 18 | fs.mkdirSync(target, { recursive: true }); 19 | 20 | // 遍历目录中的所有文件和子目录 21 | const files = fs.readdirSync(source); 22 | for (const file of files) { 23 | const sourcePath = path.join(source, file); 24 | const targetPath = path.join(target, file); 25 | const stats = fs.statSync(sourcePath); 26 | if (stats.isDirectory()) { 27 | // 递归拷贝子目录 28 | copyDirSync(sourcePath, targetPath); 29 | } else { 30 | // 拷贝文件 31 | copyFileSync(sourcePath, targetPath); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/mocks/vscode.js: -------------------------------------------------------------------------------- 1 | // test/mocks/vscode.js 2 | 3 | // 定义一个模拟的openTextDocument函数 4 | function openTextDocumentMock() { 5 | return new Promise(resolve => { 6 | // 模拟异步返回一个文档对象 7 | resolve({ 8 | // 根据需要模拟文档对象的属性和方法 9 | getText: () => "模拟文档内容", 10 | // 其他需要模拟的方法和属性 11 | }); 12 | }); 13 | } 14 | 15 | // 定义一个模拟的showTextDocument函数 16 | function showTextDocumentMock(document, options) { 17 | return new Promise(resolve => { 18 | // 模拟异步打开文档的行为 19 | resolve({ 20 | // 模拟视图或编辑器的响应 21 | // 例如: 22 | viewColumn: options?.viewColumn, 23 | // 其他需要模拟的方法和属性 24 | }); 25 | }); 26 | } 27 | 28 | // 导出一个对象,该对象模拟vscode模块的一些API 29 | module.exports = { 30 | workspace: { 31 | openTextDocument: openTextDocumentMock, 32 | // 其他workspace下需要模拟的API 33 | }, 34 | window: { 35 | showTextDocument: showTextDocumentMock, 36 | // 其他window下需要模拟的API 37 | }, 38 | // 根据需要继续添加其他模拟的vscode API 39 | }; -------------------------------------------------------------------------------- /src/handler/commitHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import DtmWrapper from '../toolwrapper/dtm'; 3 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 4 | import { runCommandAndWriteOutput } from '../util/commonUtil'; 5 | 6 | regInMessage({command: 'doCommit', content: ''}); 7 | export async function doCommit(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 8 | const dtmWrapper = new DtmWrapper(); 9 | 10 | const result = await runCommandAndWriteOutput('git', ['diff', '--cached'], ''); 11 | 12 | let commitResult = {status: -1, message: '', log: ''}; 13 | if ((await result).stdout === '') { 14 | commitResult = await dtmWrapper.commitall(message.content); 15 | } else { 16 | commitResult = await dtmWrapper.commit(message.content); 17 | } 18 | 19 | if (commitResult.status === 0) { 20 | vscode.window.showInformationMessage('Commit successfully.'); 21 | } else { 22 | vscode.window.showErrorMessage(`Error commit fail: ${commitResult.message} ${commitResult.log}`); 23 | } 24 | return; 25 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": [ 34 | "npm: watch", 35 | "npm: watch-tests" 36 | ], 37 | "problemMatcher": [] 38 | }, 39 | { 40 | "type": "npm", 41 | "script": "postbuild", 42 | "problemMatcher": [], 43 | "label": "npm: postbuild", 44 | "detail": "npm run postbuild" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/llm/constants.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_MAX_TOKENS = 1024; 2 | const DEFAULT_CONTEXT_LENGTH = 4096; 3 | const DEFAULT_TEMPERATURE = 0.5; 4 | 5 | const DEFAULT_ARGS = { 6 | maxTokens: DEFAULT_MAX_TOKENS, 7 | temperature: DEFAULT_TEMPERATURE, 8 | }; 9 | 10 | const CONTEXT_LENGTH_FOR_MODEL: { [name: string]: number } = { 11 | "gpt-3.5-turbo": 4096, 12 | "gpt-3.5-turbo-0613": 4096, 13 | "gpt-3.5-turbo-16k": 16_384, 14 | "gpt-4": 8192, 15 | "gpt-35-turbo-16k": 16_384, 16 | "gpt-35-turbo-0613": 4096, 17 | "gpt-35-turbo": 4096, 18 | "gpt-4-32k": 32_768, 19 | "gpt-4-turbo-preview": 128_000, 20 | "gpt-4-vision": 128_000, 21 | "gpt-4-0125-preview": 128_000, 22 | "gpt-4-1106-preview": 128_000, 23 | }; 24 | 25 | const TOKEN_BUFFER_FOR_SAFETY = 350; 26 | const PROXY_URL = "http://localhost:65433"; 27 | 28 | const MAX_CHUNK_SIZE = 500; // 512 - buffer for safety (in case of differing tokenizers) 29 | 30 | export { 31 | CONTEXT_LENGTH_FOR_MODEL, 32 | DEFAULT_ARGS, 33 | DEFAULT_CONTEXT_LENGTH, 34 | DEFAULT_MAX_TOKENS, 35 | MAX_CHUNK_SIZE, 36 | PROXY_URL, 37 | TOKEN_BUFFER_FOR_SAFETY, 38 | }; 39 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/debouncer.ts: -------------------------------------------------------------------------------- 1 | export class Debouncer { 2 | private debouncing = false; 3 | private debounceTimeout?: NodeJS.Timeout; 4 | private lastTimeStampt?: string; 5 | 6 | constructor(private debounceDelay: number) { } 7 | 8 | async debounce(): Promise { 9 | const timestampt = Date.now().toString(); 10 | this.lastTimeStampt = timestampt; 11 | 12 | if (this.debouncing) { 13 | this.debounceTimeout?.refresh(); 14 | const lastTimestampt = await new Promise((resolve) => 15 | setTimeout(() => { 16 | resolve(this.lastTimeStampt); 17 | }, this.debounceDelay) 18 | ); 19 | return timestampt === lastTimestampt; 20 | } else { 21 | this.debouncing = true; 22 | this.lastTimeStampt = timestampt; 23 | this.debounceTimeout = setTimeout(() => { 24 | this.debouncing = false; 25 | }, this.debounceDelay); 26 | return true; 27 | } 28 | } 29 | } 30 | 31 | export default Debouncer; -------------------------------------------------------------------------------- /src/handler/historyMessagesHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { MessageHandler } from './messageHandler'; 3 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 4 | import { loadTopicHistoryFromCurrentMessageHistory } from './historyMessagesBase'; 5 | import { DevChatConfig } from '../util/config'; 6 | 7 | 8 | 9 | regInMessage({command: 'historyMessages', topicId: '', page: 0}); 10 | regOutMessage({command: 'loadHistoryMessages', entries: [{hash: '',user: '',date: '',request: '',response: '',context: [{content: '',role: ''}]}]}); 11 | export async function getHistoryMessages(message: {command: string, topicId: string, page: number}, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 12 | // if history message has load, send it to webview 13 | const maxCount = Number(DevChatConfig.getInstance().get('max_log_count')); 14 | const skip = maxCount * (message.page ? message.page : 0); 15 | const topicId = message.topicId; 16 | 17 | const historyMessage = await loadTopicHistoryFromCurrentMessageHistory(topicId, skip, maxCount); 18 | if (historyMessage) { 19 | MessageHandler.sendMessage(panel, historyMessage); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}", 19 | "postDebugTask": "npm: postbuild", 20 | "env": { 21 | "COMPLETE_DEBUG": "true" 22 | } 23 | }, 24 | { 25 | "name": "Extension Tests", 26 | "type": "extensionHost", 27 | "request": "launch", 28 | "args": [ 29 | "--extensionDevelopmentPath=${workspaceFolder}", 30 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 31 | ], 32 | "outFiles": [ 33 | "${workspaceFolder}/out/**/*.js", 34 | "${workspaceFolder}/dist/**/*.js" 35 | ], 36 | "preLaunchTask": "tasks: watch-tests" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/contributes/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { handleCodeSelected } from '../context/contextCodeSelected'; 4 | import { handleFileSelected } from '../context/contextFileSelected'; 5 | import { MessageHandler } from '../handler/messageHandler'; 6 | import { regOutMessage } from '../util/reg_messages'; 7 | import { logger } from '../util/logger'; 8 | 9 | regOutMessage({command: 'appendContext', context: ''}); 10 | export async function sendFileSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string): Promise { 11 | logger.channel()?.info(`File selected: ${filePath}`); 12 | const codeContext = await handleFileSelected(filePath); 13 | MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext }); 14 | } 15 | 16 | regOutMessage({command: 'appendContext', context: ''}); 17 | export async function sendCodeSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string, codeBlock: string, startLine: number): Promise { 18 | logger.channel()?.info(`File selected: ${filePath}`); 19 | const codeContext = await handleCodeSelected(filePath, codeBlock, startLine); 20 | MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext }); 21 | } -------------------------------------------------------------------------------- /src/util/logger_vscode.ts: -------------------------------------------------------------------------------- 1 | import { ASSISTANT_NAME_ZH } from "./constants"; 2 | import { LogChannel } from "./logger"; 3 | import * as vscode from 'vscode'; 4 | 5 | export class LoggerChannelVscode implements LogChannel { 6 | _channel: vscode.LogOutputChannel; 7 | 8 | private static _instance: LoggerChannelVscode; 9 | 10 | private constructor() { 11 | this._channel = vscode.window.createOutputChannel(ASSISTANT_NAME_ZH, { log: true }); 12 | } 13 | 14 | public static getInstance(): LoggerChannelVscode { 15 | if (!this._instance) { 16 | this._instance = new LoggerChannelVscode(); 17 | } 18 | return this._instance; 19 | } 20 | 21 | info(message: string, ...args: any[]): void { 22 | this._channel.info(message, ...args); 23 | } 24 | 25 | warn(message: string, ...args: any[]): void { 26 | this._channel.warn(message, ...args); 27 | } 28 | 29 | error(message: string | Error, ...args: any[]): void { 30 | this._channel.error(message, ...args); 31 | } 32 | 33 | debug(message: string, ...args: any[]): void { 34 | this._channel.debug(message, ...args); 35 | } 36 | 37 | trace(message: string, ...args: any[]): void { 38 | this._channel.trace(message, ...args); 39 | } 40 | 41 | show(): void { 42 | this._channel.show(); 43 | } 44 | } -------------------------------------------------------------------------------- /instructions/default/instCode.txt: -------------------------------------------------------------------------------- 1 | As a software developer assistant, your tasks are to: 2 | 3 | - Provide a clear and concise response to address the user's . 4 | - Write code and give advice based on given code or information in the if provided. 5 | - Follow language-specific best practices and coding standards. 6 | 7 | When responding: 8 | 9 | 1. Summarize and describe the requirements or provided information in your own words. 10 | 2. The summary and description should better be written in bullet points (excluding code). 11 | 3. If modifying given code, output the changes and avoid unnecessary unchanged code. 12 | 4. Enclose code or changes within blocks using triple backticks (```), and include the programming language and the file path, if available. For example: 13 | ``` 14 | ```python path=./path/to/file.py 15 | print("Hello, World!") 16 | ``` 17 | ``` 18 | If no file paths or folder structure are provided and you are unsure about the file path of the code, you may omit the file path. 19 | 5. Use separate code blocks for different files. 20 | 6. Utilize the previous messages, if provided in the end of this prompt, to create your response. Note that not all previous messages are necessarily relevant. 21 | 7. When providing a suggestion or instruction, begin by explaining the reason behind it. 22 | 8. If you need more information, ask for it. -------------------------------------------------------------------------------- /src/contributes/codecomplete/astTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { getAst, getTreePathAtCursor, RangeInFileWithContents } from "./ast/ast"; 4 | import Parser from "web-tree-sitter"; 5 | import { logger } from "../../util/logger"; 6 | import { getCommentPrefix, getLangageFunctionConfig, getLanguageFullName, LanguageFunctionsConfig } from "./ast/language"; 7 | import { getLanguageForFile, getQueryVariablesSource } from './ast/treeSitter'; 8 | 9 | 10 | function printTree(node: Parser.SyntaxNode, indent: number = 0) { 11 | let treeText = `${' '.repeat(indent)}Node type: ${node.type}, Position: ${node.startPosition.row}:${node.startPosition.column} - ${node.endPosition.row}:${node.endPosition.column}\n`; 12 | 13 | // 遍历子节点 14 | for (let i = 0; i < node.namedChildCount; i++) { 15 | const child = node.namedChild(i); 16 | treeText += printTree(child!, indent + 2); // 增加缩进 17 | } 18 | return treeText; 19 | } 20 | 21 | export async function outputAst( 22 | filepath: string, 23 | contents: string, 24 | cursorIndex: number 25 | ) { 26 | const ast = await getAst(filepath, contents); 27 | if (!ast) { 28 | return []; 29 | } 30 | 31 | // output ast 32 | const treeText = "\n" + printTree(ast.rootNode, 0); 33 | logger.channel()?.trace(treeText); 34 | } 35 | -------------------------------------------------------------------------------- /src/context/contextCodeSelected.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil'; 4 | import { UiUtilWrapper } from '../util/uiUtil'; 5 | 6 | export async function handleCodeSelected(fileSelected: string, codeSelected: string, startLine: number) { 7 | // get file name from fileSelected 8 | const fileName = path.basename(fileSelected); 9 | 10 | // create temp directory and file 11 | const tempDir = await createTempSubdirectory('devchat/context'); 12 | const tempFile = path.join(tempDir, fileName); 13 | 14 | // get the language from fileSelected 15 | const languageId = await getLanguageIdByFileName(fileSelected); 16 | 17 | // get relative path of workspace 18 | const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); 19 | const relativePath = workspaceDir 20 | ? path.relative(workspaceDir, fileSelected) 21 | : fileSelected; 22 | 23 | // convert fileContent to markdown code block with languageId and file path 24 | const data = { 25 | languageId: languageId, 26 | path: relativePath, 27 | startLine: startLine, 28 | content: codeSelected 29 | }; 30 | const jsonData = JSON.stringify(data); 31 | 32 | // save markdownCodeBlock to temp file 33 | await UiUtilWrapper.writeFile(tempFile, jsonData); 34 | 35 | return `[context|${tempFile}]`; 36 | } -------------------------------------------------------------------------------- /src/context/contextManager.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { logger } from '../util/logger'; 3 | 4 | import { createTempSubdirectory } from '../util/commonUtil'; 5 | import { UiUtilWrapper } from '../util/uiUtil'; 6 | 7 | 8 | export interface ChatContext { 9 | name: string; 10 | description: string; 11 | handler: () => Promise; 12 | } 13 | 14 | export class ChatContextManager { 15 | private static instance: ChatContextManager; 16 | private contexts: ChatContext[] = []; 17 | 18 | private constructor() {} 19 | 20 | public static getInstance(): ChatContextManager { 21 | if (!ChatContextManager.instance) { 22 | ChatContextManager.instance = new ChatContextManager(); 23 | } 24 | 25 | return ChatContextManager.instance; 26 | } 27 | 28 | registerContext(context: ChatContext): void { 29 | const existContext = this.contexts.find(c => c.name === context.name); 30 | if (!existContext) { 31 | this.contexts.push(context); 32 | } 33 | } 34 | 35 | getContextList(): ChatContext[] { 36 | return this.contexts; 37 | } 38 | 39 | async handleContextSelected(command: string): Promise { 40 | for (const contextObj of this.contexts) { 41 | if (contextObj.name === command) { 42 | return await contextObj.handler(); 43 | } 44 | } 45 | 46 | return []; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/context/contextFileSelected.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil'; 5 | import { UiUtilWrapper } from '../util/uiUtil'; 6 | 7 | export async function handleFileSelected(fileSelected: string) { 8 | // get file name from fileSelected 9 | const fileName = path.basename(fileSelected); 10 | 11 | // create temp directory and file 12 | const tempDir = await createTempSubdirectory('devchat/context'); 13 | const tempFile = path.join(tempDir, fileName); 14 | 15 | // load content in fileSelected 16 | const fileContent = fs.readFileSync(fileSelected, 'utf-8'); 17 | // get the language from fileSelected 18 | const languageId = await getLanguageIdByFileName(fileSelected); 19 | 20 | // get relative path of workspace 21 | const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); 22 | const relativePath = workspaceDir 23 | ? path.relative(workspaceDir, fileSelected) 24 | : fileSelected; 25 | 26 | // convert fileContent to markdown code block with languageId and file path 27 | const data = { 28 | languageId: languageId, 29 | path: relativePath, 30 | content: fileContent 31 | }; 32 | const jsonData = JSON.stringify(data); 33 | 34 | // save markdownCodeBlock to temp file 35 | await UiUtilWrapper.writeFile(tempFile, jsonData); 36 | 37 | return `[context|${tempFile}]`; 38 | } -------------------------------------------------------------------------------- /src/ide_services/endpoints/legacy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Legacy endpoints migrated from language-bridge for ask-code 3 | * 4 | * Should remove these endpoints after ask-code migrated to new endpoints 5 | */ 6 | 7 | import { 8 | findDefinitions, 9 | findDefinitionsOfToken, 10 | } from "./legacy_bridge/feature/find-defs"; 11 | 12 | import { findReferences } from "./legacy_bridge/feature/find-refs"; 13 | 14 | export namespace LegacyEndpoints { 15 | export async function definitions( 16 | abspath: string, 17 | line: string | undefined = undefined, 18 | character: string | undefined = undefined, 19 | token: string | undefined = undefined 20 | ) { 21 | if (token !== undefined) { 22 | const definitions = await findDefinitionsOfToken(abspath, token); 23 | return definitions; 24 | } else { 25 | const definitions = await findDefinitions( 26 | abspath, 27 | Number(line), 28 | Number(character) 29 | ); 30 | return definitions; 31 | } 32 | } 33 | 34 | export async function references( 35 | abspath: string, 36 | line: number, 37 | character: number 38 | ) { 39 | const references = await findReferences( 40 | abspath, 41 | Number(line), 42 | Number(character) 43 | ); 44 | 45 | return references; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/panel/webviewManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | 5 | export default class WebviewManager { 6 | private _webview: vscode.Webview; 7 | private _extensionUri: vscode.Uri; 8 | 9 | constructor(webview: vscode.Webview, extensionUri: vscode.Uri) { 10 | this._webview = webview; 11 | this._extensionUri = extensionUri; 12 | this.setWebviewOptions(); 13 | this.setWebviewContent(); 14 | } 15 | 16 | private setWebviewOptions() { 17 | this._webview.options = { 18 | enableScripts: true, 19 | localResourceRoots: [vscode.Uri.joinPath(this._extensionUri, 'dist')], 20 | }; 21 | } 22 | 23 | private setWebviewContent() { 24 | this._webview.html = this._getHtmlContent(); 25 | } 26 | 27 | public reloadWebviewContent() { 28 | this._webview.html = ''; 29 | this.setWebviewContent(); 30 | } 31 | 32 | private _getHtmlContent(): string { 33 | let mainHtml = 'index.html'; 34 | 35 | // const htmlPath = vscode.Uri.joinPath(this._extensionUri, 'dist', 'assets', 'chatPanel.html'); 36 | const htmlPath = vscode.Uri.joinPath(this._extensionUri, 'dist', mainHtml); 37 | const htmlContent = fs.readFileSync(htmlPath.fsPath, 'utf8'); 38 | 39 | return htmlContent.replace(//g, (_, resourcePath) => { 40 | const resourceUri = vscode.Uri.joinPath(this._extensionUri, 'dist', resourcePath); 41 | return this._webview.asWebviewUri(resourceUri).toString(); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/handler/contextHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from "fs"; 3 | 4 | import { ChatContextManager } from '../context/contextManager'; 5 | import { MessageHandler } from './messageHandler'; 6 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 7 | import { logger } from "../util/logger"; 8 | 9 | 10 | regInMessage({command: 'addContext', selected: ''}); 11 | regOutMessage({command: 'appendContext', context: ''}); 12 | export async function addConext(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 13 | const contextStrList = await ChatContextManager.getInstance().handleContextSelected(message.selected); 14 | for (const contextStr of contextStrList) { 15 | MessageHandler.sendMessage(panel, { command: 'appendContext', context: contextStr }); 16 | } 17 | } 18 | 19 | regInMessage({ command: 'contextDetail', file: '' }); 20 | regOutMessage({ command: 'contextDetailResponse', file: '', result: '' }); 21 | // message: { command: 'contextDetail', file: string } 22 | // read detail context information from file 23 | // return json string 24 | export async function getContextDetail(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { 25 | try { 26 | const fileContent = fs.readFileSync(message.file, 'utf-8'); 27 | MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file': message.file, result: fileContent }); 28 | } catch (error) { 29 | logger.channel()?.error(`Error reading file ${message.file}: ${error}`); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/util/progressBar.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from 'vscode'; 3 | import { ASSISTANT_NAME_ZH } from './constants'; 4 | 5 | export class ProgressBar { 6 | private message: string; 7 | private error: string | undefined; 8 | private finish: boolean | undefined; 9 | 10 | constructor() { 11 | this.message = ""; 12 | } 13 | 14 | init() { 15 | vscode.window.withProgress({ 16 | location: vscode.ProgressLocation.Notification, 17 | title: ASSISTANT_NAME_ZH, 18 | cancellable: false 19 | }, (progress, token) => { 20 | return new Promise((resolve) => { 21 | const timer = setInterval(() => { 22 | if (this.finish === true && this.error === "") { 23 | // vscode.window.showInformationMessage(`${this.message}`); 24 | resolve(); 25 | clearInterval(timer); 26 | } else if (this.finish === true && this.error !== "") { 27 | vscode.window.showErrorMessage(`Indexing failed: ${this.error}`); 28 | resolve(); 29 | clearInterval(timer); 30 | } else if (this.finish !== true) { 31 | progress.report({ message: `${this.message}` }); 32 | } 33 | }, 1000); 34 | }); 35 | }); 36 | } 37 | 38 | update(message: string, increment?: number) { 39 | this.message = message; 40 | } 41 | 42 | end() { 43 | this.error = ""; 44 | this.finish = true; 45 | } 46 | 47 | endWithError(error: string) { 48 | this.error = error; 49 | this.finish = true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/publish.md: -------------------------------------------------------------------------------- 1 | # Automated Publishing Process for DevChat VSCode Extension 2 | 3 | This document aims to explain the automated publishing process for our DevChat VSCode Extension. 4 | We use CircleCI for continuous integration and deployment to streamline the development and release process. 5 | 6 | ## Tagging Triggers Publishing 7 | 8 | Every time we are ready to publish a new version of the extension, we create a Git tag on the main branch. 9 | The tag's format is vX.X.X (e.g., v1.2.3). 10 | 11 | CircleCI is set up to listen for new tags on the main branch. When a new tag is created, 12 | it triggers the publishing workflow, which consists of building the extension and then publishing it to the VSCode Marketplace. 13 | 14 | ## Tag Number as Version Number 15 | 16 | The number part of the Git tag (omitting the v prefix) is used as the version number for the extension. 17 | During the publishing process, we automatically update the version field in package.json with this number. 18 | 19 | For example, if the Git tag is v1.2.3, the version number used in package.json will be 1.2.3. 20 | 21 | This automation ensures consistency between the Git tags and the published version numbers, 22 | making it easier to manage and track versions. 23 | 24 | ## Conclusion 25 | 26 | Our CircleCI-based publishing process enables a smooth, automated workflow for releasing new versions of the DevChat VSCode Extension. 27 | By using Git tags to trigger releases and denote version numbers, we ensure reliable, 28 | trackable releases that are straightforward for both the development team and the end-users to understand. 29 | -------------------------------------------------------------------------------- /src/toolwrapper/dtm.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../util/logger"; 2 | import { CommandRun } from "../util/commonUtil"; 3 | import { UiUtilWrapper } from "../util/uiUtil"; 4 | 5 | interface DtmResponse { 6 | status: number; 7 | message: string; 8 | log: string; 9 | } 10 | 11 | class DtmWrapper { 12 | private workspaceDir: string; 13 | private commandRun: CommandRun; 14 | 15 | constructor() { 16 | this.workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() || '.'; 17 | this.commandRun = new CommandRun(); 18 | } 19 | 20 | async commit(commitMsg: string): Promise { 21 | try { 22 | logger.channel()?.info(`Running command: git commit -m ${commitMsg}`); 23 | const result = await this.commandRun.spawnAsync("git", ['commit', '-m', commitMsg], { cwd: this.workspaceDir }, undefined, undefined, undefined, undefined); 24 | return { status: result.exitCode || 0, message: result.stdout, log: result.stderr }; 25 | } catch (error) { 26 | logger.channel()?.error(`Error: ${error}`); 27 | logger.channel()?.show(); 28 | return error as DtmResponse; 29 | } 30 | } 31 | 32 | async commitall(commitMsg: string): Promise { 33 | try { 34 | logger.channel()?.info(`Running command: git commit -am ${commitMsg}`); 35 | const result = await this.commandRun.spawnAsync("git", ['commit', '-am', commitMsg], { cwd: this.workspaceDir }, undefined, undefined, undefined, undefined); 36 | return { status: result.exitCode || 0, message: result.stdout, log: result.stderr }; 37 | } catch (error) { 38 | logger.channel()?.error(`Error: ${error}`); 39 | logger.channel()?.show(); 40 | return error as DtmResponse; 41 | } 42 | } 43 | } 44 | 45 | export default DtmWrapper; 46 | -------------------------------------------------------------------------------- /test/util/messageHistory.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { MessageHistory } from '../../src/util/messageHistory'; 4 | 5 | describe('MessageHistory', () => { 6 | let messageHistory: MessageHistory; 7 | 8 | beforeEach(() => { 9 | messageHistory = new MessageHistory(); 10 | }); 11 | 12 | it('add message', () => { 13 | const message = { hash: '123', content: 'Hello' }; 14 | messageHistory.add(message); 15 | expect(messageHistory.find('123')).to.deep.equal(message); 16 | }); 17 | 18 | it('find message by hash', () => { 19 | const message1 = { hash: '123', content: 'Hello' }; 20 | const message2 = { hash: '456', content: 'World' }; 21 | messageHistory.add(message1); 22 | messageHistory.add(message2); 23 | expect(messageHistory.find('123')).to.deep.equal(message1); 24 | expect(messageHistory.find('456')).to.deep.equal(message2); 25 | }); 26 | 27 | it('find last message', () => { 28 | const message1 = { hash: '123', content: 'Hello' }; 29 | const message2 = { hash: '456', content: 'World' }; 30 | messageHistory.add(message1); 31 | messageHistory.add(message2); 32 | expect(messageHistory.findLast()).to.deep.equal(message2); 33 | }); 34 | 35 | it('clear history', () => { 36 | const message1 = { hash: '123', content: 'Hello' }; 37 | const message2 = { hash: '456', content: 'World' }; 38 | messageHistory.add(message1); 39 | messageHistory.add(message2); 40 | messageHistory.clear(); 41 | expect(messageHistory.find('123')).to.be.undefined; 42 | expect(messageHistory.find('456')).to.be.undefined; 43 | expect(messageHistory.findLast()).to.be.null; 44 | }); 45 | }); -------------------------------------------------------------------------------- /test/util/filePairManager.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { FilePairManager } from '../../src/util/diffFilePairs'; 4 | 5 | describe('FilePairManager', () => { 6 | let filePairManager: FilePairManager; 7 | 8 | beforeEach(() => { 9 | filePairManager = FilePairManager.getInstance(); 10 | }); 11 | 12 | afterEach(() => { 13 | // Clear the filePairs map after each test 14 | (filePairManager as any).filePairs.clear(); 15 | }); 16 | 17 | it('add file pair', () => { 18 | const file1 = 'file1.txt'; 19 | const file2 = 'file2.txt'; 20 | filePairManager.addFilePair(file1, file2); 21 | expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]); 22 | expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]); 23 | }); 24 | 25 | it('find pair', () => { 26 | const file1 = 'file1.txt'; 27 | const file2 = 'file2.txt'; 28 | const file3 = 'file3.txt'; 29 | const file4 = 'file4.txt'; 30 | filePairManager.addFilePair(file1, file2); 31 | filePairManager.addFilePair(file3, file4); 32 | expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]); 33 | expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]); 34 | expect(filePairManager.findPair(file3)).to.deep.equal([file3, file4]); 35 | expect(filePairManager.findPair(file4)).to.deep.equal([file3, file4]); 36 | }); 37 | 38 | it('find non-existent pair', () => { 39 | const file1 = 'file1.txt'; 40 | const file2 = 'file2.txt'; 41 | const file3 = 'file3.txt'; 42 | filePairManager.addFilePair(file1, file2); 43 | expect(filePairManager.findPair(file3)).to.be.undefined; 44 | }); 45 | }); -------------------------------------------------------------------------------- /src/util/python_installer/https_download.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import * as os from 'os'; 4 | import * as path from 'path'; 5 | import { logger } from '../logger'; 6 | 7 | // download url to tmp directory 8 | // return: local file path or empty string 9 | export async function downloadFile(url: string): Promise { 10 | const os = process.platform; 11 | const tempDir = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME; 12 | 13 | const fileName = path.basename(url); // 从 URL 中提取文件名称 14 | const destination = path.join(tempDir!, fileName); // 构建文件路径 15 | 16 | const file = fs.createWriteStream(destination); 17 | let downloadedBytes = 0; 18 | let totalBytes = 0; 19 | let lastProgress = 0; 20 | 21 | return new Promise((resolve, reject) => { 22 | https.get(url, (response) => { 23 | totalBytes = parseInt(response.headers['content-length'] || '0', 10); 24 | 25 | response.on('data', (chunk) => { 26 | downloadedBytes += chunk.length; 27 | const progress = (downloadedBytes / totalBytes) * 100; 28 | 29 | if (progress - lastProgress >= 3) { 30 | logger.channel()?.info(`Downloaded ${downloadedBytes} bytes (${progress.toFixed(2)}%)`); 31 | lastProgress = progress; 32 | } 33 | }); 34 | 35 | response.pipe(file); 36 | 37 | file.on('finish', () => { 38 | file.close(); 39 | if (downloadedBytes !== totalBytes) { 40 | resolve(''); 41 | } else { 42 | resolve(destination); 43 | } 44 | }); 45 | }).on('error', (error) => { 46 | fs.unlink(destination, () => { 47 | resolve(''); 48 | }); 49 | }); 50 | }); 51 | } -------------------------------------------------------------------------------- /src/contributes/commandsBase.ts: -------------------------------------------------------------------------------- 1 | // src/contributes/commandsBase.ts 2 | 3 | import { UiUtilWrapper } from "../util/uiUtil"; 4 | import { runCommand } from "../util/commonUtil"; 5 | import { logger } from "../util/logger"; 6 | import { DevChatConfig } from "../util/config"; 7 | 8 | let devchatStatus = ''; 9 | 10 | 11 | 12 | function locateCommand(command): string | undefined { 13 | try { 14 | // split lines and choose first line 15 | const binPaths = runCommand(`where ${command}`).toString().trim().split('\n'); 16 | return binPaths[0].trim(); 17 | } catch (error) { 18 | try { 19 | const binPaths = runCommand(`which ${command}`).toString().trim().split('\n'); 20 | return binPaths[0].trim(); 21 | } catch (error) { 22 | return undefined; 23 | } 24 | } 25 | } 26 | 27 | function getDefaultPythonCommand(): string | undefined { 28 | try { 29 | runCommand('python3 -V'); 30 | return locateCommand('python3'); 31 | } catch (error) { 32 | try { 33 | const version = runCommand('python -V'); 34 | if (version.includes('Python 3')) { 35 | return locateCommand('python'); 36 | } 37 | return undefined; 38 | } catch (error) { 39 | return undefined; 40 | } 41 | } 42 | } 43 | 44 | export function getValidPythonCommand(): string | undefined { 45 | try { 46 | const devchatConfig = DevChatConfig.getInstance(); 47 | const pythonCommand = devchatConfig.get('python_for_chat'); 48 | if (pythonCommand) { 49 | return pythonCommand; 50 | } 51 | 52 | const defaultPythonCommand = getDefaultPythonCommand(); 53 | if (defaultPythonCommand) { 54 | devchatConfig.set('python_for_chat', defaultPythonCommand); 55 | } 56 | 57 | return defaultPythonCommand; 58 | } catch (error) { 59 | return undefined; 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /prebuild.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | function copyIcon(src, dst) { 7 | if (!src) { 8 | console.warn(`Icon path for ${dst} is not defined in your environment variables`); 9 | return; 10 | } 11 | console.log(`Replacing icon ${dst} by ${src}`); 12 | if (!fs.existsSync(src)) { 13 | console.warn(`Icon file ${src} does not exist.`); 14 | return; 15 | } 16 | 17 | const destPath = path.join(__dirname, 'assets', dst); 18 | 19 | try { 20 | fs.copyFileSync(src, destPath); 21 | fs.chmodSync(destPath, 0o644); 22 | } catch(e) { 23 | console.warn(`Failed to copy logo ${e}`); 24 | } 25 | } 26 | 27 | function updatePackageJson() { 28 | const placeholders = { 29 | EXTENSION_NAME: process.env.EXTENSION_NAME || "devchat", 30 | PUBLISHER: process.env.PUBLISHER || "merico", 31 | ASSISTANT_NAME_EN: process.env.ASSISTANT_NAME_EN || "DevChat", 32 | ASSISTANT_NAME_ZH: process.env.ASSISTANT_NAME_ZH || "DevChat" 33 | } 34 | console.log(`Updating package.json, env: ${JSON.stringify(placeholders)}`); 35 | 36 | let packageJson = fs.readFileSync('package.json', 'utf8'); 37 | 38 | // Replace placeholders 39 | Object.entries(placeholders).forEach(([key, value]) => { 40 | const regex = new RegExp(`\\$\\{${key}\\}`, 'g'); 41 | packageJson = packageJson.replace(regex, value); 42 | }); 43 | 44 | fs.writeFileSync('package.json', packageJson); 45 | } 46 | 47 | copyIcon(process.env.EXTENSION_ICON, 'devchat.png'); 48 | copyIcon(process.env.SIDEBAR_ICON, 'devchat_icon.svg'); 49 | copyIcon(process.env.DIFF_APPLY_ICON, 'devchat_apply.svg'); 50 | 51 | updatePackageJson(); -------------------------------------------------------------------------------- /src/handler/historyMessagesBase.ts: -------------------------------------------------------------------------------- 1 | import { DevChatClient, ShortLog } from '../toolwrapper/devchatClient'; 2 | 3 | export interface LogEntry { 4 | hash: string; 5 | parent: string | null; 6 | user: string; 7 | date: string; 8 | request: string; 9 | response: string; 10 | context: Array<{ 11 | content: string; 12 | role: string; 13 | }>; 14 | } 15 | 16 | export interface LoadHistoryMessages { 17 | command: string; 18 | entries: Array; 19 | } 20 | 21 | async function loadTopicHistoryLogs(topicId: string | undefined): Promise | undefined> { 22 | if (!topicId) { 23 | return undefined; 24 | } 25 | 26 | const dcClient = new DevChatClient(); 27 | const shortLogs: ShortLog[] = await dcClient.getTopicLogs(topicId, 10000, 0); 28 | 29 | const logEntries: Array = []; 30 | for (const shortLog of shortLogs) { 31 | const logE: LogEntry = { 32 | hash: shortLog.hash, 33 | parent: shortLog.parent, 34 | user: shortLog.user, 35 | date: shortLog.date, 36 | request: shortLog.request, 37 | response: shortLog.responses[0], 38 | context: shortLog.context, 39 | }; 40 | 41 | logEntries.push(logE); 42 | } 43 | 44 | return logEntries; 45 | } 46 | 47 | 48 | export async function loadTopicHistoryFromCurrentMessageHistory(topicId: string, skip: number, count: number): Promise< LoadHistoryMessages > { 49 | const logEntries = await loadTopicHistoryLogs(topicId); 50 | if (!logEntries) { 51 | return { 52 | command: 'loadHistoryMessages', 53 | entries: [], 54 | } as LoadHistoryMessages; 55 | } 56 | 57 | const logEntriesFlat = logEntries.reverse().slice(skip, skip + count).reverse(); 58 | return { 59 | command: 'loadHistoryMessages', 60 | entries: logEntriesFlat, 61 | } as LoadHistoryMessages; 62 | } 63 | -------------------------------------------------------------------------------- /test/context/contextCodeSelected.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | // import { describe, it, afterEach, beforeEach } from 'mocha'; 3 | import { handleCodeSelected } from '../../src/context/contextCodeSelected'; 4 | import * as path from 'path'; 5 | import { UiUtilWrapper } from '../../src/util/uiUtil'; 6 | import sinon from 'sinon'; 7 | 8 | describe('handleCodeSelected', () => { 9 | let languageIdStub: sinon.SinonStub; 10 | let workspaceFoldersFirstPathStub: sinon.SinonStub; 11 | let writeFileStub: sinon.SinonStub; 12 | 13 | beforeEach(() => { 14 | // Mock UiUtilWrapper functions 15 | languageIdStub = sinon.stub(UiUtilWrapper, 'languageId').resolves('typescript'); 16 | workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath').returns('test'); 17 | writeFileStub = sinon.stub(UiUtilWrapper, 'writeFile').resolves(); 18 | }); 19 | 20 | afterEach(() => { 21 | // Restore the original functions after each test 22 | languageIdStub.restore(); 23 | workspaceFoldersFirstPathStub.restore(); 24 | writeFileStub.restore(); 25 | }); 26 | 27 | it('should create a context file with the correct content', async () => { 28 | const fileSelected = path.join(__dirname, 'testFile.ts'); 29 | const codeSelected = 'console.log("Hello, world!");'; 30 | 31 | const contextFile = await handleCodeSelected(fileSelected, codeSelected, 0); 32 | 33 | // Check if the mocked functions were called with the correct arguments 34 | expect(languageIdStub.calledWith(fileSelected)).to.be.true; 35 | expect(workspaceFoldersFirstPathStub.called).to.be.true; 36 | expect(writeFileStub.called).to.be.true; 37 | 38 | // Extract the temp file path from the context string 39 | const tempFilePath = contextFile.match(/\[context\|(.*?)\]/)?.[1]; 40 | 41 | expect(tempFilePath).to.not.be.undefined; 42 | }); 43 | }); -------------------------------------------------------------------------------- /test/util/logger.test.ts: -------------------------------------------------------------------------------- 1 | // test/util/logger.test.ts 2 | 3 | import { expect } from 'chai'; 4 | import { describe, it } from 'mocha'; 5 | import { logger, LogChannel } from '../../src/util/logger'; 6 | 7 | class MockLogChannel implements LogChannel { 8 | logs: string[] = []; 9 | 10 | info(message: string, ...args: any[]): void { 11 | this.logs.push(`[INFO] ${message} ${args.join(' ')}`); 12 | } 13 | 14 | warn(message: string, ...args: any[]): void { 15 | this.logs.push(`[WARN] ${message} ${args.join(' ')}`); 16 | } 17 | 18 | error(message: string | Error, ...args: any[]): void { 19 | this.logs.push(`[ERROR] ${message} ${args.join(' ')}`); 20 | } 21 | 22 | debug(message: string, ...args: any[]): void { 23 | this.logs.push(`[DEBUG] ${message} ${args.join(' ')}`); 24 | } 25 | 26 | show(): void { 27 | // Do nothing 28 | } 29 | } 30 | 31 | describe('logger', () => { 32 | it('should initialize the logger and create a channel', () => { 33 | // Arrange 34 | const mockChannel = new MockLogChannel(); 35 | 36 | // Act 37 | logger.init(mockChannel); 38 | 39 | // Assert 40 | const channel = logger.channel(); 41 | expect(channel).to.not.be.undefined; 42 | expect(channel).to.equal(mockChannel); 43 | }); 44 | 45 | it('should log messages using the initialized channel', () => { 46 | // Arrange 47 | const mockChannel = new MockLogChannel(); 48 | logger.init(mockChannel); 49 | 50 | // Act 51 | logger.channel()?.info('Test info message'); 52 | logger.channel()?.warn('Test warn message'); 53 | logger.channel()?.error('Test error message'); 54 | logger.channel()?.debug('Test debug message'); 55 | 56 | // Assert 57 | expect(mockChannel.logs).to.deep.equal([ 58 | '[INFO] Test info message ', 59 | '[WARN] Test warn message ', 60 | '[ERROR] Test error message ', 61 | '[DEBUG] Test debug message ', 62 | ]); 63 | }); 64 | }); -------------------------------------------------------------------------------- /src/ide_services/endpoints/getDocumentSymbols.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { IDEService } from "../types"; 4 | 5 | /** 6 | * Get document symbols of a file 7 | * 8 | * @param abspath: absolute path of the file 9 | * @returns an array of IDEService.SymbolNode 10 | */ 11 | export async function getDocumentSymbols( 12 | abspath: string 13 | ): Promise { 14 | const documentSymbols = await vscode.commands.executeCommand< 15 | vscode.DocumentSymbol[] | vscode.SymbolInformation[] 16 | >("vscode.executeDocumentSymbolProvider", vscode.Uri.file(abspath)); 17 | 18 | const symbols: IDEService.SymbolNode[] = []; 19 | 20 | documentSymbols.forEach((symbol) => { 21 | const symbolNode = toSymbolNode(symbol); 22 | symbols.push(symbolNode); 23 | }); 24 | 25 | return symbols; 26 | } 27 | 28 | /** 29 | * Convert vscode.DocumentSymbol or vscode.SymbolInformation to IDEService.SymbolNode recursively 30 | */ 31 | function toSymbolNode( 32 | symbol: vscode.DocumentSymbol | vscode.SymbolInformation 33 | ): IDEService.SymbolNode { 34 | const range = "range" in symbol ? symbol.range : symbol.location?.range; 35 | const start = range.start; 36 | const end = range.end; 37 | 38 | const symbolNode: IDEService.SymbolNode = { 39 | name: symbol.name, 40 | kind: vscode.SymbolKind[symbol.kind], 41 | range: { 42 | start: { 43 | line: start.line, 44 | character: start.character, 45 | }, 46 | end: { 47 | line: end.line, 48 | character: end.character, 49 | }, 50 | }, 51 | children: [], 52 | }; 53 | 54 | if ("children" in symbol) { 55 | symbol.children.forEach((child) => { 56 | symbolNode.children.push(toSymbolNode(child)); 57 | }); 58 | } 59 | 60 | return symbolNode; 61 | } 62 | -------------------------------------------------------------------------------- /src/panel/devchatView.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import WebviewManager from './webviewManager'; 3 | 4 | import '../handler/handlerRegister'; 5 | import handleMessage from '../handler/messageHandler'; 6 | import { ExtensionContextHolder } from '../util/extensionContext'; 7 | 8 | 9 | export class DevChatViewProvider implements vscode.WebviewViewProvider { 10 | private _view?: vscode.WebviewView; 11 | private _webviewManager: WebviewManager | undefined; 12 | 13 | constructor(private readonly _context: vscode.ExtensionContext) { 14 | // Subscribe to the onDidChangeWorkspaceFolders event 15 | vscode.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, _context.subscriptions); 16 | } 17 | 18 | public view() { 19 | return this._view; 20 | } 21 | 22 | resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken): void { 23 | this._view = webviewView; 24 | 25 | this._webviewManager = new WebviewManager(webviewView.webview, this._context.extensionUri); 26 | 27 | this.registerEventListeners(); 28 | } 29 | 30 | public reloadWebview(): void { 31 | if (this._webviewManager) { 32 | this._webviewManager.reloadWebviewContent(); 33 | } 34 | } 35 | 36 | private registerEventListeners() { 37 | 38 | // this._view?.onDidDispose(() => this.dispose(), null, this._disposables); 39 | 40 | this._view?.webview.onDidReceiveMessage( 41 | async (message) => { 42 | handleMessage(message, this._view!); 43 | }, 44 | null, 45 | this._context.subscriptions 46 | ); 47 | } 48 | 49 | private onDidChangeWorkspaceFolders(event: vscode.WorkspaceFoldersChangeEvent): void { 50 | // Check if any folder was added or removed 51 | if (event.added.length > 0 || event.removed.length > 0) { 52 | // Update the webviewView content 53 | vscode.window.showInformationMessage(`onDidChangeWorkspaceFolders`); 54 | // this.updateWebviewContent(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/ide_services/endpoints/findDefs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { IDEService } from "../types"; 4 | import { assert } from "console"; 5 | 6 | /** 7 | * Find definition locations for a symbol 8 | * 9 | * @param abspath: absolute path of the file 10 | * @param line: line number of the symbol, 0-based 11 | * @param character: character number, 0-based 12 | */ 13 | export async function findDefinitionLocations( 14 | abspath: string, 15 | line: string, 16 | character: string 17 | ): Promise { 18 | const ln = Number(line); 19 | const col = Number(character); 20 | assert(!isNaN(ln) && !isNaN(col), "line and character must be numbers"); 21 | 22 | const position = new vscode.Position(ln, col); 23 | const uri = vscode.Uri.file(abspath); 24 | 25 | const defs = await vscode.commands.executeCommand< 26 | vscode.Location[] | vscode.LocationLink[] 27 | >("vscode.executeDefinitionProvider", uri, position); 28 | 29 | const locations: IDEService.Location[] = []; 30 | defs.forEach((def) => { 31 | locations.push(toLocation(def)); 32 | }); 33 | 34 | return locations; 35 | } 36 | 37 | /** 38 | * 39 | * @param location 40 | * @returns 41 | */ 42 | function toLocation( 43 | location: vscode.Location | vscode.LocationLink 44 | ): IDEService.Location { 45 | const range = "range" in location ? location.range : location.targetRange; 46 | const uri = "uri" in location ? location.uri : location.targetUri; 47 | const start = range.start; 48 | const end = range.end; 49 | 50 | const loc: IDEService.Location = { 51 | abspath: uri.fsPath, 52 | range: { 53 | start: { 54 | line: start.line, 55 | character: start.character, 56 | }, 57 | end: { 58 | line: end.line, 59 | character: end.character, 60 | }, 61 | }, 62 | }; 63 | 64 | return loc; 65 | } 66 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/legacy_bridge/feature/find-refs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | 4 | // TODO: merge with find-defs.ts 5 | 6 | interface Reference { 7 | name: string; 8 | abspath: string; 9 | line: number; // 1-based 10 | character: number; // 1-based 11 | } 12 | 13 | /** 14 | * @param abspath: absolute path of the file 15 | * @param line: line number, 1-based 16 | * @param character: character number, 1-based 17 | * 18 | **/ 19 | async function findReferences( 20 | abspath: string, 21 | line: number, 22 | character: number 23 | ): Promise { 24 | const uri = vscode.Uri.file(abspath); 25 | const position = new vscode.Position(line - 1, character - 1); 26 | 27 | // TODO: verify if the file & position is correct 28 | // const document = await vscode.workspace.openTextDocument(uri); 29 | 30 | const locations = await vscode.commands.executeCommand( 31 | "vscode.executeReferenceProvider", 32 | uri, 33 | position 34 | ); 35 | 36 | const references: Reference[] = []; 37 | if (locations) { 38 | for (const location of locations) { 39 | console.log( 40 | `* Reference found in file: ${location.uri.fsPath}, line: ${location.range.start.line}, character: ${location.range.start.character}` 41 | ); 42 | // use `map` & `Promise.all` to improve performance if needed 43 | const doc = await vscode.workspace.openTextDocument(location.uri); 44 | 45 | references.push({ 46 | name: doc.getText(location.range), 47 | abspath: location.uri.fsPath, 48 | line: location.range.start.line + 1, 49 | character: location.range.start.character + 1, 50 | }); 51 | } 52 | } else { 53 | console.log("No reference found"); 54 | } 55 | 56 | return references; 57 | } 58 | 59 | export { findReferences }; 60 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/findTypeDefs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { IDEService } from "../types"; 4 | import { assert } from "console"; 5 | 6 | /** 7 | * Find type definition locations for a symbol 8 | * 9 | * @param abspath: absolute path of the file 10 | * @param line: line number of the symbol, 0-based 11 | * @param character: character number, 0-based 12 | */ 13 | export async function findTypeDefinitionLocations( 14 | abspath: string, 15 | line: string, 16 | character: string 17 | ): Promise { 18 | const ln = Number(line); 19 | const col = Number(character); 20 | assert(!isNaN(ln) && !isNaN(col), "line and character must be numbers"); 21 | 22 | const position = new vscode.Position(ln, col); 23 | const uri = vscode.Uri.file(abspath); 24 | 25 | const defs = await vscode.commands.executeCommand< 26 | vscode.Location[] | vscode.LocationLink[] 27 | >("vscode.executeTypeDefinitionProvider", uri, position); 28 | 29 | const locations: IDEService.Location[] = []; 30 | defs.forEach((def) => { 31 | locations.push(toLocation(def)); 32 | }); 33 | 34 | return locations; 35 | } 36 | 37 | /** 38 | * 39 | * @param location 40 | * @returns 41 | */ 42 | function toLocation( 43 | location: vscode.Location | vscode.LocationLink 44 | ): IDEService.Location { 45 | const range = "range" in location ? location.range : location.targetRange; 46 | const uri = "uri" in location ? location.uri : location.targetUri; 47 | const start = range.start; 48 | const end = range.end; 49 | 50 | const loc: IDEService.Location = { 51 | abspath: uri.fsPath, 52 | range: { 53 | start: { 54 | line: start.line, 55 | character: start.character, 56 | }, 57 | end: { 58 | line: end.line, 59 | character: end.character, 60 | }, 61 | }, 62 | }; 63 | 64 | return loc; 65 | } 66 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/cache.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 使用内存存储最近的代码补全结果 3 | */ 4 | 5 | type CacheItem = { 6 | value: any; 7 | timestamp: number; 8 | }; 9 | 10 | class MemoryCacheManager { 11 | private maxCapacity: number = 5; 12 | private cache: Map; 13 | 14 | constructor(maxCapacity: number = 5) { 15 | this.maxCapacity = maxCapacity; 16 | this.cache = new Map(); 17 | } 18 | 19 | /** 20 | * 添加或更新缓存 21 | */ 22 | set(key: string, value: any): void { 23 | // 首先检查缓存中是否已经有了该键值对,若有,则更新;若没有,则添加 24 | if (this.cache.has(key)) { 25 | this.cache.set(key, { value, timestamp: Date.now() }); 26 | } else { 27 | // 先确保缓存没有超出最大容量 28 | if (this.cache.size >= this.maxCapacity) { 29 | this.evict(); 30 | } 31 | this.cache.set(key, { value, timestamp: Date.now() }); 32 | } 33 | } 34 | 35 | /** 36 | * 获取缓存 37 | */ 38 | get(key: string): any | undefined { 39 | const item = this.cache.get(key); 40 | if (item) { 41 | // 更新timestamp以反映最近一次访问 42 | item.timestamp = Date.now(); 43 | return item.value; 44 | } 45 | return undefined; 46 | } 47 | 48 | /** 49 | * 删除指定的缓存项 50 | */ 51 | delete(key: string): boolean { 52 | return this.cache.delete(key); 53 | } 54 | 55 | /** 56 | * 依据时间顺序(最久未使用)删除缓存项 57 | */ 58 | private evict(): void { 59 | let oldestKey: string | null = null; 60 | let oldestTimestamp: number = Infinity; 61 | 62 | for (const [key, item] of this.cache.entries()) { 63 | if (item.timestamp < oldestTimestamp) { 64 | oldestTimestamp = item.timestamp; 65 | oldestKey = key; 66 | } 67 | } 68 | 69 | if (oldestKey !== null) { 70 | this.cache.delete(oldestKey); 71 | } 72 | } 73 | } 74 | 75 | export default MemoryCacheManager; 76 | -------------------------------------------------------------------------------- /src/handler/workflowCommandHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { MessageHandler } from "./messageHandler"; 3 | import { regInMessage, regOutMessage } from "../util/reg_messages"; 4 | import { DevChatClient } from "../toolwrapper/devchatClient"; 5 | import { logger } from "../util/logger"; 6 | 7 | let existPannel: vscode.WebviewPanel | vscode.WebviewView | undefined = 8 | undefined; 9 | 10 | regInMessage({ command: "regCommandList" }); 11 | regOutMessage({ 12 | command: "regCommandList", 13 | result: [{ name: "", pattern: "", description: "" }], 14 | }); 15 | export async function handleRegCommandList( 16 | message: any, 17 | panel: vscode.WebviewPanel | vscode.WebviewView 18 | ): Promise { 19 | existPannel = panel; 20 | if (process.env.DC_LOCALSERVICE_PORT) { 21 | await getWorkflowCommandList(message, existPannel!); 22 | } 23 | } 24 | 25 | export async function getWorkflowCommandList( 26 | message: any, 27 | panel: vscode.WebviewPanel | vscode.WebviewView 28 | ): Promise { 29 | const dcClient = new DevChatClient(); 30 | 31 | // All workflows registered in DevChat 32 | const workflows = await dcClient.getWorkflowList(); 33 | 34 | // Get recommends from config 35 | const workflowsConfig = await dcClient.getWorkflowConfig(); 36 | const recommends = workflowsConfig.recommend?.workflows || []; 37 | 38 | // Filter active workflows and add recommend info 39 | const commandList = workflows 40 | .filter((workflow) => workflow.active) 41 | .map((workflow: any) => ({ 42 | ...workflow, 43 | recommend: recommends.indexOf(workflow.name), 44 | })); 45 | 46 | if (commandList.length > 0) { 47 | MessageHandler.sendMessage(panel, { 48 | command: "regCommandList", 49 | result: commandList, 50 | }); 51 | } 52 | 53 | return; 54 | } 55 | 56 | export async function sendCommandListByDevChatRun() { 57 | if (existPannel) { 58 | await getWorkflowCommandList({}, existPannel!); 59 | } 60 | } -------------------------------------------------------------------------------- /src/util/apiKey.ts: -------------------------------------------------------------------------------- 1 | // src/apiKey.ts 2 | 3 | import { DevChatConfig } from './config'; 4 | import { logger } from './logger'; 5 | 6 | export class ApiKeyManager { 7 | static async llmModel() { 8 | const devchatConfig = DevChatConfig.getInstance(); 9 | const defaultModel = devchatConfig.get('default_model'); 10 | if (!defaultModel) { 11 | return undefined; 12 | } 13 | 14 | // get model provider 15 | const defaultModelProvider = devchatConfig.get(['models', defaultModel, 'provider']); 16 | if (!defaultModelProvider) { 17 | return undefined; 18 | } 19 | 20 | // get provider config 21 | const defaultProvider = devchatConfig.get(['providers', defaultModelProvider]); 22 | const devchatProvider = devchatConfig.get(`providers.devchat`); 23 | 24 | let defaultModelConfig = devchatConfig.get(['models', defaultModel]); 25 | defaultModelConfig["model"] = defaultModel; 26 | if (defaultProvider) { 27 | for (const key of Object.keys(defaultProvider || {})) { 28 | const property = defaultProvider[key]; 29 | defaultModelConfig[key] = property; 30 | } 31 | if (!defaultModelConfig["api_base"] && defaultProvider === "devchat") { 32 | defaultModelConfig["api_base"] = "https://api.devchat.ai/v1"; 33 | } 34 | return defaultModelConfig; 35 | } else if (devchatProvider) { 36 | for (const key of Object.keys(devchatProvider || {})) { 37 | const property = devchatProvider[key]; 38 | defaultModelConfig[key] = property; 39 | } 40 | if (!defaultModelConfig["api_base"]) { 41 | logger.channel()?.error("api_base is not set in devchat provider!!!"); 42 | logger.channel()?.show(); 43 | } 44 | if (!defaultModelConfig["api_base"]) { 45 | defaultModelConfig["api_base"] = "https://api.devchat.ai/v1"; 46 | } 47 | return defaultModelConfig; 48 | } else { 49 | return undefined; 50 | } 51 | } 52 | 53 | static getKeyType(apiKey: string): string | undefined { 54 | if (apiKey.startsWith("sk-")) { 55 | return "sk"; 56 | } else if (apiKey.startsWith("DC.")) { 57 | return "DC"; 58 | } else { 59 | return undefined; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/panel/statusBarViewBase.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../util/logger"; 2 | 3 | import { installDevchat } from '../util/python_installer/install_devchat'; 4 | import { ASSISTANT_NAME_EN } from '../util/constants'; 5 | 6 | 7 | let devchatStatus = ''; 8 | 9 | let preDevchatStatus = ''; 10 | 11 | export async function dependencyCheck(): Promise { 12 | // there are some different status of devchat: 13 | // 0. not checked 14 | // 1. has statisfied the dependency 15 | // 2. is installing 16 | // 3. install failed 17 | // 4. install success 18 | 19 | // key status: 20 | // 0. not checked 21 | // 1. invalid or not set 22 | // 2. valid key 23 | 24 | // define subfunction to check devchat dependency 25 | const getDevChatStatus = async (): Promise => { 26 | const statuses = { 27 | installing: `installing ${ASSISTANT_NAME_EN}`, 28 | installed: `${ASSISTANT_NAME_EN} has been installed`, 29 | error: `An error occurred during the installation of ${ASSISTANT_NAME_EN}` 30 | } 31 | if (devchatStatus === '') { 32 | devchatStatus = statuses.installing; 33 | const devchatCommandEnv = await installDevchat(); 34 | if (devchatCommandEnv) { 35 | logger.channel()?.info(`Python: ${devchatCommandEnv}`); 36 | devchatStatus = statuses.installed; 37 | return devchatStatus; 38 | } else { 39 | logger.channel()?.info(`Python: undefined`); 40 | 41 | devchatStatus = statuses.error; 42 | return devchatStatus; 43 | } 44 | } else if (devchatStatus === 'has statisfied the dependency') { 45 | return devchatStatus; 46 | } else if (devchatStatus === statuses.installing) { 47 | return devchatStatus; 48 | } else if (devchatStatus === statuses.installed) { 49 | return devchatStatus; 50 | } else if (devchatStatus === statuses.error) { 51 | return devchatStatus; 52 | } 53 | return ""; 54 | }; 55 | 56 | const devchatPackageStatus = await getDevChatStatus(); 57 | 58 | if (devchatPackageStatus !== preDevchatStatus) { 59 | logger.channel()?.info(`${ASSISTANT_NAME_EN} status: ${devchatPackageStatus}`); 60 | preDevchatStatus = devchatPackageStatus; 61 | } 62 | 63 | return devchatPackageStatus; 64 | } -------------------------------------------------------------------------------- /src/util/askCodeUtil.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Util for askCode 3 | */ 4 | 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import { UiUtilWrapper } from './uiUtil'; 8 | 9 | let indexingStatus = 'stopped'; // 'started' | 'indexing' | 'stopped' 10 | 11 | export function updateLastModifyTime() { 12 | const workspaceFolder = UiUtilWrapper.workspaceFoldersFirstPath(); 13 | if (!workspaceFolder) { 14 | return; 15 | } 16 | 17 | let files = fs.readdirSync(workspaceFolder).filter(file => !file.startsWith('.')); 18 | 19 | let lastModifyTime = {}; 20 | for (let file of files) { 21 | let stats = fs.statSync(path.join(workspaceFolder, file)); 22 | lastModifyTime[file] = stats.mtime.toUTCString(); 23 | } 24 | 25 | fs.writeFileSync(path.join(workspaceFolder, '.chat', '.lastModifyTime.json'), JSON.stringify(lastModifyTime)); 26 | } 27 | 28 | export function isNeedIndexingCode() { 29 | const workspaceFolder = UiUtilWrapper.workspaceFoldersFirstPath(); 30 | if (!workspaceFolder) { 31 | return false; 32 | } 33 | 34 | let lastModifyTimeFile = path.join(workspaceFolder, '.chat', '.lastModifyTime.json'); 35 | if (!fs.existsSync(lastModifyTimeFile)) { 36 | return true; 37 | } 38 | 39 | let files = fs.readdirSync(workspaceFolder).filter(file => !file.startsWith('.')); 40 | 41 | // load lastModifyTime from .chat/.lastModifyTime.json 42 | let lastModifyTime = {}; 43 | if (fs.existsSync(lastModifyTimeFile)) { 44 | lastModifyTime = JSON.parse(fs.readFileSync(lastModifyTimeFile, 'utf-8')); 45 | } 46 | 47 | for (let file of files) { 48 | let stats = fs.statSync(path.join(workspaceFolder, file)); 49 | if (!lastModifyTime[file] || stats.mtime.toUTCString() !== lastModifyTime[file]) { 50 | return true; 51 | } 52 | } 53 | if (Object.keys(lastModifyTime).length !== files.length) { 54 | return true; 55 | } 56 | return false; 57 | } 58 | 59 | export function updateIndexingStatus(status: string) { 60 | if (status === "started") { 61 | updateLastModifyTime(); 62 | } 63 | indexingStatus = status; 64 | } 65 | 66 | export function isIndexingStopped() { 67 | return indexingStatus === 'stopped'; 68 | } 69 | -------------------------------------------------------------------------------- /src/handler/topicHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { regInMessage, regOutMessage } from "../util/reg_messages"; 4 | import { MessageHandler } from "./messageHandler"; 5 | import { DevChatClient } from "../toolwrapper/devchatClient"; 6 | import { LogEntry } from "./historyMessagesBase"; 7 | 8 | const dcClient = new DevChatClient(); 9 | 10 | export interface TopicEntry { 11 | // eslint-disable-next-line @typescript-eslint/naming-convention 12 | root_prompt: LogEntry; 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | latest_time: number; 15 | hidden: boolean; 16 | title: string | null; 17 | } 18 | 19 | // 注册获取当前topic列表的命令 20 | regInMessage({ command: "getTopics" }); 21 | regOutMessage({ command: "getTopics", topics: [] }); 22 | export async function getTopics( 23 | message: any, 24 | panel: vscode.WebviewPanel | vscode.WebviewView 25 | ): Promise { 26 | const topics = await dcClient.getTopics(100, 0); 27 | const entries: TopicEntry[] = []; 28 | 29 | for (const topic of topics) { 30 | const rootLog: LogEntry = { 31 | hash: topic.root_prompt_hash, 32 | parent: topic.root_prompt_parent, 33 | user: topic.root_prompt_user, 34 | date: topic.root_prompt_date, 35 | request: topic.root_prompt_request, 36 | response: topic.root_prompt_response, 37 | context: [], 38 | }; 39 | const e: TopicEntry = { 40 | root_prompt: rootLog, 41 | latest_time: topic.latest_time, 42 | hidden: topic.hidden, 43 | title: topic.title, 44 | }; 45 | entries.push(e); 46 | } 47 | 48 | MessageHandler.sendMessage(panel, { 49 | command: "getTopics", 50 | topicEntries: entries, 51 | }); 52 | } 53 | 54 | // 注册删除topic的命令 55 | regInMessage({ command: "deleteTopic", topicId: "" }); 56 | export async function deleteTopic( 57 | message: any, 58 | panel: vscode.WebviewPanel | vscode.WebviewView 59 | ): Promise { 60 | const topicId = message.topicId; 61 | await dcClient.deleteTopic(topicId); 62 | } 63 | -------------------------------------------------------------------------------- /src/ide_services/endpoints/unofficial.ts: -------------------------------------------------------------------------------- 1 | import { applyCodeWithDiff, applyEditCodeWithDiff } from "../../handler/diffHandler"; 2 | import * as vscode from 'vscode'; 3 | 4 | export namespace UnofficialEndpoints { 5 | export async function diffApply(filepath: string, content: string, autoedit: boolean = false) { 6 | if (autoedit) { 7 | applyEditCodeWithDiff({ fileName: filepath, content: content }, undefined) 8 | } else { 9 | applyCodeWithDiff({ fileName: filepath, content: content }, undefined); 10 | } 11 | return true; 12 | } 13 | 14 | export async function runCode(code: string) { 15 | // run code 16 | // delcare use vscode 17 | const vscode = require('vscode'); 18 | const evalCode = `(async () => { ${code} })();`; 19 | const res = eval(evalCode); 20 | return res; 21 | } 22 | 23 | export async function selectRange(fileName: string, startLine: number, startColumn: number, endLine: number, endColumn: number) { 24 | let editor = vscode.window.activeTextEditor; 25 | 26 | // If the file is not open or not the active editor, open it 27 | if (!editor || editor.document.fileName !== fileName) { 28 | const document = await vscode.workspace.openTextDocument(fileName); 29 | editor = await vscode.window.showTextDocument(document); 30 | } 31 | 32 | if (editor) { 33 | if (startLine === -1) { 34 | // Cancel selection 35 | editor.selection = new vscode.Selection(editor.selection.active, editor.selection.active); 36 | } else { 37 | // Select range 38 | const selection = new vscode.Selection( 39 | new vscode.Position(startLine, startColumn), 40 | new vscode.Position(endLine, endColumn) 41 | ); 42 | editor.selection = selection; 43 | 44 | // Reveal the selection 45 | editor.revealRange(selection, vscode.TextEditorRevealType.InCenter); 46 | } 47 | return true; 48 | } 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | name: "vscode extension", 13 | target: "node", // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 14 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 15 | 16 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 17 | 18 | output: { 19 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 20 | path: path.resolve(__dirname, "dist"), 21 | filename: "extension.js", 22 | libraryTarget: "commonjs2", 23 | }, 24 | externals: { 25 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 26 | // modules added here also need to be added in the .vscodeignore file 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: [".ts", ".js", ".json"], 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts?$/, 36 | exclude: /node_modules/, 37 | use: [ 38 | { 39 | loader: "babel-loader", 40 | options: { 41 | presets: [ 42 | "@babel/preset-env", 43 | "@babel/preset-react", 44 | "@babel/preset-typescript", 45 | ], 46 | }, 47 | }, 48 | { 49 | loader: "ts-loader", 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | devtool: "nosources-source-map", 56 | infrastructureLogging: { 57 | level: "log", // enables logging required for problem matchers 58 | }, 59 | plugins: [ 60 | ], 61 | }; 62 | 63 | module.exports = extensionConfig; 64 | -------------------------------------------------------------------------------- /src/contributes/quickFixProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { collapseFileExculdeSelectRange } from "./codecomplete/ast/collapseBlock"; 3 | import { ASSISTANT_NAME_EN } from "../util/constants"; 4 | 5 | class DevChatQuickFixProvider implements vscode.CodeActionProvider { 6 | public static readonly providedCodeActionKinds = [ 7 | vscode.CodeActionKind.QuickFix, 8 | ]; 9 | 10 | provideCodeActions( 11 | document: vscode.TextDocument, 12 | range: vscode.Range | vscode.Selection, 13 | context: vscode.CodeActionContext, 14 | token: vscode.CancellationToken, 15 | ): vscode.ProviderResult<(vscode.Command | vscode.CodeAction)[]> { 16 | if (context.diagnostics.length === 0) { 17 | return []; 18 | } 19 | 20 | const diagnostic = context.diagnostics[0]; 21 | const quickFix = new vscode.CodeAction( 22 | `Ask ${ASSISTANT_NAME_EN}`, 23 | vscode.CodeActionKind.QuickFix, 24 | ); 25 | quickFix.isPreferred = false; 26 | 27 | const fixUsingDevChat = new vscode.CodeAction( 28 | `Fix using ${ASSISTANT_NAME_EN}`, 29 | vscode.CodeActionKind.QuickFix, 30 | ); 31 | fixUsingDevChat.isPreferred = true; 32 | 33 | return new Promise(async (resolve) => { 34 | quickFix.command = { 35 | command: "DevChat.quickFixAskDevChat", 36 | title: `Ask ${ASSISTANT_NAME_EN}`, 37 | arguments: [ 38 | document, 39 | range, 40 | diagnostic, 41 | ], 42 | }; 43 | 44 | fixUsingDevChat.command = { 45 | command: "DevChat.quickFixUsingDevChat", 46 | title: `Fix using ${ASSISTANT_NAME_EN}`, 47 | arguments: [ 48 | document, 49 | range, 50 | diagnostic, 51 | ], 52 | }; 53 | 54 | resolve([quickFix, fixUsingDevChat]); 55 | }); 56 | } 57 | } 58 | 59 | export default function registerQuickFixProvider() { 60 | vscode.languages.registerCodeActionsProvider( 61 | { language: "*" }, 62 | new DevChatQuickFixProvider(), 63 | { 64 | providedCodeActionKinds: DevChatQuickFixProvider.providedCodeActionKinds, 65 | }, 66 | ); 67 | } -------------------------------------------------------------------------------- /src/ide_services/endpoints/installPythonEnv.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../../util/logger"; 2 | 3 | import { 4 | createEnvByConda, 5 | createEnvByMamba, 6 | } from "../../util/python_installer/app_install"; 7 | import { installRequirements } from "../../util/python_installer/package_install"; 8 | 9 | export async function installPythonEnv( 10 | command_name: string, 11 | requirements_file: string 12 | ) { 13 | // 1. install python >= 3.11 14 | logger.channel()?.info(`create env for python ...`); 15 | logger.channel()?.info(`try to create env by mamba ...`); 16 | let pythonCommand = await createEnvByMamba(command_name, "", "3.11.4"); 17 | 18 | if (!pythonCommand || pythonCommand === "") { 19 | logger 20 | .channel() 21 | ?.info( 22 | `create env by mamba failed, try to create env by conda ...` 23 | ); 24 | pythonCommand = await createEnvByConda(command_name, "", "3.11.4"); 25 | } 26 | 27 | if (!pythonCommand || pythonCommand === "") { 28 | logger 29 | .channel() 30 | ?.error( 31 | `create virtual python env failed, you need create it by yourself with command: "conda create -n devchat-commands python=3.11.4"` 32 | ); 33 | logger.channel()?.show(); 34 | 35 | return ""; 36 | } 37 | 38 | // 3. install requirements.txt 39 | // run command: pip install -r {requirementsFile} 40 | let isInstalled = false; 41 | // try 3 times 42 | for (let i = 0; i < 4; i++) { 43 | let otherSource: string | undefined = undefined; 44 | if (i > 1) { 45 | otherSource = "https://pypi.tuna.tsinghua.edu.cn/simple/"; 46 | } 47 | isInstalled = await installRequirements( 48 | pythonCommand, 49 | requirements_file, 50 | otherSource 51 | ); 52 | if (isInstalled) { 53 | break; 54 | } 55 | logger.channel()?.info(`Install packages failed, try again: ${i + 1}`); 56 | } 57 | if (!isInstalled) { 58 | logger 59 | .channel() 60 | ?.error( 61 | `Install packages failed, you can install it with command: "${pythonCommand} -m pip install -r ${requirements_file}"` 62 | ); 63 | logger.channel()?.show(); 64 | return ""; 65 | } 66 | 67 | return pythonCommand.trim(); 68 | } 69 | -------------------------------------------------------------------------------- /test/util/config.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it, beforeEach, afterEach } from 'mocha'; 3 | import fs from 'fs'; 4 | import yaml from 'yaml'; 5 | import { DevChatConfig } from '../../src/util/config'; // 调整路径以指向config.ts的实际位置 6 | import sinon from 'sinon'; 7 | import { logger } from '../../src/util/logger'; // 调整路径以指向logger的实际位置 8 | 9 | describe('DevChatConfig', () => { 10 | let readFileStub: sinon.SinonStub; 11 | let writeFileStub: sinon.SinonStub; 12 | let loggerStub: sinon.SinonStub; 13 | 14 | const mockData = { 15 | username: 'DevUser', 16 | theme: 'dark', 17 | }; 18 | 19 | beforeEach(() => { 20 | // Mock fs.readFileSync to return a YAML string based on mockData 21 | readFileStub = sinon.stub(fs, 'readFileSync').returns(yaml.stringify(mockData)); 22 | 23 | // Mock fs.writeFileSync to fake the writing process 24 | writeFileStub = sinon.stub(fs, 'writeFileSync'); 25 | 26 | // Mock the logger to prevent logging during tests 27 | loggerStub = sinon.stub(logger, 'channel').callsFake(() => ({ 28 | info: sinon.fake(), 29 | warn: sinon.fake(), 30 | error: sinon.fake(), 31 | debug: sinon.fake(), 32 | show: sinon.fake(), 33 | })); 34 | }); 35 | 36 | afterEach(() => { 37 | // Restore the original functionalities 38 | readFileStub.restore(); 39 | writeFileStub.restore(); 40 | loggerStub.restore(); 41 | }); 42 | 43 | it('should read config file and get the correct value for a given key', () => { 44 | const config = DevChatConfig.getInstance(); 45 | expect(config.get('username')).to.equal('DevUser'); 46 | }); 47 | 48 | it('should set a new key-value pair and write to the config file', () => { 49 | const config = DevChatConfig.getInstance(); 50 | const newKey = 'notifications.enabled'; 51 | const newValue = true; 52 | 53 | config.set(newKey, newValue); 54 | 55 | expect(config.get('notifications.enabled')).to.equal(true); 56 | // Check if fs.writeFileSync was called 57 | sinon.assert.calledOnce(writeFileStub); 58 | }); 59 | 60 | it('should handle errors when reading an invalid config file', () => { 61 | readFileStub.throws(new Error('Failed to read file')); 62 | 63 | // Constructing the config will attempt to read the file and log an error 64 | const config = DevChatConfig.getInstance(); 65 | 66 | // Check if the error was logged 67 | sinon.assert.called(loggerStub); 68 | }); 69 | }); -------------------------------------------------------------------------------- /src/contributes/codecomplete/recentEdits.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 记录最近修改的内容,用于代码补全 3 | */ 4 | import { logger } from '../../util/logger'; 5 | import * as vscode from 'vscode'; 6 | import { collapseFile } from './ast/collapseBlock'; 7 | import { getCommentPrefix } from './ast/language'; 8 | 9 | 10 | export class RecentEdit { 11 | fileName: string; 12 | content: string; 13 | collapseContent: string; 14 | 15 | constructor(fileName: string, content: string) { 16 | this.fileName = fileName; 17 | this.content = content; 18 | this.collapseContent = ""; 19 | } 20 | 21 | async close() { 22 | // collapse file 23 | this.collapseContent = await collapseFile(this.fileName, this.content); 24 | } 25 | 26 | async update(content: string) { 27 | this.content = content; 28 | this.collapseContent = ""; 29 | } 30 | } 31 | 32 | export class RecentEditsManager { 33 | private edits: RecentEdit[]; 34 | private maxCount: number = 10; 35 | 36 | constructor() { 37 | this.edits = []; 38 | 39 | vscode.workspace.onDidChangeTextDocument(e => { 40 | if (e.document.uri.scheme !== "file") { 41 | return; 42 | } 43 | // logger.channel()?.info(`onDidChangeTextDocument: ${e.document.fileName}`); 44 | // find edit 45 | let edit = this.edits.find(editFile => editFile.fileName === e.document.fileName); 46 | if (edit) { 47 | edit.update(e.document.getText()); 48 | } else { 49 | this.edits.push(new RecentEdit(e.document.fileName, e.document.getText())); 50 | } 51 | }); 52 | 53 | // onDidChangeActiveTextEditor: Event 54 | vscode.window.onDidChangeActiveTextEditor(e => { 55 | if (e) { 56 | // logger.channel()?.info(`onDidChangeActiveTextEditor: ${e.document.fileName}`); 57 | // close last edit 58 | this.edits.forEach(edit => { 59 | edit.close(); 60 | }); 61 | // move edit with the same file name to the end of the list 62 | let edit = this.edits.find(editFile => editFile.fileName === e.document.fileName); 63 | if (edit) { 64 | this.edits.splice(this.edits.indexOf(edit), 1); 65 | this.edits.push(edit); 66 | } else { 67 | this.edits.push(new RecentEdit(e.document.fileName, e.document.getText())); 68 | } 69 | } 70 | }); 71 | } 72 | 73 | public getEdits(): RecentEdit[] { 74 | return this.edits; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | node-executor: 4 | docker: 5 | - image: cimg/node:20.2.0 6 | jobs: 7 | build: 8 | executor: node-executor 9 | steps: 10 | - checkout 11 | - run: git submodule sync 12 | - run: git submodule update --init --recursive 13 | # 创建必要的目录 14 | - run: mkdir -p dist 15 | # 首先在根目录安装依赖 16 | - run: yarn install 17 | # 然后进入 gui 目录安装依赖并构建 18 | - run: | 19 | cd gui 20 | yarn install 21 | yarn vscode 22 | cd .. 23 | - persist_to_workspace: 24 | root: . 25 | paths: 26 | - . 27 | test: 28 | executor: node-executor 29 | steps: 30 | - attach_workspace: 31 | at: . 32 | - run: 33 | name: Install Python 34 | command: | 35 | sudo apt-get update 36 | sudo apt-get install -y python3 python3-pip 37 | - run: 38 | name: update workflows 39 | command: | 40 | export PYTHONPATH="$PYTHONPATH:$(pwd)/tools/site-packages" 41 | mkdir -p ~/.chat/workflows 42 | git clone https://github.com/devchat-ai/workflows.git ~/.chat/workflows/sys 43 | cd ~/.chat/workflows/sys 44 | - run: 45 | name: Run workflow tests 46 | command: | 47 | export PYTHONPATH="$PYTHONPATH:$(pwd)/tools/site-packages" 48 | git config --global user.email "tester@merico.dev" 49 | git config --global user.name "tester" 50 | python3 test/workflows/workflow_test.py 51 | - persist_to_workspace: 52 | root: . 53 | paths: 54 | - . 55 | publish: 56 | executor: node-executor 57 | steps: 58 | - attach_workspace: 59 | at: . 60 | - run: 61 | name: "Update version in package.json" 62 | command: | 63 | sed -i "s/\"version\": \".*\",/\"version\": \"${CIRCLE_TAG:1}\",/" package.json 64 | - run: 65 | name: "Publish to Marketplace" 66 | command: npx vsce publish -p $VSCE_TOKEN --allow-star-activation --pre-release 67 | workflows: 68 | version: 2 69 | build-and-publish: 70 | jobs: 71 | - build: 72 | filters: 73 | tags: 74 | only: /.*/ 75 | branches: 76 | ignore: [] 77 | - test: 78 | requires: 79 | - build 80 | filters: 81 | tags: 82 | only: /.*/ 83 | branches: 84 | ignore: /.*/ 85 | - publish: 86 | requires: 87 | - build 88 | filters: 89 | tags: 90 | only: /.*/ 91 | branches: 92 | ignore: /.*/ -------------------------------------------------------------------------------- /src/handler/fileHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from "fs"; 3 | 4 | import { MessageHandler } from './messageHandler'; 5 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 6 | import { logger } from "../util/logger"; 7 | 8 | 9 | // New: writeFile message handler 10 | regInMessage({ command: 'writeFile', file: '', content: '' }); 11 | // Write content to specified file 12 | export async function writeFile(message: any): Promise { 13 | try { 14 | fs.writeFileSync(message.file, message.content, 'utf-8'); 15 | logger.channel()?.info(`File ${message.file} has been written successfully.`); 16 | } catch (error) { 17 | logger.channel()?.error(`Error writing file ${message.file}: ${error}`); 18 | } 19 | } 20 | 21 | // New: readFile message handler 22 | regInMessage({ command: 'readFile', file: '' }); 23 | regOutMessage({ command: 'readFileResponse', file: '', content: '' }); 24 | // Read content from specified file and return it 25 | export async function readFile(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { 26 | try { 27 | const fileContent = fs.readFileSync(message.file, 'utf-8'); 28 | MessageHandler.sendMessage(panel, { command: 'readFileResponse', file: message.file, content: fileContent }); 29 | } catch (error) { 30 | logger.channel()?.error(`Error reading file ${message.file}: ${error}`); 31 | } 32 | } 33 | 34 | regInMessage({ command: 'getCurrentFileInfo'}); 35 | regOutMessage({ command: 'getCurrentFileInfo', result: '' }); 36 | // Read content from specified file and return it 37 | export async function getCurrentFileInfo(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { 38 | try { 39 | // 获取当前文件的绝对路径 40 | const fileUri = vscode.window.activeTextEditor?.document.uri; 41 | const filePath = fileUri?.fsPath; 42 | MessageHandler.sendMessage(panel, { command: 'getCurrentFileInfo', result: filePath ?? "" }); 43 | } catch (error) { 44 | logger.channel()?.error(`Error getting current file info: ${error}`); 45 | } 46 | } 47 | 48 | regInMessage({ command: 'getIDEServicePort'}); 49 | regOutMessage({ command: 'getIDEServicePort', result: 8090 }); 50 | // Read content from specified file and return it 51 | export async function getIDEServicePort(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { 52 | try { 53 | // Get IDE service port 54 | const port = process.env.DEVCHAT_IDE_SERVICE_PORT; 55 | MessageHandler.sendMessage(panel, { command: 'getIDEServicePort', result: port ?? 0 }); 56 | } catch (error) { 57 | logger.channel()?.error(`Error getting IDE service port: ${error}`); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | 4 | export interface Range { 5 | start: Position; 6 | end: Position; 7 | } 8 | export interface Position { 9 | line: number; 10 | character: number; 11 | } 12 | 13 | export interface RangeInFile { 14 | filepath: string; 15 | range: Range; 16 | } 17 | 18 | export async function readRangeInFile( 19 | filepath: string, 20 | range: vscode.Range 21 | ): Promise { 22 | const contents = new TextDecoder().decode( 23 | await vscode.workspace.fs.readFile(vscode.Uri.file(filepath)) 24 | ); 25 | const lines = contents.split("\n"); 26 | return ( 27 | lines.slice(range.start.line, range.end.line).join("\n") + 28 | "\n" + 29 | lines[ 30 | range.end.line < lines.length - 1 ? range.end.line : lines.length - 1 31 | ].slice(0, range.end.character) 32 | ); 33 | } 34 | 35 | export async function readFileByVSCode(filepath: string): Promise { 36 | const contents = new TextDecoder().decode( 37 | await vscode.workspace.fs.readFile(vscode.Uri.file(filepath)) 38 | ); 39 | 40 | return contents; 41 | } 42 | 43 | export async function readRangesInFileContents( contents: string, lines: string[], range: Range ) { 44 | if (!lines) { 45 | lines = contents.split("\n"); 46 | } 47 | 48 | if (range.start.line < range.end.line) { 49 | // TODO 50 | // handle start column 51 | return ( 52 | lines.slice(range.start.line, range.end.line).join("\n") + 53 | "\n" + 54 | lines[ 55 | range.end.line < lines.length - 1 ? range.end.line : lines.length - 1 56 | ].slice(0, range.end.character) 57 | ); 58 | } else { 59 | // TODO 60 | // handle start column 61 | return lines[ 62 | range.end.line < lines.length - 1 ? range.end.line : lines.length - 1 63 | ].slice(0, range.end.character); 64 | } 65 | } 66 | 67 | export async function readRangesInFile( 68 | filepath: string, 69 | ranges: Range[] 70 | ): Promise { 71 | const contents = new TextDecoder().decode( 72 | await vscode.workspace.fs.readFile(vscode.Uri.file(filepath)) 73 | ); 74 | const lines = contents.split("\n"); 75 | 76 | const result: string[] = []; 77 | for (const range of ranges) { 78 | result.push( 79 | ( 80 | lines.slice(range.start.line, range.end.line).join("\n") + 81 | "\n" + 82 | lines[ 83 | range.end.line < lines.length - 1 ? range.end.line : lines.length - 1 84 | ].slice(0, range.end.character) 85 | ) 86 | ); 87 | } 88 | return result; 89 | } -------------------------------------------------------------------------------- /test/util/localService.test.ts: -------------------------------------------------------------------------------- 1 | // test/util/localService.test.ts 2 | 3 | import { expect } from 'chai'; 4 | import { startLocalService, stopLocalService } from '../../src/util/localService'; 5 | import * as http from 'http'; 6 | 7 | describe('localService', () => { 8 | let port: number; 9 | 10 | describe('startLocalService', () => { 11 | it('should start the local service successfully', async () => { 12 | const extensionPath = '.'; 13 | const workspacePath = '.'; 14 | 15 | port = await startLocalService(extensionPath, workspacePath); 16 | 17 | expect(port).to.be.a('number'); 18 | expect(process.env.DC_SVC_PORT).to.equal(port.toString()); 19 | expect(process.env.DC_SVC_WORKSPACE).to.equal(workspacePath); 20 | expect(process.env.DC_LOCALSERVICE_PORT).to.equal(port.toString()); 21 | 22 | // Verify that the service is running by sending a ping request 23 | const response = await sendPingRequest(port); 24 | expect(response).to.equal('{"message":"pong"}'); 25 | }); 26 | }); 27 | 28 | describe('stopLocalService', () => { 29 | it('should stop the local service', async () => { 30 | await stopLocalService(); 31 | 32 | // Wait a bit to ensure the service has fully stopped 33 | await new Promise(resolve => setTimeout(resolve, 1000)); 34 | 35 | // Verify that the service is no longer running 36 | try { 37 | await sendPingRequest(port); 38 | throw new Error('Service is still running'); 39 | } catch (error) { 40 | console.log('Error type:', typeof error); 41 | console.log('Error:', error); 42 | 43 | if (error instanceof Error) { 44 | expect(error.message).to.include('connect ECONNREFUSED'); 45 | } else if (typeof error === 'object' && error !== null) { 46 | // Check if the error object has a 'code' property 47 | if ('code' in error) { 48 | expect(error.code).to.equal('ECONNREFUSED'); 49 | } else if ('errors' in error && Array.isArray(error.errors)) { 50 | // Check if it's an AggregateError-like object 51 | const hasConnectionRefused = error.errors.some((e: any) => e.code === 'ECONNREFUSED'); 52 | expect(hasConnectionRefused).to.be.true; 53 | } else { 54 | throw new Error(`Unexpected error structure: ${JSON.stringify(error)}`); 55 | } 56 | } else { 57 | throw new Error(`Unexpected error type: ${typeof error}`); 58 | } 59 | } 60 | }); 61 | }); 62 | }); 63 | 64 | function sendPingRequest(port: number): Promise { 65 | return new Promise((resolve, reject) => { 66 | http.get(`http://localhost:${port}/ping`, (res) => { 67 | let data = ''; 68 | res.on('data', (chunk) => data += chunk); 69 | res.on('end', () => resolve(data)); 70 | }).on('error', reject); 71 | }); 72 | } -------------------------------------------------------------------------------- /src/util/uiUtil.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface UiUtil { 3 | languageId(uri: string): Promise; 4 | workspaceFoldersFirstPath(): string | undefined; 5 | secretStorageGet(key: string): Promise; 6 | writeFile(uri: string, content: string): Promise; 7 | showInputBox(option: object): Promise; 8 | storeSecret(key: string, value: string): Promise; 9 | extensionPath(): string; 10 | runTerminal(terminalName:string, command: string): void; 11 | // current active file path 12 | activeFilePath(): string | undefined; 13 | // current selected range, return undefined if no selection 14 | selectRange(): [number, number] | undefined; 15 | // current selected text 16 | selectText(): string | undefined; 17 | showErrorMessage(message: string): void; 18 | getLSPBrigePort(): Promise; 19 | } 20 | 21 | 22 | export class UiUtilWrapper { 23 | private static _uiUtil: UiUtil | undefined; 24 | public static init(uiUtil: UiUtil): void { 25 | this._uiUtil = uiUtil; 26 | } 27 | 28 | public static async languageId(uri: string): Promise { 29 | return await this._uiUtil?.languageId(uri); 30 | } 31 | public static workspaceFoldersFirstPath(): string | undefined { 32 | return this._uiUtil?.workspaceFoldersFirstPath(); 33 | } 34 | public static async secretStorageGet(key: string): Promise { 35 | return await this._uiUtil?.secretStorageGet(key); 36 | } 37 | public static async writeFile(uri: string, content: string): Promise { 38 | return await this._uiUtil?.writeFile(uri, content); 39 | } 40 | public static async showInputBox(option: object): Promise { 41 | return await this._uiUtil?.showInputBox(option); 42 | } 43 | public static async storeSecret(key: string, value: string): Promise { 44 | return await this._uiUtil?.storeSecret(key, value); 45 | } 46 | public static extensionPath(): string { 47 | return this._uiUtil?.extensionPath()!; 48 | } 49 | public static runTerminal(terminalName: string, command: string): void { 50 | this._uiUtil?.runTerminal(terminalName, command); 51 | } 52 | // current active file path 53 | public static activeFilePath(): string | undefined { 54 | return this._uiUtil?.activeFilePath(); 55 | } 56 | // current selected range, return undefined if no selection 57 | public static selectRange(): [number, number] | undefined { 58 | return this._uiUtil?.selectRange(); 59 | } 60 | // current selected text 61 | public static selectText(): string | undefined { 62 | return this._uiUtil?.selectText(); 63 | } 64 | 65 | public static showErrorMessage(message: string): void { 66 | this._uiUtil?.showErrorMessage(message); 67 | } 68 | 69 | public static async getLSPBrigePort(): Promise { 70 | return await this._uiUtil?.getLSPBrigePort(); 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 37 | * Press `F5` to run the tests in a new window with your extension loaded. 38 | * See the output of the test result in the debug console. 39 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 40 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 41 | * You can create folders inside the `test` folder to structure your tests any way you want. 42 | 43 | ## Go further 44 | 45 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 46 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 47 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 48 | -------------------------------------------------------------------------------- /src/util/python_installer/package_install.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Install specific version of package. e.g. devchat 3 | */ 4 | 5 | 6 | import { spawn } from 'child_process'; 7 | import { logger } from '../logger'; 8 | 9 | // install specific version of package 10 | // pythonCommand -m install pkgName 11 | // if install success, return true 12 | // else return false 13 | export async function installPackage(pythonCommand: string, pkgName: string, otherSource: string | undefined) : Promise { 14 | return new Promise((resolve, reject) => { 15 | let errorOut = ''; 16 | 17 | const cmd = pythonCommand; 18 | let args = ['-m', 'pip', 'install', pkgName, '--force-reinstall']; 19 | if (otherSource) { 20 | args.push("-i"); 21 | args.push(otherSource); 22 | } 23 | const child = spawn(cmd, args); 24 | logger.channel()?.info(`Run command: ${cmd} ${args.join(' ')}`); 25 | 26 | child.stdout.on('data', (data) => { 27 | logger.channel()?.info(`${data}`); 28 | }); 29 | 30 | child.stderr.on('data', (data) => { 31 | logger.channel()?.error(`${data}`); 32 | logger.channel()?.show(); 33 | errorOut += data; 34 | }); 35 | 36 | child.on('error', (error) => { 37 | logger.channel()?.error(`exec error: ${error}`); 38 | logger.channel()?.show(); 39 | resolve(false); 40 | }); 41 | 42 | child.on('close', (code) => { 43 | if (code !== 0 && errorOut !== "") { 44 | resolve(false); 45 | } else { 46 | resolve(true); 47 | } 48 | }); 49 | }); 50 | } 51 | 52 | export async function installRequirements(pythonCommand: string, requirementsFile: string, otherSource: string | undefined) : Promise { 53 | return new Promise((resolve, reject) => { 54 | let errorOut = ''; 55 | 56 | const cmd = pythonCommand; 57 | let args = ['-m', 'pip', 'install', '-r', requirementsFile]; 58 | if (otherSource) { 59 | args.push("-i"); 60 | args.push(otherSource); 61 | } 62 | const child = spawn(cmd, args); 63 | logger.channel()?.info(`Run command: ${cmd} ${args.join(' ')}`); 64 | 65 | child.stdout.on('data', (data) => { 66 | logger.channel()?.info(`${data}`); 67 | }); 68 | 69 | child.stderr.on('data', (data) => { 70 | logger.channel()?.error(`${data}`); 71 | logger.channel()?.show(); 72 | errorOut += data; 73 | }); 74 | 75 | child.on('error', (error) => { 76 | logger.channel()?.error(`exec error: ${error}`); 77 | logger.channel()?.show(); 78 | resolve(false); 79 | }); 80 | 81 | child.on('close', (code) => { 82 | if (code !== 0 && errorOut !== "") { 83 | resolve(false); 84 | } else { 85 | resolve(true); 86 | } 87 | }); 88 | }); 89 | } -------------------------------------------------------------------------------- /src/handler/messageHandler.ts: -------------------------------------------------------------------------------- 1 | // messageHandler.ts 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | import { logger } from '../util/logger'; 6 | import { ExtensionContextHolder } from '../util/extensionContext'; 7 | 8 | 9 | export class MessageHandler { 10 | private handlers: { [command: string]: (message: any, panel: vscode.WebviewPanel|vscode.WebviewView) => Promise } = {}; 11 | 12 | constructor() { 13 | } 14 | 15 | registerHandler(command: string, handler: (message: any, panel: vscode.WebviewPanel|vscode.WebviewView) => Promise): void { 16 | this.handlers[command] = handler; 17 | } 18 | 19 | async handleMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 20 | try { 21 | let isNeedSendResponse = this.shouldSendResponse(message); 22 | 23 | const handler = this.handlers[message.command]; 24 | if (handler) { 25 | logger.channel()?.trace(`Handling the command "${message.command}"`); 26 | await handler(message, panel); 27 | logger.channel()?.trace(`Handling the command "${message.command}" done`); 28 | } else { 29 | logger.channel()?.warn(`No handler found for the command "${message.command}"`); 30 | logger.channel()?.show(); 31 | } 32 | 33 | if (isNeedSendResponse) { 34 | MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: 'finish', hash: '', user: '', date: 1, isError: false }); 35 | } 36 | } catch (e) { 37 | logger.channel()?.warn(`Error handling the message: "${JSON.stringify(message)}"`); 38 | logger.channel()?.show(); 39 | } 40 | } 41 | 42 | private shouldSendResponse(message: any): boolean { 43 | if (message.command === 'sendMessage') { 44 | try { 45 | const messageObject = JSON.parse(message.text); 46 | if (messageObject && messageObject.user && messageObject.user === 'merico-devchat') { 47 | message = messageObject; // Update the message reference 48 | return !messageObject.hasResponse; 49 | } 50 | } catch (e) { 51 | // Silence JSON parse error, log if necessary 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | public static sendMessage(panel: vscode.WebviewPanel|vscode.WebviewView, message : any, log: boolean = true): void { 58 | if (log) { 59 | logger.channel()?.trace(`Message to GUI: "${JSON.stringify(message)}"`); 60 | } 61 | 62 | panel.webview.postMessage(message); 63 | } 64 | 65 | public static sendMessage2(message : any, log: boolean = true): void { 66 | if (log) { 67 | logger.channel()?.trace(`Message to GUI: "${JSON.stringify(message)}"`); 68 | } 69 | 70 | const panel = ExtensionContextHolder.provider?.view()!; 71 | if (!panel) { 72 | logger.channel()?.warn(`No panel found to send message: "${JSON.stringify(message)}"`); 73 | return; 74 | } 75 | panel.webview.postMessage(message); 76 | } 77 | } 78 | 79 | export const messageHandler = new MessageHandler(); 80 | export default messageHandler.handleMessage.bind(messageHandler); 81 | -------------------------------------------------------------------------------- /test/workflows/workflow_test.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import subprocess 4 | import sys 5 | import threading 6 | 7 | import commit_cases 8 | 9 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def run_devchat_command(model, commit_command, input_mock): 13 | timeout = 300 # 超时时间,单位为秒 14 | # 构建命令 15 | command = [ 16 | sys.executable, '-m', 'devchat', 'route', '-m', 'gpt-3.5-turbo', '--', commit_command 17 | ] 18 | 19 | # 使用subprocess.Popen执行命令 20 | with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ) as process: 21 | def monitor(): 22 | try: 23 | # 等待设定的超时时间 24 | process.wait(timeout=timeout) 25 | except subprocess.TimeoutExpired: 26 | if process.poll() is None: # 如果进程仍然在运行,则终止它 27 | print(f"Process exceeded timeout of {timeout} seconds. Terminating...") 28 | process.terminate() 29 | 30 | # 创建并启动监控线程 31 | monitor_thread = threading.Thread(target=monitor) 32 | monitor_thread.start() 33 | 34 | # 循环读取输出并打印 35 | while True: 36 | output = process.stdout.readline() 37 | if output == '' and process.poll() is not None: 38 | break 39 | if output: 40 | print(output.strip()) 41 | user_input = input_mock(output.strip()) 42 | if user_input: 43 | process.stdin.write(user_input) 44 | process.stdin.flush() 45 | 46 | # 等待进程结束 47 | process.wait() 48 | 49 | # 等待监控线程结束 50 | monitor_thread.join() 51 | 52 | # 返回进程退出码 53 | return process.returncode 54 | 55 | 56 | 57 | 58 | def run_commit_tests(): 59 | model = 'gpt-4-1106-preview' # 替换为实际的模型名称 60 | 61 | case_results = [] 62 | for case in commit_cases.get_cases(): 63 | case_result = False 64 | exit_code = -1 65 | setup_result = case['setup']() 66 | if setup_result: 67 | exit_code = run_devchat_command(model, case['input'], case['input_mock']) 68 | case_result = case['assert']() 69 | case['teardown']() 70 | case_results.append((case['title'], case_result and exit_code == 0)) 71 | else: 72 | print('Error: test case setup failed!') 73 | case_results.append((case['title'](), 'Error: test case setup failed!')) 74 | print('Case result:', case_result, ' Exit code:', exit_code) 75 | 76 | print('All case results:') 77 | is_error = False 78 | for case_result in case_results: 79 | print(case_result[0], case_result[1]) 80 | if case_result[1] != True: 81 | is_error = True 82 | if is_error: 83 | sys.exit(-1) 84 | else: 85 | sys.exit(0) 86 | 87 | run_commit_tests() 88 | -------------------------------------------------------------------------------- /src/util/python_installer/conda_url.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Get conda download url 3 | */ 4 | 5 | import os from 'os'; 6 | import { logger } from '../logger'; 7 | import { UiUtilVscode } from '../uiUtil_vscode'; 8 | import { UiUtilWrapper } from '../uiUtil'; 9 | import path from 'path'; 10 | 11 | function getDownloadFileName(): string { 12 | const platform = os.platform(); 13 | const arch = os.arch(); 14 | logger.channel()?.debug(`Platform: ${platform}, Arch: ${arch}`); 15 | 16 | if (platform === "win32") { 17 | if (arch === "x64") { 18 | return "Miniconda3-latest-Windows-x86_64.exe"; 19 | } else if (arch === "ia32") { 20 | return "Miniconda3-latest-Windows-x86.exe"; 21 | } else { 22 | return "Miniconda3-latest-Windows-x86_64.exe"; 23 | } 24 | } else if (platform === "darwin") { 25 | if (arch === "x64") { 26 | return "Miniconda3-latest-MacOSX-x86_64.sh"; 27 | } else if (arch === "arm64") { 28 | return "Miniconda3-latest-MacOSX-arm64.sh"; 29 | } else if (arch === "x86") { 30 | return "Miniconda3-latest-MacOSX-x86.sh"; 31 | } else { 32 | return "Miniconda3-latest-MacOSX-arm64.sh"; 33 | } 34 | } else if (platform === "linux") { 35 | if (arch === "x64") { 36 | return "Miniconda3-latest-Linux-x86_64.sh"; 37 | } else if (arch === "s390x") { 38 | return "Miniconda3-latest-Linux-s390x.sh"; 39 | } else if (arch === "ppc64le") { 40 | return "Miniconda3-latest-Linux-ppc64le.sh"; 41 | } else if (arch === "aarch64") { 42 | return "Miniconda3-latest-Linux-aarch64.sh"; 43 | } else if (arch === "x86") { 44 | return "Miniconda3-latest-Linux-x86.sh"; 45 | } else if (arch === "armv7l") { 46 | return "Miniconda3-latest-Linux-armv7l.sh"; 47 | } else { 48 | return "Miniconda3-latest-Linux-x86_64.sh"; 49 | } 50 | } 51 | 52 | return ""; 53 | } 54 | 55 | export function getMicromambaUrl(): string { 56 | const platform = os.platform(); 57 | const arch = os.arch(); 58 | logger.channel()?.debug(`Platform: ${platform}, Arch: ${arch}`); 59 | 60 | let micromambaUrl = ''; 61 | if (platform === "win32") { 62 | micromambaUrl = "micromamba-win-64"; 63 | } else if (platform === "darwin") { 64 | if (arch === "arm64") { 65 | micromambaUrl = "micromamba-osx-arm64"; 66 | } else if (arch === "x86" || arch === "x64") { 67 | micromambaUrl = "micromamba-osx-64"; 68 | } else { 69 | micromambaUrl = "micromamba-osx-64"; 70 | } 71 | } else if (platform === "linux") { 72 | if (arch === "x64") { 73 | micromambaUrl = "micromamba-linux-64"; 74 | } else if (arch === "ppc64le") { 75 | micromambaUrl = "micromamba-linux-ppc64le"; 76 | } else if (arch === "aarch64") { 77 | micromambaUrl = "micromamba-linux-aarch64"; 78 | } else { 79 | micromambaUrl = "micromamba-linux-64"; 80 | } 81 | } 82 | 83 | const micromambaPath = path.join(UiUtilWrapper.extensionPath(), 'tools', micromambaUrl, "bin", "micromamba"); 84 | return micromambaPath; 85 | } 86 | 87 | export function getCondaDownloadUrl(): string { 88 | return 'https://repo.anaconda.com/miniconda/' + getDownloadFileName(); 89 | } -------------------------------------------------------------------------------- /src/panel/statusBarView.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { dependencyCheck } from './statusBarViewBase'; 4 | import { ProgressBar } from '../util/progressBar'; 5 | import { ASSISTANT_NAME_EN } from '../util/constants'; 6 | import { logger } from '../util/logger'; 7 | 8 | 9 | export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem { 10 | const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); 11 | 12 | // Set the status bar item properties 13 | statusBarItem.text = `$(warning)${ASSISTANT_NAME_EN}`; 14 | statusBarItem.tooltip = `${ASSISTANT_NAME_EN} is checking ..., please wait`; 15 | // when statsBarItem.command is '', then there is "command '' not found" error. 16 | statusBarItem.command = undefined; 17 | 18 | const progressBar = new ProgressBar(); 19 | progressBar.init(); 20 | 21 | // add a timer to update the status bar item 22 | progressBar.update("Checking dependencies", 0); 23 | let hasInstallCommands = false; 24 | 25 | const timer = setInterval(async () => { 26 | try { 27 | progressBar.update("Checking dependencies", 0); 28 | 29 | const devchatStatus = await dependencyCheck(); 30 | if (devchatStatus !== 'has statisfied the dependency' && devchatStatus !== `${ASSISTANT_NAME_EN} has been installed`) { 31 | statusBarItem.text = `$(warning)${ASSISTANT_NAME_EN}`; 32 | statusBarItem.tooltip = `${devchatStatus}`; 33 | 34 | if (devchatStatus === 'Missing required dependency: Python3') { 35 | statusBarItem.command = "devchat.PythonPath"; 36 | } else { 37 | statusBarItem.command = undefined; 38 | } 39 | 40 | // set statusBarItem warning color 41 | progressBar.update(`Checking dependencies: ${devchatStatus}`, 0); 42 | return; 43 | } 44 | 45 | statusBarItem.text = `$(pass)${ASSISTANT_NAME_EN}`; 46 | statusBarItem.tooltip = `ready to chat`; 47 | statusBarItem.command = 'devcaht.onStatusBarClick'; 48 | progressBar.update(`Checking dependencies: Success`, 0); 49 | progressBar.end(); 50 | 51 | // install devchat workflow commands 52 | if (!hasInstallCommands) { 53 | hasInstallCommands = true; 54 | logger.channel()?.debug("Starting local service..."); 55 | await vscode.commands.executeCommand('DevChat.StartLocalService'); 56 | logger.channel()?.debug("Installing commands..."); 57 | await vscode.commands.executeCommand('DevChat.InstallCommands'); 58 | // vscode.commands.executeCommand('DevChat.InstallCommandPython'); 59 | } 60 | 61 | clearInterval(timer); 62 | } catch (error) { 63 | statusBarItem.text = `$(warning)${ASSISTANT_NAME_EN}`; 64 | statusBarItem.tooltip = `Error: ${error}`; 65 | statusBarItem.command = undefined; 66 | progressBar.endWithError(`Checking dependencies: Fail with exception.`); 67 | } 68 | }, 3000); 69 | 70 | // Add the status bar item to the status bar 71 | statusBarItem.show(); 72 | 73 | context.subscriptions.push(statusBarItem); 74 | return statusBarItem; 75 | } 76 | 77 | -------------------------------------------------------------------------------- /assets/devchat_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/handler/codeBlockHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import vscodeMock from '../mocks/vscode'; 4 | import * as proxyquire from 'proxyquire'; 5 | 6 | const proxy = proxyquire.noCallThru(); 7 | 8 | // 使用 proxyquire 加载业务代码,并替换 'vscode' 模块 9 | const { createAndOpenFile } = proxy('../../src/handler/codeBlockHandler', { 'vscode': vscodeMock }); 10 | 11 | describe('createAndOpenFile', () => { 12 | let openTextDocumentStub: sinon.SinonStub; 13 | let showTextDocumentStub: sinon.SinonStub; 14 | 15 | beforeEach(() => { 16 | // 模拟 vscode.workspace.openTextDocument 17 | openTextDocumentStub = sinon.stub(vscodeMock.workspace, 'openTextDocument').resolves(); 18 | // 模拟 vscode.window.showTextDocument 19 | showTextDocumentStub = sinon.stub(vscodeMock.window, 'showTextDocument').resolves(); 20 | }); 21 | 22 | afterEach(() => { 23 | sinon.restore(); 24 | }); 25 | 26 | // 1. happy path: 当提供有效的语言和内容时,应该成功创建并打开一个新文档。 27 | it('当提供有效的语言和内容时,应该成功创建并打开一个新文档', async () => { 28 | const message = { language: 'javascript', content: 'console.log("Hello World");' }; 29 | await createAndOpenFile(message); 30 | expect(openTextDocumentStub.calledOnce).to.be.true; 31 | expect(showTextDocumentStub.calledOnce).to.be.true; 32 | }); 33 | 34 | // 2. happy path: 当提供的语言是VSCode支持的一种常见编程语言时,应该成功创建对应语言的文档。 35 | it('当提供的语言是VSCode支持的一种常见编程语言时,应该成功创建对应语言的文档', async () => { 36 | const message = { language: 'python', content: 'print("Hello World")' }; 37 | await createAndOpenFile(message); 38 | expect(openTextDocumentStub.calledWith(sinon.match.has('language', 'python'))).to.be.true; 39 | expect(showTextDocumentStub.calledOnce).to.be.true; 40 | }); 41 | 42 | // 3. happy path: 当提供的内容是空字符串时,应该成功创建一个空的文档。 43 | it('当提供的内容是空字符串时,应该成功创建一个空的文档', async () => { 44 | const message = { language: 'plaintext', content: '' }; 45 | await createAndOpenFile(message); 46 | expect(openTextDocumentStub.calledWith(sinon.match.has('content', ''))).to.be.true; 47 | expect(showTextDocumentStub.calledOnce).to.be.true; 48 | }); 49 | 50 | // 4. edge case: 当提供的语言不被VSCode支持时,应该抛出错误或以默认语言创建文档。 51 | it('当提供的语言不被VSCode支持时,应该以默认语言创建文档', async () => { 52 | const message = { language: 'nonexistentLanguage', content: 'Hello World' }; 53 | await createAndOpenFile(message); 54 | // TODO: 验证是否以默认语言创建了文档,这需要根据vscode API的实际行为来确定 55 | }); 56 | 57 | // 5. edge case: 当message对象缺少language属性时,应该抛出错误或以默认设置创建文档。 58 | it('当message对象缺少language属性时,应该以默认设置创建文档', async () => { 59 | const message = { content: 'Hello World' }; 60 | await createAndOpenFile(message as any); // 强制类型转换以模拟缺少属性 61 | // TODO: 验证是否以默认设置创建了文档,这需要根据vscode API的实际行为来确定 62 | }); 63 | 64 | // 6. edge case: 当message对象是null时,应该抛出错误,防止函数执行失败。 65 | it('当message对象是null时,应该抛出错误,防止函数执行失败', async () => { 66 | let error; 67 | try { 68 | await createAndOpenFile(null as any); // 强制类型转换以模拟null值 69 | } catch (e) { 70 | error = e; 71 | } 72 | expect(error).to.not.be.null; 73 | }); 74 | }); -------------------------------------------------------------------------------- /assets/devchat_apply.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/util/python_installer/install_devchat.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Install DevChat with python=3.11.4 3 | */ 4 | 5 | import { logger } from "../logger"; 6 | import { appInstall, createEnvByConda, createEnvByMamba } from "./app_install"; 7 | 8 | import * as os from 'os'; 9 | import * as path from 'path'; 10 | import * as fs from 'fs'; 11 | import { UiUtilWrapper } from "../uiUtil"; 12 | import { DevChatConfig } from "../config"; 13 | import { getValidPythonCommand } from "../../contributes/commandsBase"; 14 | 15 | 16 | let isDevChatInstalling: boolean | undefined = undefined; 17 | 18 | export function isDevchatInstalling(): boolean { 19 | if (isDevChatInstalling === true) { 20 | return true; 21 | } 22 | return false; 23 | } 24 | 25 | // python version: 3.11.4 26 | // pkg name: devchat 27 | // return: path to devchat, devchat is located in the same directory as python 28 | export async function installDevchat(): Promise { 29 | try { 30 | // if current os is windows, we don't need to install devchat 31 | if (os.platform() === "win32" && os.arch() === "x64") { 32 | // rewrite ._pth file in python directory 33 | const arch = os.arch(); 34 | const targetPythonPath = os.arch() === "x64"? "python-3.11.6-embed-amd64" : "python-3.11.6-embed-arm64"; 35 | const pythonTargetPath = path.join(UiUtilWrapper.extensionPath(), "tools", targetPythonPath); 36 | const pythonApp = path.join(pythonTargetPath, "python.exe"); 37 | const pythonPathFile = path.join(pythonTargetPath, "python311._pth"); 38 | const sitepackagesPath = path.join(UiUtilWrapper.extensionPath(), "tools", "site-packages"); 39 | 40 | const userHomeDir = os.homedir(); 41 | const WORKFLOWS_BASE_NAME = "scripts"; 42 | const workflow_base_path = path.join(userHomeDir, ".chat", WORKFLOWS_BASE_NAME); 43 | 44 | const new_python_path = [workflow_base_path, sitepackagesPath].join("\n"); 45 | 46 | // read content in pythonPathFile 47 | let content = fs.readFileSync(pythonPathFile, { encoding: 'utf-8' }); 48 | // replace %PYTHONPATH% with sitepackagesPath 49 | content = content.replace(/%PYTHONPATH%/g, new_python_path); 50 | // write content to pythonPathFile 51 | fs.writeFileSync(pythonPathFile, content); 52 | 53 | // update DevChat.PythonForChat configration 54 | await DevChatConfig.getInstance().set("python_for_chat", pythonApp); 55 | return pythonApp; 56 | } else { 57 | // if current os is not windows, we need to get default python path 58 | const pythonPath = getValidPythonCommand(); 59 | if (pythonPath) { 60 | return pythonPath; 61 | } 62 | 63 | logger.channel()?.info(`create env for python ...`); 64 | logger.channel()?.info(`try to create env by mamba ...`); 65 | let pythonCommand = await createEnvByMamba("devchat", "", "3.11.4"); 66 | 67 | if (!pythonCommand || pythonCommand === "") { 68 | logger.channel()?.info(`create env by mamba failed, try to create env by conda ...`); 69 | pythonCommand = await createEnvByConda("devchat", "", "3.11.4"); 70 | } 71 | 72 | if (!pythonCommand) { 73 | logger.channel()?.error('Create env failed'); 74 | logger.channel()?.show(); 75 | return ''; 76 | } 77 | logger.channel()?.info(`Create env success: ${pythonCommand}`); 78 | 79 | await DevChatConfig.getInstance().set("python_for_chat", pythonCommand); 80 | return pythonCommand; 81 | } 82 | } catch (error) { 83 | logger.channel()?.error(`${error}`); 84 | logger.channel()?.show(); 85 | isDevChatInstalling = false; 86 | return ''; 87 | } 88 | } -------------------------------------------------------------------------------- /src/util/localService.ts: -------------------------------------------------------------------------------- 1 | import { spawn, ChildProcess } from 'child_process'; 2 | import { findAvailablePort } from './findServicePort'; 3 | import * as http from 'http'; 4 | import { logger } from './logger'; 5 | import { DevChatConfig } from './config'; 6 | 7 | let serviceProcess: ChildProcess | null = null; 8 | 9 | export async function startLocalService(extensionPath: string, workspacePath: string): Promise { 10 | if (serviceProcess) { 11 | throw new Error('Local service is already running'); 12 | } 13 | 14 | try { 15 | // 1. 获取可用端口号 16 | const port = await findAvailablePort(); 17 | 18 | // 2. 设置环境变量 DC_SVC_PORT 19 | process.env.DC_SVC_PORT = port.toString(); 20 | 21 | // 3. 设置 DC_SVC_WORKSPACE 环境变量 22 | process.env.DC_SVC_WORKSPACE = workspacePath; 23 | 24 | // 新增:设置 PYTHONPATH 环境变量 25 | process.env.PYTHONPATH = `${extensionPath}/tools/site-packages`; 26 | 27 | // 4. 启动进程 python main.py 28 | const mainPyPath = extensionPath + "/tools/site-packages/devchat/_service/main.py"; 29 | const pythonApp = 30 | DevChatConfig.getInstance().get("python_for_chat") || "python3"; 31 | serviceProcess = spawn(pythonApp, [mainPyPath], { 32 | env: { ...process.env }, 33 | stdio: 'inherit', 34 | windowsHide: true, // hide the console window on Windows 35 | }); 36 | 37 | serviceProcess.on('error', (err) => { 38 | logger.channel()?.error('Failed to start local service:', err); 39 | serviceProcess = null; 40 | }); 41 | 42 | serviceProcess.on('exit', (code) => { 43 | logger.channel()?.info(`Local service exited with code ${code}`); 44 | serviceProcess = null; 45 | }); 46 | 47 | // 5. 等待服务启动并验证 48 | await waitForServiceToStart(port); 49 | 50 | // 6. 服务启动成功后,记录启动的端口号到环境变量 51 | process.env.DC_LOCALSERVICE_PORT = port.toString(); 52 | logger.channel()?.info(`Local service port recorded: ${port}`); 53 | 54 | return port; 55 | } catch (error) { 56 | logger.channel()?.error('Error starting local service:', error); 57 | throw error; 58 | } 59 | } 60 | 61 | async function waitForServiceToStart(port: number): Promise { 62 | const maxRetries = 30; 63 | const retryInterval = 1000; // 1 second 64 | 65 | for (let i = 0; i < maxRetries; i++) { 66 | try { 67 | const response = await new Promise((resolve, reject) => { 68 | http.get(`http://localhost:${port}/ping`, (res) => { 69 | let data = ''; 70 | res.on('data', (chunk) => data += chunk); 71 | res.on('end', () => resolve(data)); 72 | }).on('error', reject); 73 | }); 74 | 75 | if (response === '{"message":"pong"}') { 76 | logger.channel()?.info('Local service started successfully'); 77 | return; 78 | } 79 | } catch (error) { 80 | // Ignore errors and continue retrying 81 | } 82 | 83 | await new Promise(resolve => setTimeout(resolve, retryInterval)); 84 | } 85 | 86 | throw new Error('Failed to start local service: timeout'); 87 | } 88 | 89 | export async function stopLocalService(): Promise { 90 | return new Promise((resolve) => { 91 | if (!serviceProcess) { 92 | logger.channel()?.warn('No local service is running'); 93 | resolve(); 94 | return; 95 | } 96 | 97 | serviceProcess.on('exit', () => { 98 | serviceProcess = null; 99 | logger.channel()?.info('Local service stopped'); 100 | resolve(); 101 | }); 102 | 103 | serviceProcess.kill(); 104 | }); 105 | } -------------------------------------------------------------------------------- /src/context/contextDefRefs.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | import { ChatContext } from './contextManager'; 6 | 7 | import { logger } from '../util/logger'; 8 | import { handleCodeSelected } from './contextCodeSelected'; 9 | import { log } from 'console'; 10 | 11 | 12 | async function getSelectedSymbol(): Promise { 13 | const activeEditor = vscode.window.activeTextEditor; 14 | if (!activeEditor) { 15 | return undefined; 16 | } 17 | 18 | const document = activeEditor.document; 19 | const selection = activeEditor.selection; 20 | 21 | const symbols = await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri); 22 | if (!symbols) { 23 | return undefined; 24 | } 25 | 26 | let closestSymbol: vscode.DocumentSymbol | undefined = undefined; 27 | let maxCloseness = -1; 28 | 29 | const checkSymbol = (symbol: vscode.DocumentSymbol) => { 30 | if (symbol.range.start.isAfter(selection.end) || symbol.range.end.isBefore(selection.start)) { 31 | return; 32 | } 33 | 34 | const intersection = Math.max(-2, Math.min(selection.end.line, symbol.range.end.line) - Math.max(selection.start.line, symbol.range.start.line) + 1); 35 | const closeness = intersection / Math.max(selection.end.line - selection.start.line + 1, symbol.range.end.line - symbol.range.start.line + 1); 36 | if (closeness > maxCloseness) { 37 | maxCloseness = closeness; 38 | closestSymbol = symbol; 39 | } 40 | 41 | for (const child of symbol.children) { 42 | checkSymbol(child); 43 | } 44 | }; 45 | 46 | for (const symbol of symbols) { 47 | checkSymbol(symbol); 48 | } 49 | 50 | return closestSymbol; 51 | } 52 | 53 | export const defRefsContext: ChatContext = { 54 | name: 'symbol references', 55 | description: 'find all references of selected symbol', 56 | handler: async () => { 57 | const activeEditor = vscode.window.activeTextEditor; 58 | if (!activeEditor) { 59 | return []; 60 | } 61 | 62 | const document = activeEditor.document; 63 | 64 | // get all references of selected symbol define 65 | const selection = activeEditor.selection; 66 | if (selection.isEmpty) { 67 | logger.channel()?.error(`Error: no selected text!`); 68 | logger.channel()?.show(); 69 | return []; 70 | } 71 | const symbolText = document.getText(selection); 72 | 73 | logger.channel()?.info(`selected text: ${document.uri} ${symbolText} ${selection.start}`); 74 | 75 | // 获取selectedSymbol的引用信息 76 | let contextList: string[] = []; 77 | let refLocations; 78 | try { 79 | refLocations = await vscode.commands.executeCommand( 80 | 'vscode.executeReferenceProvider', 81 | document.uri, 82 | selection.start 83 | ); 84 | } catch (error) { 85 | logger.channel()?.error(`secretStorageGet error: ${error}`); 86 | return []; 87 | } 88 | 89 | if (refLocations) { 90 | // find symbol include refLocation symbol 91 | for (const refLocation of refLocations) { 92 | const refLocationFile = refLocation.uri.fsPath; 93 | const documentNew = await vscode.workspace.openTextDocument(refLocationFile); 94 | 95 | const startLine = refLocation.range.start.line - 2 > 0 ? refLocation.range.start.line - 2 : 0; 96 | const renageNew = new vscode.Range(startLine, 0, refLocation.range.end.line + 2, 10000); 97 | contextList.push(await handleCodeSelected(refLocationFile, documentNew.getText(renageNew), startLine)); 98 | } 99 | } else { 100 | logger.channel()?.info(`no reference found!`); 101 | } 102 | return contextList; 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /src/util/python_installer/app_install.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Install devchat 3 | */ 4 | 5 | import { logger } from "../logger"; 6 | import { installConda } from "./conda_install"; 7 | import { getMicromambaUrl } from "./conda_url"; 8 | import { installPackage } from "./package_install"; 9 | import { installPython, installPythonMicromamba } from "./python_install"; 10 | 11 | // step 1. install conda 12 | // step 2. create env with python 3.11.4 13 | // step 3. install devchat in the env 14 | 15 | 16 | export async function createEnvByMamba(pkgName: string, pkgVersion: string, pythonVersion: string) : Promise { 17 | logger.channel()?.info('Find micromamba ...'); 18 | const mambaCommand = getMicromambaUrl(); 19 | logger.channel()?.info('micromamba url: ' + mambaCommand); 20 | 21 | // create env with specify python 22 | logger.channel()?.info('Create env ...'); 23 | let pythonCommand = ''; 24 | // try 3 times 25 | for (let i = 0; i < 3; i++) { 26 | try { 27 | pythonCommand = await installPythonMicromamba(mambaCommand, pkgName, pythonVersion); 28 | if (pythonCommand) { 29 | break; 30 | } 31 | } catch(error) { 32 | logger.channel()?.info(`Exception: ${error}`); 33 | } 34 | 35 | logger.channel()?.info(`Create env failed, try again: ${i + 1}`); 36 | } 37 | 38 | return pythonCommand; 39 | } 40 | 41 | export async function createEnvByConda(pkgName: string, pkgVersion: string, pythonVersion: string) : Promise { 42 | // install conda 43 | logger.channel()?.info('Install conda ...'); 44 | const condaCommand = await installConda(); 45 | if (!condaCommand) { 46 | logger.channel()?.error('Install conda failed'); 47 | logger.channel()?.show(); 48 | return ''; 49 | } 50 | 51 | // create env with specify python 52 | logger.channel()?.info('Create env ...'); 53 | let pythonCommand = ''; 54 | // try 3 times 55 | for (let i = 0; i < 3; i++) { 56 | try { 57 | pythonCommand = await installPython(condaCommand, pkgName, pythonVersion); 58 | if (pythonCommand) { 59 | break; 60 | } 61 | } catch(error) { 62 | logger.channel()?.info(`Exception: ${error}`); 63 | } 64 | 65 | logger.channel()?.info(`Create env failed, try again: ${i + 1}`); 66 | } 67 | 68 | return pythonCommand; 69 | } 70 | 71 | export async function appInstall(pkgName: string, pkgVersion: string, pythonVersion: string) : Promise { 72 | logger.channel()?.info(`create env for python ...`); 73 | logger.channel()?.info(`try to create env by mamba ...`); 74 | let pythonCommand = await createEnvByMamba(pkgName, pkgVersion, pythonVersion); 75 | 76 | if (!pythonCommand || pythonCommand === "") { 77 | logger.channel()?.info(`create env by mamba failed, try to create env by conda ...`); 78 | pythonCommand = await createEnvByConda(pkgName, pkgVersion, pythonVersion); 79 | } 80 | 81 | if (!pythonCommand) { 82 | logger.channel()?.error('Create env failed'); 83 | logger.channel()?.show(); 84 | return ''; 85 | } 86 | logger.channel()?.info(`Create env success: ${pythonCommand}`); 87 | 88 | // install devchat in the env 89 | logger.channel()?.info('Install python packages ...'); 90 | let isInstalled = false; 91 | // try 3 times 92 | for (let i = 0; i < 4; i++) { 93 | let otherSource: string | undefined = undefined; 94 | if (i>1) { 95 | otherSource = 'https://pypi.tuna.tsinghua.edu.cn/simple/'; 96 | } 97 | isInstalled = await installPackage(pythonCommand, pkgName + pkgVersion, otherSource); 98 | if (isInstalled) { 99 | break; 100 | } 101 | logger.channel()?.info(`Install packages failed, try again: ${i + 1}`); 102 | } 103 | if (!isInstalled) { 104 | logger.channel()?.error('Install packages failed'); 105 | logger.channel()?.show(); 106 | return ''; 107 | } 108 | 109 | return pythonCommand; 110 | } -------------------------------------------------------------------------------- /src/contributes/codecomplete/ast/findIdentifiers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 针对代码补全功能,构建prompt 3 | 4 | prompt的好坏,取决于提供的上下文信息。 5 | 通过AST获取相对完整的信息,可能会增加提示的准确度,但也会增加代码提示的复杂度。 6 | */ 7 | 8 | import { logger } from "../../../util/logger"; 9 | import * as vscode from "vscode"; 10 | import { getAst, getTreePathAtCursor, RangeInFileWithContents } from "./ast"; 11 | import Parser from "web-tree-sitter"; 12 | import { getCommentPrefix, getLangageFunctionConfig, LanguageFunctionsConfig } from "./language"; 13 | import { getLanguageForFile, getQueryFunctionsSource } from "./treeSitter"; 14 | import MemoryCacheManager from "../cache"; 15 | 16 | const identifierQueryCache: MemoryCacheManager = new MemoryCacheManager(4); 17 | 18 | export async function visitAstNode(node: Parser.SyntaxNode, identifiers: Parser.SyntaxNode[], startLine: number | undefined = undefined, endLine: number | undefined = undefined) { 19 | const regex = /^[a-zA-Z_][0-9a-zA-Z_]*$/; 20 | // visit children of node 21 | for (let child of node.children) { 22 | // visit children, if child has children nodes 23 | if (child.childCount > 0) { 24 | if (startLine !== undefined && endLine !== undefined) { 25 | if (child.startPosition.row > endLine || child.endPosition.row < startLine) { 26 | continue; 27 | } 28 | visitAstNode(child, identifiers, startLine, endLine); 29 | } else { 30 | visitAstNode(child, identifiers, startLine, endLine); 31 | } 32 | } else { 33 | const isIdentifier = regex.test(child.text); 34 | 35 | if (startLine !== undefined && endLine!== undefined) { 36 | if (child.startPosition.row >= startLine && child.endPosition.row <= endLine) { 37 | if (isIdentifier && child.text.length >= 3) { 38 | identifiers.push(child); 39 | } 40 | } 41 | } else { 42 | if (isIdentifier && child.text.length >= 3) { 43 | identifiers.push(child); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | export async function findIdentifiersInAstNodeRange(node: Parser.SyntaxNode, startLine: number, endLine: number): Promise { 51 | const identifiers: Parser.SyntaxNode[] = []; 52 | await visitAstNode(node, identifiers, startLine, endLine); 53 | return identifiers; 54 | } 55 | 56 | export async function findIdentifiers(filepath: string, node: Parser.SyntaxNode): Promise { 57 | try { 58 | const lang = await getLanguageForFile(filepath); 59 | if (!lang) { 60 | return []; 61 | } 62 | 63 | let querySource = "(identifier) @identifier"; 64 | const extension = filepath.split('.').pop() || ''; 65 | if (extension === 'kt') { 66 | querySource = "(simple_identifier) @identifier"; 67 | } 68 | 69 | try { 70 | let query: Parser.Query | undefined = identifierQueryCache.get(extension); 71 | if (!query) { 72 | query = lang?.query(querySource); 73 | identifierQueryCache.set(extension, query); 74 | } 75 | const matches = query?.matches(node); 76 | 77 | if (!matches || matches.length === 0) { 78 | let identifiers: Parser.SyntaxNode[] = []; 79 | await visitAstNode(node, identifiers); 80 | return identifiers; 81 | } else { 82 | return matches?.map((match) => match.captures[0].node) ?? []; 83 | } 84 | } catch(error) { 85 | let identifiers: Parser.SyntaxNode[] = []; 86 | await visitAstNode(node, identifiers); 87 | return identifiers; 88 | } 89 | } catch (error) { 90 | logger.channel()?.error(`findIdentifiers error: ${error}`); 91 | return []; 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /test/workflows/ui_parser.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | checkbox: 4 | ```chatmark type=form 5 | Which files would you like to commit? I've suggested a few. 6 | > [x](file1) devchat/engine/prompter.py 7 | > [x](file2) devchat/prompt.py 8 | > [](file3) tests/test_cli_prompt.py 9 | ``` 10 | 11 | radio: 12 | ```chatmark type=form 13 | How would you like to make the change? 14 | > - (insert) Insert the new code. 15 | > - (new) Put the code in a new file. 16 | > - (replace) Replace the current code. 17 | ``` 18 | 19 | editor: 20 | ```chatmark type=form 21 | I've drafted a commit message for you as below. Feel free to modify it. 22 | 23 | > | (ID) 24 | > fix: prevent racing of requests 25 | > 26 | > Introduce a request id and a reference to latest request. Dismiss 27 | > incoming responses other than from latest request. 28 | > 29 | > Reviewed-by: Z 30 | > Refs: #123 31 | ``` 32 | """ 33 | 34 | 35 | import re 36 | 37 | def extract_ui_blocks(text): 38 | # 定义用于提取各种UI块的正则表达式 39 | ui_block_pattern = re.compile(r'```chatmark.*?\n(.*?)\n```', re.DOTALL) 40 | return ui_block_pattern.findall(text) 41 | 42 | def parse_ui_block(block): 43 | # 解析checkbox 44 | result = [] 45 | checkbox_pattern = re.compile(r'> \[(x|)\]\((.*?)\)') 46 | checkboxes = checkbox_pattern.findall(block) 47 | if checkboxes: 48 | result.append({ 49 | 'type': 'checkbox', 50 | 'items': [{'checked': bool(x.strip()), 'id': file_id} for x, file_id in checkboxes] 51 | }) 52 | 53 | # 解析radio 54 | radio_pattern = re.compile(r'> - \((.*?)\)') 55 | radios = radio_pattern.findall(block) 56 | if radios: 57 | result.append({ 58 | 'type': 'radio', 59 | 'items': [{'id': radio_id} for radio_id in radios] 60 | }) 61 | 62 | # 解析editor 63 | editor_pattern = re.compile(r'> \| \((.*?)\)\n((?:> .*\n?)*)', re.DOTALL) 64 | editor_match = editor_pattern.search(block) 65 | if editor_match: 66 | editor_id = editor_match.group(1) 67 | editor_text = editor_match.group(2).strip().replace('> ', '') 68 | result.append({ 69 | 'type': 'editor', 70 | 'id': editor_id, 71 | 'text': editor_text 72 | }) 73 | 74 | return result 75 | 76 | def parse_ui_description(text): 77 | # 提取所有UI块 78 | blocks = extract_ui_blocks(text) 79 | # 解析每个UI块并返回结果 80 | return [parse_ui_block(block) for block in blocks if parse_ui_block(block)] 81 | 82 | 83 | def test_ui(): 84 | description = """ 85 | checkbox: 86 | ```chatmark type=form 87 | Which files would you like to commit? I've suggested a few. 88 | > [x](file1) devchat/engine/prompter.py 89 | > [x](file2) devchat/prompt.py 90 | > [](file3) tests/test_cli_prompt.py 91 | ``` 92 | 93 | radio: 94 | ```chatmark type=form 95 | How would you like to make the change? 96 | > - (insert) Insert the new code. 97 | > - (new) Put the code in a new file. 98 | > - (replace) Replace the current code. 99 | ``` 100 | 101 | editor: 102 | ```chatmark type=form 103 | I've drafted a commit message for you as below. Feel free to modify it. 104 | 105 | > | (ID) 106 | > fix: prevent racing of requests 107 | > 108 | > Introduce a request id and a reference to latest request. Dismiss 109 | > incoming responses other than from latest request. 110 | > 111 | > Reviewed-by: Z 112 | > Refs: #123 113 | ``` 114 | 115 | checkbox and radio: 116 | ```chatmark type=form 117 | Which files would you like to commit? I've suggested a few. 118 | > [x](file1) devchat/engine/prompter.py 119 | > [x](file2) devchat/prompt.py 120 | > [](file3) tests/test_cli_prompt.py 121 | 122 | How would you like to make the change? 123 | > - (insert) Insert the new code. 124 | > - (new) Put the code in a new file. 125 | > - (replace) Replace the current code. 126 | ``` 127 | """ 128 | 129 | parsed_data = parse_ui_description(description) 130 | for ui_element in parsed_data: 131 | print(ui_element) 132 | 133 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/astIndex/createIdentifierSet.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import Parser from "web-tree-sitter"; 3 | import { BlockInfo, FileBlockInfo } from "./types"; 4 | import { getAst } from "../ast/ast"; 5 | import { findFunctionRanges } from "../ast/findFunctions"; 6 | import { FunctionInfo, splitFileIntoBlocks } from "../symbolindex/embedding"; 7 | import { findIdentifiers } from "../ast/findIdentifiers"; 8 | 9 | 10 | 11 | // create FileBlockInfo by file 12 | export async function createFileBlockInfo(file: string): Promise { 13 | // 读取文件内容 14 | const contents = fs.readFileSync(file, 'utf-8'); 15 | const fileBlocInfok: FileBlockInfo = { 16 | path: file, 17 | // 记录文件的最后修改时间 18 | lastTime: fs.statSync(file).mtimeMs, 19 | // 根据文件内容计算唯一的hashKey,类似sha 20 | hashKey: await createHashKey(contents), 21 | blocks: [], 22 | }; 23 | 24 | // 对文件进行AST解析 25 | const ast: Parser.Tree | undefined = await getAst(file, contents, false); 26 | if (!ast) { 27 | return fileBlocInfok; 28 | } 29 | 30 | // 获取函数范围 31 | const functionRanges = await findFunctionRanges(file, ast.rootNode); 32 | // 根据函数的范围,将分为拆分为不同的块 33 | const functionNewRanges: FunctionInfo[] = functionRanges.map((func) => { 34 | return { 35 | startLine: func.define.start.row, 36 | endLine: func.body.end.row, 37 | }; 38 | }); 39 | 40 | // 计算总行数 41 | const contentLines = contents.split('\n'); 42 | const totalLines = contentLines.length; 43 | 44 | // 将文件拆分为不同的块 45 | const blocks = splitFileIntoBlocks(functionNewRanges, totalLines); 46 | 47 | // 获取文件中所有identifiers 48 | const identifiers: Parser.SyntaxNode[] = await findIdentifiers(file, ast.rootNode); 49 | 50 | // 根据identifier的位置,将其归属到对应范围的block中 51 | // block的顺序是从小到大排序,identifier也是从小到大排序,利用这个特性优化查找 52 | const blockIdentifiers: Map = new Map(); 53 | let currentBlockIndex = 0; 54 | identifiers.sort((a, b) => a.startPosition.row - b.startPosition.row); 55 | for (const identifier of identifiers) { 56 | while (currentBlockIndex < blocks.length && blocks[currentBlockIndex].endLine < identifier.startPosition.row) { 57 | currentBlockIndex++; 58 | } 59 | if (currentBlockIndex < blocks.length && blocks[currentBlockIndex].startLine <= identifier.startPosition.row) { 60 | if (!blockIdentifiers.has(currentBlockIndex)) { 61 | blockIdentifiers.set(currentBlockIndex, []); 62 | } 63 | blockIdentifiers.get(currentBlockIndex)?.push(identifier); 64 | } 65 | } 66 | 67 | // 遍历每个block,将其中的identifiers转化为BlockInfo 68 | for (const [blockIndex, identifiers] of blockIdentifiers) { 69 | const block = blocks[blockIndex]; 70 | const blockInfo: BlockInfo = { 71 | file: file, 72 | startLine: block.startLine, 73 | endLine: block.endLine, 74 | lastTime: fs.statSync(file).mtimeMs, 75 | identifiers: identifiers.map((identifier) => identifier.text).sort(), 76 | }; 77 | fileBlocInfok.blocks.push(blockInfo); 78 | } 79 | 80 | return fileBlocInfok; 81 | } 82 | 83 | export async function createIdentifierSetByQuery(file: string, query: string): Promise { 84 | const ast: Parser.Tree | undefined = await getAst(file, query, false); 85 | if (!ast) { 86 | return []; 87 | } 88 | 89 | return createIdentifierSetByQueryAst(file, ast.rootNode); 90 | } 91 | 92 | export async function createIdentifierSetByQueryAst(filepath: string, node: Parser.SyntaxNode): Promise { 93 | const identifiers: Parser.SyntaxNode[] = await findIdentifiers(filepath, node); 94 | return identifiers.map((identifier) => identifier.text).sort(); 95 | } 96 | 97 | export async function createHashKey(contents: string): Promise { 98 | return require('crypto').createHash('sha256').update(contents).digest('hex'); 99 | } -------------------------------------------------------------------------------- /src/handler/configHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Commands for handling configuration read and write 3 | */ 4 | 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import * as vscode from 'vscode'; 8 | import yaml from 'yaml'; 9 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 10 | import { MessageHandler } from './messageHandler'; 11 | import { DevChatConfig } from '../util/config'; 12 | import { logger } from '../util/logger'; 13 | 14 | 15 | // 读取YAML配置文件的函数 16 | function readYamlConfigFile(configFilePath: string): any { 17 | try { 18 | // 如果配置文件不存在,创建一个空文件 19 | if (!fs.existsSync(configFilePath)) { 20 | fs.mkdirSync(path.dirname(configFilePath), { recursive: true }); 21 | fs.writeFileSync(configFilePath, '', 'utf8'); 22 | } 23 | 24 | const fileContents = fs.readFileSync(configFilePath, 'utf8'); 25 | const data = yaml.parse(fileContents) || {}; 26 | 27 | return data; 28 | } catch (error) { 29 | logger.channel()?.error(`Error reading the config file: ${error}`); 30 | logger.channel()?.show(); 31 | return {}; 32 | } 33 | } 34 | 35 | // 写入YAML配置文件的函数 36 | function writeYamlConfigFile(configFilePath: string, data: any): void { 37 | try { 38 | const yamlStr = yaml.stringify(data); 39 | fs.writeFileSync(configFilePath, yamlStr, 'utf8'); 40 | } catch (error) { 41 | logger.channel()?.error(`Error writing the config file: ${error}`); 42 | logger.channel()?.show(); 43 | } 44 | } 45 | 46 | regInMessage({command: 'readConfig', key: ['A','B']}); // when key is "", it will get all config values 47 | regOutMessage({command: 'readConfig', key: ['A', 'B'], value: 'any'}); 48 | export async function readConfig(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 49 | if (message.key === '' || message.key === '*' || message.key.length === 0 || message.key[1] === '*') { 50 | const config = DevChatConfig.getInstance().getAll(); 51 | MessageHandler.sendMessage(panel, {command: 'readConfig', key: message.key, value: config}); 52 | } else { 53 | const config = DevChatConfig.getInstance().get(message.key); 54 | MessageHandler.sendMessage(panel, {command: 'readConfig', key: message.key, value: config}); 55 | } 56 | } 57 | 58 | regInMessage({command: 'writeConfig', key: ['A', 'B'], value: 'any'}); // when key is "", it will rewrite all config values 59 | export async function writeConfig(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 60 | if (message.key === '' || message.key === '*' || message.key.length === 0 || message.key[1] === '*') { 61 | DevChatConfig.getInstance().setAll(message.value); 62 | } else { 63 | DevChatConfig.getInstance().set(message.key, message.value); 64 | } 65 | } 66 | 67 | regInMessage({command: 'readServerConfigBase', key: ['A','B']}); // when key is "", it will get all config values 68 | regOutMessage({command: 'readServerConfigBase', key: ['A', 'B'], value: 'any'}); 69 | export async function readServerConfigBase(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 70 | const serverConfigFile = path.join(process.env.HOME || process.env.USERPROFILE || '', '.chat', 'server_config.yml'); 71 | const config = readYamlConfigFile(serverConfigFile); 72 | if (!config) { 73 | MessageHandler.sendMessage(panel, {command: 'readServerConfigBase', value: {}}); 74 | } else { 75 | MessageHandler.sendMessage(panel, {command: 'readServerConfigBase', value: config}); 76 | } 77 | } 78 | 79 | regInMessage({command: 'writeServerConfigBase', key: ['A', 'B'], value: 'any'}); // when key is "", it will rewrite all config values 80 | export async function writeServerConfigBase(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 81 | const serverConfigFile = path.join(process.env.HOME || process.env.USERPROFILE || '', '.chat', 'server_config.yml'); 82 | const config = message.value; 83 | writeYamlConfigFile(serverConfigFile, config); 84 | } -------------------------------------------------------------------------------- /src/util/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import yaml from 'yaml'; 3 | import path from 'path'; 4 | import { logger } from './logger'; 5 | 6 | 7 | export class DevChatConfig { 8 | private static instance: DevChatConfig; 9 | // 配置文件路径,根据操作系统的差异,可能需要调整 10 | private configFilePath: string; 11 | private data: any; 12 | // last modify timestamp of the config file 13 | private lastModifyTime: number; 14 | 15 | private constructor() { 16 | // 视操作系统的差异,可能需要调整路径 ~/.chat/config.yml 17 | this.configFilePath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.chat', 'config.yml'); 18 | this.lastModifyTime = 0; 19 | this.readConfigFile(); 20 | } 21 | 22 | public static getInstance(): DevChatConfig { 23 | if (!DevChatConfig.instance) { 24 | DevChatConfig.instance = new DevChatConfig(); 25 | } 26 | return DevChatConfig.instance; 27 | } 28 | 29 | private readConfigFile() { 30 | try { 31 | // if config file not exist, create a empty file 32 | if (!fs.existsSync(this.configFilePath)) { 33 | fs.mkdirSync(path.dirname(this.configFilePath), { recursive: true }); 34 | fs.writeFileSync(this.configFilePath, '', 'utf8'); 35 | } 36 | 37 | const fileContents = fs.readFileSync(this.configFilePath, 'utf8'); 38 | this.data = yaml.parse(fileContents) ?? {}; 39 | this.lastModifyTime = fs.statSync(this.configFilePath).mtimeMs; 40 | } catch (error) { 41 | logger.channel()?.error(`Error reading the config file: ${error}`); 42 | logger.channel()?.show(); 43 | this.data = {}; 44 | } 45 | } 46 | 47 | private writeConfigFile() { 48 | try { 49 | const yamlStr = yaml.stringify(this.data); 50 | fs.writeFileSync(this.configFilePath, yamlStr, 'utf8'); 51 | } catch (error) { 52 | logger.channel()?.error(`Error writing the config file: ${error}`); 53 | logger.channel()?.show(); 54 | } 55 | } 56 | 57 | public get(key: string | string[], defaultValue: any = undefined): any { 58 | // check if the config file has been modified 59 | const currentModifyTime = fs.statSync(this.configFilePath).mtimeMs; 60 | if (currentModifyTime > this.lastModifyTime) { 61 | this.readConfigFile(); 62 | } 63 | 64 | let keys: string[] = []; 65 | 66 | if (typeof key === 'string') { 67 | keys = key.split('.'); 68 | } else { 69 | keys = key; 70 | } 71 | 72 | let value = this.data; 73 | for (const k of keys) { 74 | if (value && typeof value === 'object' && k in value) { 75 | value = value[k]; 76 | } else { 77 | // If the key is not found or value is not an object, return the default value 78 | return defaultValue || undefined; 79 | } 80 | } 81 | 82 | return value; 83 | } 84 | 85 | public set(key: string | string[], value: any): void { 86 | let keys: string[] = []; 87 | 88 | if (typeof key === 'string') { 89 | keys = key.split('.'); 90 | } else { 91 | keys = key; 92 | } 93 | 94 | let lastKey = keys.pop(); 95 | let lastObj = keys.reduce((prev, k) => { 96 | if (!prev[k]) { 97 | prev[k] = {}; 98 | } 99 | return prev[k]; 100 | }, this.data); // 这创建一个嵌套的对象结构,如果不存在的话 101 | if (lastKey) { 102 | lastObj[lastKey] = value; // 设置值 103 | } 104 | this.writeConfigFile(); // 更新配置文件 105 | } 106 | 107 | public getAll(): any { 108 | // check if the config file has been modified 109 | const currentModifyTime = fs.statSync(this.configFilePath).mtimeMs; 110 | if (currentModifyTime > this.lastModifyTime) { 111 | this.readConfigFile(); 112 | } 113 | 114 | return this.data; 115 | } 116 | 117 | public setAll(newData: any): void { 118 | this.data = newData; 119 | this.writeConfigFile(); // 更新配置文件 120 | } 121 | } -------------------------------------------------------------------------------- /src/util/uiUtil_vscode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { ExtensionContextHolder } from './extensionContext'; 4 | import { UiUtil } from './uiUtil'; 5 | import { logger } from './logger'; 6 | 7 | 8 | export class UiUtilVscode implements UiUtil { 9 | public async languageId(uri: string): Promise { 10 | const document = await vscode.workspace.openTextDocument(uri); 11 | return document.languageId; 12 | } 13 | public workspaceFoldersFirstPath(): string | undefined { 14 | return vscode.workspace.workspaceFolders?.[0].uri.fsPath; 15 | } 16 | 17 | public async secretStorageGet(key: string): Promise { 18 | try { 19 | const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets; 20 | let openaiApiKey = await secretStorage.get(key); 21 | return openaiApiKey; 22 | } catch (error) { 23 | logger.channel()?.error(`secretStorageGet error: ${error}`); 24 | return undefined; 25 | } 26 | } 27 | public async writeFile(uri: string, content: string): Promise { 28 | await vscode.workspace.fs.writeFile(vscode.Uri.file(uri), Buffer.from(content)); 29 | } 30 | public async showInputBox(option: object): Promise { 31 | return vscode.window.showInputBox(option); 32 | } 33 | public async storeSecret(key: string, value: string): Promise { 34 | const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets; 35 | await secretStorage.store(key, value); 36 | } 37 | public extensionPath(): string { 38 | return ExtensionContextHolder.context!.extensionUri.fsPath; 39 | } 40 | public runTerminal(terminalName: string, command: string): void { 41 | const terminals = vscode.window.terminals; 42 | for (const terminal of terminals) { 43 | if (terminal.name === terminalName) { 44 | terminal.dispose(); 45 | } 46 | } 47 | const terminal = vscode.window.createTerminal(terminalName); 48 | terminal.sendText(command); 49 | terminal.show(); 50 | } 51 | 52 | // current active file path 53 | public activeFilePath(): string | undefined { 54 | const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); 55 | 56 | if (validVisibleTextEditors.length > 1) { 57 | vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`); 58 | return undefined; 59 | } 60 | 61 | const editor = validVisibleTextEditors[0]; 62 | if (!editor) { 63 | return undefined; 64 | } 65 | 66 | return editor.document.fileName; 67 | } 68 | // current selected range, return undefined if no selection 69 | public selectRange(): [number, number] | undefined { 70 | const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); 71 | 72 | if (validVisibleTextEditors.length > 1) { 73 | vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`); 74 | return undefined; 75 | } 76 | 77 | const editor = validVisibleTextEditors[0]; 78 | if (!editor) { 79 | return undefined; 80 | } 81 | 82 | if (editor.selection.isEmpty) { 83 | return undefined; 84 | } 85 | 86 | return [editor.selection.start.character, editor.selection.end.character]; 87 | } 88 | // current selected text 89 | public selectText(): string | undefined { 90 | const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); 91 | 92 | if (validVisibleTextEditors.length > 1) { 93 | vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`); 94 | return undefined; 95 | } 96 | 97 | const editor = validVisibleTextEditors[0]; 98 | if (!editor) { 99 | return undefined; 100 | } 101 | 102 | if (editor.selection.isEmpty) { 103 | return undefined; 104 | } 105 | 106 | return editor.document.getText(editor.selection); 107 | } 108 | 109 | public showErrorMessage(message: string): void { 110 | vscode.window.showErrorMessage(message); 111 | } 112 | 113 | public async getLSPBrigePort(): Promise { 114 | const port = await vscode.commands.executeCommand('LangBrige.getAddress') as number | undefined;; 115 | return port; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/ast/treeSitter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is copied from Continut repo. 3 | */ 4 | 5 | import { logger } from "../../../util/logger"; 6 | import { UiUtilWrapper } from "../../../util/uiUtil"; 7 | import * as path from "path"; 8 | import * as fs from "fs"; 9 | import { Language } from "web-tree-sitter"; 10 | import Parser = require("web-tree-sitter"); 11 | import { getLanguageFullName, supportedLanguages } from "./language"; 12 | import MemoryCacheManager from "../cache"; 13 | 14 | 15 | const parserCache: MemoryCacheManager = new MemoryCacheManager(4); 16 | const langCache: MemoryCacheManager = new MemoryCacheManager(4); 17 | 18 | export async function getParserForFile(filepath: string) { 19 | if (process.env.IS_BINARY) { 20 | return undefined; 21 | } 22 | 23 | try { 24 | const extension = filepath.split('.').pop() || ''; 25 | const cachedParser = parserCache.get(extension); 26 | if (cachedParser) { 27 | return cachedParser; 28 | } 29 | 30 | await Parser.init({ 31 | locateFile(filename) { 32 | if (filename === 'tree-sitter.wasm') { 33 | // Return the path where you have placed the tree-sitter.wasm file 34 | const wasmPath = path.join( 35 | UiUtilWrapper.extensionPath(), 36 | "tools", 37 | "tree-sitter-wasms", 38 | `tree-sitter.wasm`, 39 | ); 40 | return wasmPath; 41 | } 42 | return filename; 43 | } 44 | }); 45 | 46 | const language = await getLanguageForFile(filepath); 47 | if (!language) { 48 | return undefined; 49 | } 50 | 51 | const parser = new Parser(); 52 | parser.setLanguage(language); 53 | 54 | parserCache.set(extension, parser); 55 | return parser; 56 | } catch (e) { 57 | logger.channel()?.warn("Unable to load language for file", filepath, e); 58 | return undefined; 59 | } 60 | } 61 | 62 | export async function getLanguageForFile( 63 | filepath: string, 64 | ): Promise { 65 | try { 66 | await Parser.init(); 67 | const extension = filepath.split('.').pop() || ''; 68 | const cachedLang = langCache.get(extension); 69 | if (cachedLang) { 70 | return cachedLang; 71 | } 72 | 73 | if (!supportedLanguages[extension]) { 74 | return undefined; 75 | } 76 | 77 | const wasmPath = path.join( 78 | UiUtilWrapper.extensionPath(), 79 | "tools", 80 | "tree-sitter-wasms", 81 | `tree-sitter-${supportedLanguages[extension]}.wasm`, 82 | ); 83 | if (!fs.existsSync(wasmPath)) { 84 | return undefined; 85 | } 86 | 87 | const language = await Parser.Language.load(wasmPath); 88 | 89 | langCache.set(extension, language); 90 | return language; 91 | } catch (e) { 92 | logger.channel()?.warn("Unable to load language for file:", filepath, e); 93 | return undefined; 94 | } 95 | } 96 | 97 | export async function getQueryVariablesSource(filepath: string) { 98 | const fullLangName = await getLanguageFullName(filepath); 99 | if (!fullLangName) { 100 | return ""; 101 | } 102 | const sourcePath = path.join( 103 | UiUtilWrapper.extensionPath(), 104 | "tools", 105 | "tree-sitter-queries", 106 | fullLangName, 107 | "variables.scm", 108 | ); 109 | if (!fs.existsSync(sourcePath)) { 110 | return ""; 111 | } 112 | return fs.readFileSync(sourcePath).toString(); 113 | } 114 | 115 | export async function getQueryFunctionsSource(filepath: string) { 116 | const fullLangName = await getLanguageFullName(filepath); 117 | if (!fullLangName) { 118 | return ""; 119 | } 120 | const sourcePath = path.join( 121 | UiUtilWrapper.extensionPath(), 122 | "tools", 123 | "tree-sitter-queries", 124 | fullLangName, 125 | "functions.scm", 126 | ); 127 | if (!fs.existsSync(sourcePath)) { 128 | return ""; 129 | } 130 | return fs.readFileSync(sourcePath).toString(); 131 | } 132 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/ast/ast.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import Parser from "web-tree-sitter"; 3 | import { getParserForFile } from "./treeSitter"; 4 | import MemoryCacheManager from "../cache"; 5 | import * as crypto from 'crypto'; 6 | import { UiUtilWrapper } from "../../../util/uiUtil"; 7 | 8 | 9 | 10 | export interface RangeInFileWithContents { 11 | filepath: string; 12 | range: { 13 | start: { line: number; character: number }; 14 | end: { line: number; character: number }; 15 | }; 16 | contents: string; 17 | } 18 | 19 | // cache ast results 20 | const astCache: MemoryCacheManager = new MemoryCacheManager(30); 21 | 22 | export async function getAst( 23 | filepath: string, 24 | fileContents: string, 25 | cacheEnable: boolean = true 26 | ): Promise { 27 | // calulate hash for file contents, then use that hash as cache key 28 | const hash = crypto.createHash('sha256'); 29 | hash.update(fileContents); 30 | const cacheKey = hash.digest('hex'); 31 | 32 | const cachedAst: {ast: Parser.Tree, hash: string} = astCache.get(filepath); 33 | if (cachedAst && cachedAst.hash === cacheKey) { 34 | return cachedAst.ast; 35 | } 36 | 37 | const parser = await getParserForFile(filepath); 38 | if (!parser) { 39 | return undefined; 40 | } 41 | 42 | try { 43 | const ast = parser.parse(fileContents); 44 | if (cacheEnable) { 45 | astCache.set(filepath, {ast, hash: cacheKey}); 46 | } 47 | return ast; 48 | } catch (e) { 49 | return undefined; 50 | } 51 | } 52 | 53 | export async function getTreePathAtCursor( 54 | ast: Parser.Tree, 55 | cursorIndex: number, 56 | ): Promise { 57 | const path = [ast.rootNode]; 58 | while (path[path.length - 1].childCount > 0) { 59 | let foundChild = false; 60 | for (let child of path[path.length - 1].children) { 61 | if (child.startIndex <= cursorIndex && child.endIndex >= cursorIndex) { 62 | path.push(child); 63 | foundChild = true; 64 | break; 65 | } 66 | } 67 | 68 | if (!foundChild) { 69 | break; 70 | } 71 | } 72 | 73 | return path; 74 | } 75 | 76 | export async function getAstNodeByRange( ast: Parser.Tree, line: number, character: number): Promise { 77 | let node = ast.rootNode; 78 | 79 | if (node.childCount > 0) { 80 | for (let child of node.children) { 81 | if (child.startPosition.row <= line && child.endPosition.row >= line) { 82 | return child; 83 | } 84 | } 85 | } 86 | 87 | return undefined; 88 | } 89 | 90 | export async function getScopeAroundRange( 91 | range: RangeInFileWithContents, 92 | ): Promise { 93 | const ast = await getAst(range.filepath, range.contents); 94 | if (!ast) { 95 | return undefined; 96 | } 97 | 98 | const { start: s, end: e } = range.range; 99 | const lines = range.contents.split("\n"); 100 | const startIndex = 101 | lines.slice(0, s.line).join("\n").length + 102 | (lines[s.line]?.slice(s.character).length ?? 0); 103 | const endIndex = 104 | lines.slice(0, e.line).join("\n").length + 105 | (lines[e.line]?.slice(0, e.character).length ?? 0); 106 | 107 | let node = ast.rootNode; 108 | while (node.childCount > 0) { 109 | let foundChild = false; 110 | for (let child of node.children) { 111 | if (child.startIndex < startIndex && child.endIndex > endIndex) { 112 | node = child; 113 | foundChild = true; 114 | break; 115 | } 116 | } 117 | 118 | if (!foundChild) { 119 | break; 120 | } 121 | } 122 | 123 | return { 124 | contents: node.text, 125 | filepath: range.filepath, 126 | range: { 127 | start: { 128 | line: node.startPosition.row, 129 | character: node.startPosition.column, 130 | }, 131 | end: { 132 | line: node.endPosition.row, 133 | character: node.endPosition.column, 134 | }, 135 | }, 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /src/handler/codeBlockHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { regInMessage, regOutMessage } from '../util/reg_messages'; 3 | 4 | 5 | export async function applyCode(text: string) { 6 | const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); 7 | 8 | if (validVisibleTextEditors.length > 1) { 9 | vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`); 10 | return; 11 | } 12 | 13 | const editor = validVisibleTextEditors[0]; 14 | if (!editor) { 15 | return; 16 | } 17 | 18 | const selection = editor.selection; 19 | const start = selection.start; 20 | const end = selection.end; 21 | 22 | await editor.edit((editBuilder: vscode.TextEditorEdit) => { 23 | editBuilder.replace(new vscode.Range(start, end), text); 24 | }); 25 | 26 | // Calculate the range of the inserted text 27 | const insertedRange = new vscode.Range(start, editor.document.positionAt(text.length + editor.document.offsetAt(start))); 28 | 29 | // Check if the inserted text is already within the visible range 30 | const visibleRanges = editor.visibleRanges; 31 | const isRangeVisible = visibleRanges.some(range => range.contains(insertedRange)); 32 | 33 | // If the inserted text is not visible, reveal it 34 | if (!isRangeVisible) { 35 | editor.revealRange(insertedRange, vscode.TextEditorRevealType.InCenterIfOutsideViewport); 36 | } 37 | } 38 | 39 | 40 | 41 | export async function applyCodeFile(text: string, fileName: string): Promise { 42 | if (fileName) { 43 | await replaceFileContent(vscode.Uri.file(fileName), text); 44 | return; 45 | } 46 | 47 | const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined); 48 | 49 | if (validVisibleTextEditors.length > 1) { 50 | vscode.window.showErrorMessage(`2There are more then one visible text editors. Please close all but one and try again.`); 51 | return; 52 | } 53 | 54 | const editor = validVisibleTextEditors[0]; 55 | if (!editor) { 56 | return; 57 | } 58 | 59 | const document = editor.document; 60 | const fullRange = new vscode.Range( 61 | document.positionAt(0), 62 | document.positionAt(document.getText().length) 63 | ); 64 | 65 | await editor.edit((editBuilder: vscode.TextEditorEdit) => { 66 | editBuilder.replace(fullRange, text); 67 | }); 68 | } 69 | export async function replaceFileContent(uri: vscode.Uri, newContent: string) { 70 | try { 71 | // 创建一个 WorkspaceEdit 对象 72 | const workspaceEdit = new vscode.WorkspaceEdit(); 73 | 74 | // 获取文件的当前内容 75 | const document = await vscode.workspace.openTextDocument(uri); 76 | 77 | // 计算文件的完整范围(从文件开始到文件结束) 78 | const fullRange = new vscode.Range( 79 | document.positionAt(0), 80 | document.positionAt(document.getText().length) 81 | ); 82 | 83 | // 使用 WorkspaceEdit 的 replace 方法替换文件的完整范围内容 84 | workspaceEdit.replace(uri, fullRange, newContent); 85 | 86 | // 应用编辑更改 87 | await vscode.workspace.applyEdit(workspaceEdit); 88 | 89 | // 显示成功消息 90 | vscode.window.showInformationMessage('File content replaced successfully.'); 91 | } catch (error) { 92 | // 显示错误消息 93 | vscode.window.showErrorMessage('Failed to replace file content: ' + error); 94 | } 95 | } 96 | 97 | 98 | regInMessage({command: 'code_apply', content: ''}); 99 | export async function insertCodeBlockToFile(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise { 100 | await applyCode(message.content); 101 | } 102 | 103 | regInMessage({command: 'code_file_apply', content: '', fileName: ''}); 104 | export async function replaceCodeBlockToFile(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise { 105 | await applyCodeFile(message.content, message.fileName); 106 | } 107 | 108 | /** 109 | * 创建并打开一个新文件 110 | * @param message 包含要创建文件的语言和内容的对象 111 | * - language: 文件的语言 112 | * - content: 文件的内容 113 | */ 114 | export async function createAndOpenFile(message: any) { 115 | try { 116 | if (!message || !message.language || !message.content) { 117 | vscode.window.showErrorMessage('Invalid message received'); 118 | return; 119 | } 120 | const document = await vscode.workspace.openTextDocument({ 121 | language: message.language, 122 | content: message.content 123 | }); 124 | await vscode.window.showTextDocument(document, { preview: false }); 125 | } catch (error) { 126 | vscode.window.showErrorMessage(`Error creating or opening file: ${error}`); 127 | } 128 | } -------------------------------------------------------------------------------- /src/handler/handlerRegister.ts: -------------------------------------------------------------------------------- 1 | import { messageHandler } from './messageHandler'; 2 | import { insertCodeBlockToFile } from './codeBlockHandler'; 3 | import { replaceCodeBlockToFile } from './codeBlockHandler'; 4 | import { doCommit } from './commitHandler'; 5 | import { getHistoryMessages } from './historyMessagesHandler'; 6 | import { handleRegCommandList } from './workflowCommandHandler'; 7 | import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage'; 8 | import { applyCodeWithDiff } from './diffHandler'; 9 | import { addConext } from './contextHandler'; 10 | import { getContextDetail } from './contextHandler'; 11 | import {createAndOpenFile} from './codeBlockHandler'; 12 | import { listAllMessages } from './listMessages'; 13 | import { doVscodeCommand } from './vscodeCommandHandler'; 14 | import { readFile, writeFile, getIDEServicePort, getCurrentFileInfo } from './fileHandler'; 15 | import { getTopics, deleteTopic } from './topicHandler'; 16 | import { readConfig, writeConfig, readServerConfigBase, writeServerConfigBase } from './configHandler'; 17 | import { openLink } from './openlinkHandler'; 18 | 19 | 20 | // According to the context menu selected by the user, add the corresponding context file 21 | // Response: { command: 'appendContext', context: } 22 | messageHandler.registerHandler('addContext', addConext); 23 | // Apply the code block replied by AI to the currently active view 24 | // Response: none 25 | messageHandler.registerHandler('code_apply', insertCodeBlockToFile); 26 | // Apply the code block replied by AI to the currently active view, replacing the current file content 27 | // Response: none 28 | messageHandler.registerHandler('code_file_apply', replaceCodeBlockToFile); 29 | // Apply the code block to a new file 30 | messageHandler.registerHandler('code_new_file', createAndOpenFile); 31 | // Perform commit operation 32 | // Response: none 33 | messageHandler.registerHandler('doCommit', doCommit); 34 | // Get the history messages, called when the user view is displayed 35 | // Response: { command: 'historyMessages', result: } 36 | // is a list, the specific attribute information is determined when the interface is added 37 | messageHandler.registerHandler('historyMessages', getHistoryMessages); 38 | // Register the command list 39 | // Response: { command: 'regCommandList', result: } 40 | messageHandler.registerHandler('regCommandList', handleRegCommandList); 41 | // Send a message, send the message entered by the user to AI 42 | // Response: 43 | // { command: 'receiveMessagePartial', text: , user: , date: } 44 | // { command: 'receiveMessagePartial', text: , user: , date: } 45 | messageHandler.registerHandler('sendMessage', sendMessage); 46 | // Stop devchat, used to stop devchat by the user 47 | // Response: none 48 | messageHandler.registerHandler('stopDevChat', stopDevChat); 49 | // Show diff 50 | // Response: none 51 | // Show diff, for historical reasons, the same as above 52 | messageHandler.registerHandler('show_diff', applyCodeWithDiff); 53 | // Get context details 54 | // Response: { command: 'contextDetailResponse', 'file':, result: } 55 | // is a JSON string 56 | messageHandler.registerHandler('contextDetail', getContextDetail); 57 | // Debug handler 58 | messageHandler.registerHandler('listAllMessages', listAllMessages); 59 | // Regeneration 60 | // The response is the same as sendMessage 61 | messageHandler.registerHandler('regeneration', regeneration); 62 | // Delete chat message 63 | // Response: { command: 'deletedChatMessage', result: } 64 | messageHandler.registerHandler('deleteChatMessage', deleteChatMessage); 65 | 66 | // Execute vscode command 67 | // Response: none 68 | messageHandler.registerHandler('doCommand', doVscodeCommand); 69 | 70 | messageHandler.registerHandler('userInput', userInput); 71 | 72 | messageHandler.registerHandler('readFile', readFile); 73 | messageHandler.registerHandler('writeFile', writeFile); 74 | 75 | messageHandler.registerHandler('getTopics', getTopics); 76 | messageHandler.registerHandler('deleteTopic', deleteTopic); 77 | 78 | messageHandler.registerHandler('readConfig', readConfig); 79 | messageHandler.registerHandler('writeConfig', writeConfig); 80 | 81 | messageHandler.registerHandler('getCurrentFileInfo', getCurrentFileInfo); 82 | messageHandler.registerHandler('getIDEServicePort', getIDEServicePort); 83 | 84 | messageHandler.registerHandler('readServerConfigBase', readServerConfigBase); 85 | messageHandler.registerHandler('writeServerConfigBase', writeServerConfigBase); 86 | 87 | messageHandler.registerHandler('openLink', openLink); 88 | 89 | -------------------------------------------------------------------------------- /src/contributes/codecomplete/astIndex/indexStore.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | // 对代码文件进行管理,记录每个文件的block信息 5 | // 同时支持本地文件存储结果,以及支持针对新变量集合进行相似度计算,并返回相似度最高的一组block 6 | import { BlockInfo, FileBlockInfo } from "./types"; 7 | import { UiUtilWrapper } from '../../../util/uiUtil'; 8 | 9 | // difine interface for {score: number, block: BlockInfo} 10 | export interface SimilarBlock { 11 | score: number; 12 | block: BlockInfo; 13 | } 14 | 15 | export class IndexStore { 16 | private index: Map = new Map(); 17 | private storePath: string; 18 | 19 | constructor() { 20 | const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); 21 | if (!workspaceDir) { 22 | this.storePath = ""; 23 | return ; 24 | } 25 | this.storePath = path.join(workspaceDir, ".chat", "index", "index.json"); 26 | } 27 | 28 | // 获取文件block信息 29 | public get(file: string): FileBlockInfo | undefined { 30 | return this.index.get(file); 31 | } 32 | 33 | public async load(): Promise { 34 | // 读取写入的文件,并解析为index变量 35 | if (this.storePath === "" || !fs.existsSync(this.storePath)) { 36 | return false; 37 | } 38 | 39 | const content = await fs.promises.readFile(this.storePath, 'utf-8'); 40 | const index = JSON.parse(content); 41 | this.index = new Map(Object.entries(index)); 42 | return true; 43 | } 44 | 45 | public async save(): Promise { 46 | if (this.storePath === "") { 47 | return false; 48 | } 49 | 50 | // 将index变量写入文件 51 | const content = JSON.stringify(Object.fromEntries(this.index)); 52 | await fs.promises.writeFile(this.storePath, content, 'utf-8'); 53 | return true; 54 | } 55 | 56 | public async search(identifiers: string[], limit: number): Promise { 57 | // 遍历所有文件中block,计算相似度,返回相似度最高的一组block 58 | const similarBlocks: SimilarBlock[] = []; 59 | 60 | for (const [file, fileBlocInfo] of this.index) { 61 | for (const block of fileBlocInfo.blocks) { 62 | // 计算相似度 63 | const similarity = this.similar(identifiers, block.identifiers); 64 | if (similarity === 0) { 65 | continue; 66 | } 67 | 68 | // 根据相似度插入到similarBlocks数组中 69 | let index = 0; 70 | while (index < similarBlocks.length && similarBlocks[index].score > similarity) { 71 | index++; 72 | } 73 | if (index === similarBlocks.length) { 74 | similarBlocks.push({score: similarity, block}); 75 | } else { 76 | similarBlocks.splice(index, 0, {score: similarity, block}); 77 | } 78 | 79 | // 限制返回的数量 80 | if (similarBlocks.length > limit) { 81 | similarBlocks.pop(); 82 | } 83 | } 84 | } 85 | 86 | return similarBlocks; 87 | } 88 | 89 | public similar(setA: string[], setB: string[]): number { 90 | if (setA.length === 0 || setB.length === 0) { 91 | return 0; 92 | } 93 | 94 | // 相似度 = 交集 / setA.length 95 | let count = 0; // 用来计数共有元素的数量 96 | let i = 0, j = 0; // 初始化两个指针 97 | 98 | while (i < setA.length && j < setB.length) { 99 | if (setA[i] === setB[j]) { 100 | count++; // 找到一个共有元素,计数器加一 101 | i++; // 移动两个指针 102 | j++; 103 | } else if (setA[i] < setB[j]) { 104 | i++; // 只移动setA的指针 105 | } else { 106 | j++; // 只移动setB的指针 107 | } 108 | } 109 | 110 | return (count / setA.length)*0.7 + (count / setB.length)*0.3; 111 | } 112 | 113 | public async addBlocksToFile(file: string, hashKey: string, blocks: BlockInfo[]) { 114 | // 遍历blocks,将每个block添加到index中 115 | for (const block of blocks) { 116 | const fileBlockInfo = this.index.get(file); 117 | if (fileBlockInfo) { 118 | fileBlockInfo.blocks.push(block); 119 | } else { 120 | this.index.set(file, {path: file, lastTime: Date.now(), blocks: [block], hashKey: hashKey}); 121 | } 122 | } 123 | } 124 | 125 | // add FileBlockInfo 126 | public async add(fileBlockInfo: FileBlockInfo) { 127 | this.index.set(fileBlockInfo.path, fileBlockInfo); 128 | } 129 | } -------------------------------------------------------------------------------- /src/ide_services/endpoints/legacy_bridge/feature/find-defs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | interface Definition { 4 | name: string; 5 | abspath: string; 6 | line: number; // 1-based 7 | character: number; // 1-based 8 | } 9 | 10 | /** 11 | * @param abspath: absolute path of the file 12 | * @param line: line number, 1-based 13 | * @param character: character number, 1-based 14 | * 15 | **/ 16 | async function findDefinitions( 17 | abspath: string, 18 | line: number, 19 | character: number 20 | ): Promise { 21 | const uri = vscode.Uri.file(abspath); 22 | const position = new vscode.Position(line - 1, character - 1); 23 | 24 | // TODO: verify if the file & position is correct 25 | // const document = await vscode.workspace.openTextDocument(uri); 26 | 27 | const locations = await vscode.commands.executeCommand( 28 | "vscode.executeDefinitionProvider", 29 | uri, 30 | position 31 | ); 32 | 33 | const definitions: Definition[] = []; 34 | if (locations) { 35 | for (const location of locations) { 36 | console.log( 37 | `* Definition found in file: ${location.uri.fsPath}, line: ${location.range.start.line}, character: ${location.range.start.character}` 38 | ); 39 | // use `map` & `Promise.all` to improve performance if needed 40 | const doc = await vscode.workspace.openTextDocument(location.uri); 41 | 42 | definitions.push({ 43 | name: doc.getText(location.range), 44 | abspath: location.uri.fsPath, 45 | line: location.range.start.line + 1, 46 | character: location.range.start.character + 1, 47 | }); 48 | } 49 | } else { 50 | console.log("No definition found"); 51 | } 52 | 53 | console.log(`* Found ${definitions.length} definitions`); 54 | console.log(definitions); 55 | return definitions; 56 | } 57 | 58 | function findText( 59 | document: vscode.TextDocument, 60 | text: string 61 | ): vscode.Position[] { 62 | const positions: vscode.Position[] = []; 63 | if (!text) { 64 | return positions; 65 | } 66 | 67 | const content = document.getText(); 68 | 69 | let index = content.indexOf(text); 70 | while (index >= 0) { 71 | const position = document.positionAt(index); 72 | positions.push(position); 73 | 74 | // Find the next occurrence 75 | index = content.indexOf(text, index + 1); 76 | } 77 | 78 | return positions; 79 | } 80 | 81 | async function findDefinitionsOfToken( 82 | abspath: string, 83 | token: string 84 | ): Promise { 85 | const uri = vscode.Uri.file(abspath); 86 | const document = await vscode.workspace.openTextDocument(uri); 87 | const positions = findText(document, token); 88 | console.log(`- Found ${positions.length} <${token}>`); 89 | 90 | // TODO: verify if the file & position is correct 91 | // const document = await vscode.workspace.openTextDocument(uri); 92 | const definitions: Definition[] = []; 93 | const visited = new Set(); 94 | 95 | for (const position of positions) { 96 | const locations = await vscode.commands.executeCommand< 97 | vscode.Location[] 98 | >("vscode.executeDefinitionProvider", uri, position); 99 | 100 | if (!locations) { 101 | console.log("No definition found"); 102 | continue; 103 | } 104 | 105 | for (const location of locations) { 106 | const locationKey = `${location.uri.fsPath}:${location.range.start.line}:${location.range.start.character}`; 107 | if (visited.has(locationKey)) { 108 | continue; 109 | } 110 | visited.add(locationKey); 111 | 112 | console.log( 113 | `- <${token}> Definition found in file: ${location.uri.fsPath}, line: ${location.range.start.line}, character: ${location.range.start.character}` 114 | ); 115 | // use `map` & `Promise.all` to improve performance if needed 116 | const doc = await vscode.workspace.openTextDocument(location.uri); 117 | 118 | definitions.push({ 119 | name: doc.getText(location.range), 120 | abspath: location.uri.fsPath, 121 | line: location.range.start.line + 1, 122 | character: location.range.start.character + 1, 123 | }); 124 | } 125 | } 126 | console.log(`* Found ${definitions.length} definitions`); 127 | console.log(definitions); 128 | return definitions; 129 | } 130 | 131 | export { findDefinitions, findDefinitionsOfToken }; 132 | -------------------------------------------------------------------------------- /test/util/commonUtil.test.ts: -------------------------------------------------------------------------------- 1 | // test/commonUtil.test.ts 2 | 3 | import { expect } from 'chai'; 4 | import * as fs from 'fs'; 5 | import * as os from 'os'; 6 | import * as path from 'path'; 7 | import { 8 | createTempSubdirectory, 9 | CommandRun, 10 | runCommandAndWriteOutput, 11 | runCommandStringAndWriteOutput, 12 | getLanguageIdByFileName, 13 | } from '../../src/util/commonUtil'; 14 | import { UiUtilWrapper } from '../../src/util/uiUtil'; 15 | 16 | import sinon from 'sinon'; 17 | 18 | describe('commonUtil', () => { 19 | afterEach(() => { 20 | sinon.restore(); 21 | }); 22 | 23 | describe('createTempSubdirectory', () => { 24 | it('should create a temporary subdirectory', () => { 25 | const tempDir = os.tmpdir(); 26 | const subdir = 'test-subdir'; 27 | const targetDir = createTempSubdirectory(subdir); 28 | 29 | expect(targetDir.startsWith(path.join(tempDir, subdir))).to.be.true; 30 | expect(fs.existsSync(targetDir)).to.be.true; 31 | fs.rmdirSync(targetDir, { recursive: true }); 32 | }); 33 | }); 34 | 35 | describe('CommandRun', () => { 36 | it('should run a command and capture stdout and stderr', async () => { 37 | const command = 'echo'; 38 | const args = ['hello', 'world']; 39 | const options = { shell: true }; 40 | 41 | const run = new CommandRun(); 42 | const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, undefined); 43 | 44 | expect(result.exitCode).to.equal(0); 45 | expect(result.stdout.trim()).to.equal('hello world'); 46 | expect(result.stderr).to.equal(''); 47 | }); 48 | 49 | it('should run a command and write output to a file', async () => { 50 | const command = 'echo'; 51 | const args = ['hello', 'world']; 52 | const options = { shell: true }; 53 | const outputFile = path.join(os.tmpdir(), 'test-output.txt'); 54 | 55 | const run = new CommandRun(); 56 | const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile); 57 | 58 | expect(result.exitCode).to.equal(0); 59 | expect(result.stdout.trim()).to.equal('hello world'); 60 | expect(result.stderr).to.equal(''); 61 | expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world'); 62 | fs.unlinkSync(outputFile); 63 | }); 64 | 65 | it('should handle command not found error and output the error message', async () => { 66 | const command = 'nonexistent-command'; 67 | const args: string[] = []; 68 | const options = { shell: true }; 69 | 70 | const run = new CommandRun(); 71 | 72 | const result = await run.spawnAsync( 73 | command, 74 | args, 75 | options, 76 | undefined, 77 | undefined, 78 | undefined, 79 | undefined 80 | ); 81 | 82 | expect(result.exitCode).to.not.equal(0); 83 | expect(result.stderr).to.include(`${command}: command not found`); 84 | }); 85 | }); 86 | 87 | describe('runCommandAndWriteOutput', () => { 88 | it('should run a command and write output to a file', async () => { 89 | const command = 'echo'; 90 | const args: string[] = ['hello', 'world']; 91 | const outputFile = path.join(os.tmpdir(), 'test-output.txt'); 92 | 93 | await runCommandAndWriteOutput(command, args, outputFile); 94 | 95 | expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world'); 96 | fs.unlinkSync(outputFile); 97 | }); 98 | }); 99 | describe('runCommandStringAndWriteOutput', () => { 100 | it('should run a command string and write output to a file', async () => { 101 | const commandString = 'echo hello world'; 102 | const outputFile = path.join(os.tmpdir(), 'test-output.txt'); 103 | 104 | await runCommandStringAndWriteOutput(commandString, outputFile); 105 | 106 | const fileContent = fs.readFileSync(outputFile, 'utf-8').trim(); 107 | const parsedContent = JSON.parse(fileContent); 108 | 109 | expect(parsedContent.command).to.equal(commandString); 110 | expect(parsedContent.content.trim()).to.equal('hello world'); 111 | fs.unlinkSync(outputFile); 112 | }); 113 | }); 114 | 115 | describe('getLanguageIdByFileName', () => { 116 | beforeEach(() => { 117 | sinon.stub(UiUtilWrapper, 'languageId').callsFake(async (fileName: string) => { 118 | const languageIds: { [key: string]: string } = { 119 | 'test.py': 'python', 120 | 'test.js': 'javascript', 121 | 'test.ts': 'typescript', 122 | }; 123 | return languageIds[fileName]; 124 | }); 125 | }); 126 | 127 | afterEach(() => { 128 | sinon.restore(); 129 | }); 130 | 131 | it('should return the correct language ID for a given file name', async () => { 132 | expect(await getLanguageIdByFileName('test.py')).to.equal('python'); 133 | expect(await getLanguageIdByFileName('test.js')).to.equal('javascript'); 134 | expect(await getLanguageIdByFileName('test.ts')).to.equal('typescript'); 135 | expect(await getLanguageIdByFileName('test.unknown')).to.equal(undefined); 136 | }); 137 | }); 138 | }); -------------------------------------------------------------------------------- /src/contributes/codecomplete/ast/findFunctions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 针对代码补全功能,构建prompt 3 | 4 | prompt的好坏,取决于提供的上下文信息。 5 | 通过AST获取相对完整的信息,可能会增加提示的准确度,但也会增加代码提示的复杂度。 6 | */ 7 | 8 | import { logger } from "../../../util/logger"; 9 | import * as vscode from "vscode"; 10 | import { getAst, getTreePathAtCursor, RangeInFileWithContents } from "./ast"; 11 | import Parser from "web-tree-sitter"; 12 | import { getCommentPrefix, getLangageFunctionConfig, LanguageFunctionsConfig } from "./language"; 13 | import { getLanguageForFile, getQueryFunctionsSource } from "./treeSitter"; 14 | import MemoryCacheManager from "../cache"; 15 | 16 | const functionCache: MemoryCacheManager = new MemoryCacheManager(4); 17 | 18 | export interface FunctionRange { 19 | define: { 20 | start: { row: number, column: number }, 21 | end: { row: number, column: number } 22 | }, 23 | body: { 24 | start: { row: number, column: number }, 25 | end: { row: number, column: number } 26 | }, 27 | name: string 28 | } 29 | 30 | export async function findFunctionRanges(filepath: string, node: Parser.SyntaxNode): Promise { 31 | const lang = await getLanguageForFile(filepath); 32 | if (!lang) { 33 | return []; 34 | } 35 | 36 | const querySource = await getQueryFunctionsSource(filepath); 37 | if (!querySource) { 38 | return []; 39 | } 40 | 41 | const extension = filepath.split('.').pop() || ''; 42 | let query: Parser.Query | undefined = functionCache.get(extension); 43 | if (!query) { 44 | query = lang?.query(querySource); 45 | functionCache.set(extension, query); 46 | } 47 | const matches = query?.matches(node); 48 | 49 | const functionRanges: FunctionRange[] = []; 50 | if (matches) { 51 | for (const match of matches) { 52 | // find functionNode through tag name 53 | const functionNode = match.captures.find((capture) => capture.name === "function")?.node; 54 | const bodyNode = match.captures.find((capture) => capture.name === "function.body")?.node; 55 | const nameNode = match.captures.find((capture) => capture.name === "function.name")?.node; 56 | if (!functionNode || !bodyNode) { 57 | continue; 58 | } 59 | 60 | const functionRange: FunctionRange = { 61 | define: { 62 | start: { 63 | row: functionNode.startPosition.row, 64 | column: functionNode.startPosition.column, 65 | }, 66 | end: { 67 | row: functionNode.endPosition.row, 68 | column: functionNode.endPosition.column, 69 | }, 70 | }, 71 | body: { 72 | start: { 73 | row: bodyNode.startPosition.row, 74 | column: bodyNode.startPosition.column, 75 | }, 76 | end: { 77 | row: bodyNode.endPosition.row, 78 | column: bodyNode.endPosition.column, 79 | }, 80 | }, 81 | name: nameNode?.text ?? "", 82 | }; 83 | 84 | // Check if this function range is not fully contained within another function range 85 | const isContained = functionRanges.some(range => { 86 | return ( 87 | range.define.start.row <= functionRange.define.start.row && 88 | range.define.end.row >= functionRange.define.end.row && 89 | range.body.start.row <= functionRange.body.start.row && 90 | range.body.end.row >= functionRange.body.end.row 91 | ); 92 | }); 93 | 94 | if (!isContained) { 95 | functionRanges.push(functionRange); 96 | } 97 | } 98 | } 99 | 100 | return functionRanges; 101 | } 102 | 103 | export async function findFunctionNodes(filepath: string, node: Parser.SyntaxNode): Promise { 104 | const lang = await getLanguageForFile(filepath); 105 | if (!lang) { 106 | return []; 107 | } 108 | 109 | const querySource = await getQueryFunctionsSource(filepath); 110 | if (!querySource) { 111 | return []; 112 | } 113 | 114 | const extension = filepath.split('.').pop() || ''; 115 | let query: Parser.Query | undefined = functionCache.get(extension); 116 | if (!query) { 117 | query = lang?.query(querySource); 118 | functionCache.set(extension, query); 119 | } 120 | const matches = query?.matches(node); 121 | let functionNodes: Parser.SyntaxNode[] = []; 122 | for (const match of matches?? []) { 123 | // find functionNode through tag name 124 | const functionNode = match.captures.find((capture) => capture.name === "function")?.node; 125 | if (functionNode) { 126 | functionNodes.push(functionNode); 127 | } 128 | } 129 | 130 | return functionNodes; 131 | } 132 | --------------------------------------------------------------------------------