Merge pull request #133 from covespace/refactor_code2

Refactor code2
This commit is contained in:
boob.yang 2023-05-31 16:32:49 +08:00 committed by GitHub
commit 9a4840c514
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 4041 additions and 955 deletions

6
.mocharc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extension": ["ts"],
"spec": "test/**/*.test.ts",
"require": "ts-node/register",
"project": "tsconfig.test.json"
}

1590
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "devchat",
"displayName": "DevChat",
"description": "Write prompts, not code",
"version": "0.0.45",
"version": "0.0.47",
"icon": "assets/devchat.png",
"publisher": "merico",
"engines": {
@ -155,7 +155,7 @@
},
{
"command": "DevChat.OPENAI_API_KEY",
"title": "DEVCHAT_API_KEY",
"title": "Input Access Key",
"category": "DevChat"
},
{
@ -314,7 +314,7 @@
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js",
"test": "mocha",
"build": "webpack --config webpack.config.js",
"dev": "webpack serve --config webpack.config.js --open"
},
@ -323,19 +323,24 @@
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.5",
"@types/chai": "^4.3.5",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.1",
"@types/mocha": "^10.0.1",
"@types/mock-fs": "^4.13.1",
"@types/ncp": "^2.0.5",
"@types/node": "16.x",
"@types/proxyquire": "^1.3.28",
"@types/react-dom": "^18.2.3",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/shell-escape": "^0.2.1",
"@types/sinon": "^10.0.15",
"@types/uuid": "^9.0.1",
"@types/vscode": "^1.77.0",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@vscode/test-electron": "^2.3.0",
"babel-loader": "^9.1.2",
"chai": "^4.3.7",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"dotenv": "^16.0.3",
@ -345,15 +350,21 @@
"html-webpack-plugin": "^5.5.1",
"jest": "^29.5.0",
"json-loader": "^0.5.7",
"mocha": "^10.2.0",
"mock-fs": "^5.2.0",
"proxyquire": "^2.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"redux": "^4.2.1",
"sinon": "^15.1.0",
"style-loader": "^3.3.2",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"url-loader": "^4.1.1",
"vscode-test": "^1.6.1",
"webpack": "^5.76.3",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.3"

View File

@ -1,8 +1,4 @@
import { vs } from "react-syntax-highlighter/dist/esm/styles/hljs";
import CustomCommands from "./customCommand";
import { logger } from "../util/logger";
import * as vscode from 'vscode';
import * as path from 'path';
export interface Command {
name: string;

View File

@ -2,88 +2,92 @@ import fs from 'fs';
import path from 'path';
import { logger } from '../util/logger';
interface Command {
name: string;
pattern: string;
description: string;
message: string;
default: boolean;
show: boolean;
instructions: string[];
export interface Command {
name: string;
pattern: string;
description: string;
message: string;
default: boolean;
show: boolean;
instructions: string[];
}
class CustomCommands {
private static instance: CustomCommands | null = null;
private commands: Command[] = [];
private constructor() {
}
private constructor() {
}
public static getInstance(): CustomCommands {
if (!CustomCommands.instance) {
CustomCommands.instance = new CustomCommands();
}
return CustomCommands.instance;
}
public parseCommands(workflowsDir: string): void {
this.commands = [];
try {
const subDirs = fs.readdirSync(workflowsDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const dir of subDirs) {
const settingsPath = path.join(workflowsDir, dir, '_setting_.json');
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
const command: Command = {
name: dir,
pattern: settings.pattern,
description: settings.description,
message: settings.message,
default: settings.default,
show: settings.show === undefined ? "true": settings.show,
instructions: settings.instructions
};
this.commands.push(command);
public static getInstance(): CustomCommands {
if (!CustomCommands.instance) {
CustomCommands.instance = new CustomCommands();
}
return CustomCommands.instance;
}
public parseCommands(workflowsDir: string): void {
this.commands = [];
try {
const subDirs = fs.readdirSync(workflowsDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const dir of subDirs) {
const settingsPath = path.join(workflowsDir, dir, '_setting_.json');
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
const command: Command = {
name: dir,
pattern: settings.pattern,
description: settings.description,
message: settings.message,
default: settings.default,
show: settings.show === undefined ? "true" : settings.show,
instructions: settings.instructions
};
this.commands.push(command);
}
}
} catch (error) {
// 显示错误消息
logger.channel()?.error(`Failed to parse commands: ${error}`);
logger.channel()?.show();
}
} catch (error) {
// 显示错误消息
logger.channel()?.error(`Failed to parse commands: ${error}`);
logger.channel()?.show();
}
}
}
public getCommands(): Command[] {
return this.commands;
}
public regCommand(command: Command) {
this.commands.push(command);
}
public getCommand(commandName: string): Command | null {
const foundCommand = this.commands.find(command => command.name === commandName);
return foundCommand ? foundCommand : null;
public getCommands(): Command[] {
return this.commands;
}
public getCommand(commandName: string): Command | null {
const foundCommand = this.commands.find(command => command.name === commandName);
return foundCommand ? foundCommand : null;
}
public handleCommand(commandName: string): string {
// 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象
const command = this.getCommand(commandName);
if (!command) {
logger.channel()?.error(`Command ${commandName} not found!`);
logger.channel()?.show();
return '';
public handleCommand(commandName: string): string {
// 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象
const command = this.getCommand(commandName);
if (!command) {
logger.channel()?.error(`Command ${commandName} not found!`);
logger.channel()?.show();
return '';
}
// 构建instructions列表字符串
const instructions = command!.instructions
.map((instruction: string) => `[instruction|./.chat/workflows/${command.name}/${instruction}]`)
.join(' ');
// 返回结果字符串
return `${instructions} ${command!.message}`;
}
// 构建instructions列表字符串
const instructions = command!.instructions
.map((instruction: string) => `[instruction|./.chat/workflows/${command.name}/${instruction}]`)
.join(' ');
// 返回结果字符串
return `${instructions} ${command!.message}`;
}
}
export default CustomCommands;

View File

@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil';
import { UiUtilWrapper } from '../util/uiUtil';
export async function handleCodeSelected(fileSelected: string, codeSelected: string) {
// get file name from fileSelected
@ -15,7 +15,7 @@ export async function handleCodeSelected(fileSelected: string, codeSelected: str
const languageId = await getLanguageIdByFileName(fileSelected);
// get relative path of workspace
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
const relativePath = path.relative(workspaceDir!, fileSelected);
// convert fileContent to markdown code block with languageId and file path
@ -27,7 +27,7 @@ export async function handleCodeSelected(fileSelected: string, codeSelected: str
const jsonData = JSON.stringify(data);
// save markdownCodeBlock to temp file
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(jsonData));
await UiUtilWrapper.writeFile(tempFile, jsonData);
return `[context|${tempFile}]`;
}

View File

@ -1,15 +1,16 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { ChatContext } from './contextManager';
import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/commonUtil';
import { logger } from '../util/logger';
import { UiUtilWrapper } from '../util/uiUtil';
export const customCommandContext: ChatContext = {
name: '<custom command>',
description: 'custorm command',
handler: async () => {
// popup a dialog to ask for the command line to run
const customCommand = await vscode.window.showInputBox({
const customCommand = await UiUtilWrapper.showInputBox({
prompt: 'Input your custom command',
placeHolder: 'for example: ls -l'
});
@ -17,15 +18,15 @@ export const customCommandContext: ChatContext = {
// 检查用户是否输入了命令
if (customCommand) {
const tempDir = await createTempSubdirectory('devchat/context');
const diff_file = path.join(tempDir, 'custom.txt');
const diffFile = path.join(tempDir, 'custom.txt');
logger.channel()?.info(`custom command: ${customCommand}`);
const result = await runCommandStringAndWriteOutput(customCommand, diff_file);
const result = await runCommandStringAndWriteOutput(customCommand, diffFile);
logger.channel()?.info(`custom command: ${customCommand} exit code:`, result.exitCode);
logger.channel()?.debug(`custom command: ${customCommand} stdout:`, result.stdout);
logger.channel()?.debug(`custom command: ${customCommand} stderr:`, result.stderr);
return `[context|${diff_file}]`;
return `[context|${diffFile}]`;
}
return '';
},

View File

@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil';
import { UiUtilWrapper } from '../util/uiUtil';
export async function handleFileSelected(fileSelected: string) {
// get file name from fileSelected
@ -18,7 +18,7 @@ export async function handleFileSelected(fileSelected: string) {
const languageId = await getLanguageIdByFileName(fileSelected);
// get relative path of workspace
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
const relativePath = path.relative(workspaceDir!, fileSelected);
// convert fileContent to markdown code block with languageId and file path
@ -30,7 +30,7 @@ export async function handleFileSelected(fileSelected: string) {
const jsonData = JSON.stringify(data);
// save markdownCodeBlock to temp file
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(jsonData));
await UiUtilWrapper.writeFile(tempFile, jsonData);
return `[context|${tempFile}]`;
}

View File

@ -1,5 +1,4 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/commonUtil';
import { logger } from '../util/logger';

View File

@ -1,186 +1,184 @@
import * as vscode from 'vscode';
import ChatPanel from '../panel/chatPanel';
import { sendFileSelectMessage, sendCodeSelectMessage } from './util';
import { logger } from '../util/logger';
import * as childProcess from 'child_process';
import { DevChatViewProvider } from '../panel/devchatView';
import ExtensionContextHolder from '../util/extensionContext';
import { TopicManager } from '../topic/topicManager';
import { TopicTreeDataProvider, TopicTreeItem } from '../panel/topicView';
import { FilePairManager } from '../util/diffFilePairs';
import { ApiKeyManager } from '../util/apiKey';
import * as process from 'process';
export function checkDevChatDependency(): boolean {
try {
// Get pipx environment
const pipxEnvOutput = childProcess.execSync('python3 -m pipx environment').toString();
const binPathRegex = /PIPX_BIN_DIR=\s*(.*)/;
// Get BIN path from pipx environment
const match = pipxEnvOutput.match(binPathRegex);
if (match && match[1]) {
const binPath = match[1];
// Add BIN path to PATH
process.env.PATH = `${binPath}:${process.env.PATH}`;
// Check if DevChat is installed
childProcess.execSync('devchat --help');
return true;
} else {
return false;
}
} catch (error) {
// DevChat dependency check failed
return false;
}
}
export async function checkOpenaiApiKey() {
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
let openaiApiKey = await secretStorage.get("devchat_OPENAI_API_KEY");
if (!openaiApiKey) {
openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY');
}
if (!openaiApiKey) {
openaiApiKey = process.env.OPENAI_API_KEY;
}
if (!openaiApiKey) {
return false;
}
return true;
}
function checkOpenaiKey() {
let openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY');
if (!openaiApiKey) {
openaiApiKey = process.env.OPENAI_API_KEY;
}
if (!openaiApiKey) {
// OpenAI key not set
vscode.window.showInputBox({
placeHolder: 'Please input your OpenAI API key (or DevChat access key)'
}).then((value) => {
if (value) {
// 设置用户输入的API Key
vscode.workspace.getConfiguration('DevChat').update('API_KEY', value, true);
}
});
return false;
}
return true;
}
function checkDependencyPackage() {
const dependencyInstalled = checkDevChatDependency();
if (!dependencyInstalled) {
// Prompt the user, whether to install devchat using pip3 install devchat
const installPrompt = 'devchat is not installed. Do you want to install it using pip3 install devchat?';
const installAction = 'Install';
vscode.window.showInformationMessage(installPrompt, installAction).then((selectedAction) => {
if (selectedAction === installAction) {
// Install devchat using pip3 install devchat
const terminal = vscode.window.createTerminal("DevChat Install");
terminal.sendText("pip3 install --upgrade devchat");
terminal.show();
}
});
}
if (!checkOpenaiKey()) {
return;
}
}
function registerOpenChatPanelCommand(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand('devchat.openChatPanel',async () => {
let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => {
await vscode.commands.executeCommand('devchat-view.focus');
});
context.subscriptions.push(disposable);
});
context.subscriptions.push(disposable);
}
async function ensureChatPanel(context: vscode.ExtensionContext): Promise<boolean> {
await vscode.commands.executeCommand('devchat-view.focus');
return true;
await vscode.commands.executeCommand('devchat-view.focus');
return true;
}
function registerAddContextCommand(context: vscode.ExtensionContext) {
const disposableAddContext = vscode.commands.registerCommand('devchat.addConext', async (uri: { path: any; }) => {
if (!await ensureChatPanel(context)) {
return;
}
const callback = async (uri: { path: any; }) => {
if (!await ensureChatPanel(context)) {
return;
}
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path);
});
context.subscriptions.push(disposableAddContext);
const disposableAddContextChinese = vscode.commands.registerCommand('devchat.addConext_chinese', async (uri: { path: any; }) => {
if (!await ensureChatPanel(context)) {
return;
}
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path);
});
context.subscriptions.push(disposableAddContextChinese);
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path);
};
context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext', callback));
context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext_chinese', callback));
}
function registerAskForCodeCommand(context: vscode.ExtensionContext) {
const disposableCodeContext = vscode.commands.registerCommand('devchat.askForCode', async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!await ensureChatPanel(context)) {
return;
}
const callback = async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!await ensureChatPanel(context)) {
return;
}
const selectedText = editor.document.getText(editor.selection);
await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText);
}
});
context.subscriptions.push(disposableCodeContext);
const disposableCodeContextChinese = vscode.commands.registerCommand('devchat.askForCode_chinese', async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!await ensureChatPanel(context)) {
return;
}
const selectedText = editor.document.getText(editor.selection);
await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText);
}
});
context.subscriptions.push(disposableCodeContextChinese);
const selectedText = editor.document.getText(editor.selection);
await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText);
}
};
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForCode', callback));
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForCode_chinese', callback));
}
function registerAskForFileCommand(context: vscode.ExtensionContext) {
const disposableAskFile = vscode.commands.registerCommand('devchat.askForFile', async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!await ensureChatPanel(context)) {
return;
}
const callback = async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!await ensureChatPanel(context)) {
return;
}
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
}
});
context.subscriptions.push(disposableAskFile);
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
}
};
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForFile', callback));
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForFile_chinese', callback));
}
const disposableAskFileChinese = vscode.commands.registerCommand('devchat.askForFile_chinese', async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!await ensureChatPanel(context)) {
return;
}
export function registerApiKeySettingCommand(context: vscode.ExtensionContext) {
const secretStorage: vscode.SecretStorage = context.secrets;
context.subscriptions.push(
vscode.commands.registerCommand('DevChat.OPENAI_API_KEY', async () => {
const passwordInput: string = await vscode.window.showInputBox({
password: true,
title: "Input Access Key",
placeHolder: "Set OPENAI_API_KEY (or DevChat Access Key)"
}) ?? '';
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
}
});
context.subscriptions.push(disposableAskFileChinese);
ApiKeyManager.writeApiKeySecret(passwordInput);
})
);
}
export function registerStatusBarItemClickCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devcaht.onStatusBarClick', async () => {
await vscode.commands.executeCommand('devchat-view.focus');
})
);
}
const topicDeleteCallback = async (item: TopicTreeItem) => {
const confirm = 'Delete';
const cancel = 'Cancel';
const label = typeof item.label === 'string' ? item.label : item.label!.label;
const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : '');
const result = await vscode.window.showWarningMessage(
`Are you sure you want to delete the topic "${truncatedLabel}"?`,
{ modal: true },
confirm,
cancel
);
if (result === confirm) {
TopicManager.getInstance().deleteTopic(item.id);
}
};
;
export function regTopicDeleteCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback)
);
}
export function regAddTopicCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devchat-topicview.addTopic', () => {
const topic = TopicManager.getInstance().createTopic();
TopicManager.getInstance().setCurrentTopic(topic.topicId);
})
);
}
export function regDeleteSelectTopicCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => {
const selectedItem = TopicTreeDataProvider.getInstance().selectedItem;
if (selectedItem) {
topicDeleteCallback(selectedItem);
} else {
vscode.window.showErrorMessage('No item selected');
}
})
);
}
export function regSelectTopicCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devchat-topicview.selectTopic', (item: TopicTreeItem) => {
TopicTreeDataProvider.getInstance().setSelectedItem(item);
TopicManager.getInstance().setCurrentTopic(item.id);
})
);
}
export function regReloadTopicCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devchat-topicview.reloadTopic', async () => {
TopicManager.getInstance().loadTopics();
})
);
}
export function regApplyDiffResultCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('devchat.applyDiffResult', async () => {
const activeEditor = vscode.window.activeTextEditor;
const fileName = activeEditor!.document.fileName;
const [leftUri, rightUri] = FilePairManager.getInstance().findPair(fileName) || [undefined, undefined];
if (leftUri && rightUri) {
// 获取对比的两个文件
const leftDoc = await vscode.workspace.openTextDocument(leftUri);
const rightDoc = await vscode.workspace.openTextDocument(rightUri);
// 将右边文档的内容替换到左边文档
const leftEditor = await vscode.window.showTextDocument(leftDoc);
await leftEditor.edit(editBuilder => {
const fullRange = new vscode.Range(0, 0, leftDoc.lineCount, 0);
editBuilder.replace(fullRange, rightDoc.getText());
});
// 保存左边文档
await leftDoc.save();
} else {
vscode.window.showErrorMessage('No file to apply diff result.');
}
})
);
}
export {
checkDependencyPackage,
registerOpenChatPanelCommand,
registerAddContextCommand,
registerAskForCodeCommand,
registerAskForFileCommand,
registerOpenChatPanelCommand,
registerAddContextCommand,
registerAskForCodeCommand,
registerAskForFileCommand,
};

View File

@ -0,0 +1,42 @@
// src/contributes/commandsBase.ts
import { runCommand } from "../util/commonUtil";
export function checkDevChatDependency(): boolean {
try {
const binPath = getPipxEnvironmentPath();
if (binPath) {
updateEnvironmentPath(binPath);
// Check if DevChat is installed
runCommand('devchat --help');
return true;
} else {
return false;
}
} catch (error) {
// DevChat dependency check failed
return false;
}
}
export function getPipxEnvironmentPath(): string | null {
// Get pipx environment
const pipxEnvOutput = runCommand('python3 -m pipx environment').toString();
const binPathRegex = /PIPX_BIN_DIR=\s*(.*)/;
// Get BIN path from pipx environment
const match = pipxEnvOutput.match(binPathRegex);
if (match && match[1]) {
return match[1];
} else {
return null;
}
}
function updateEnvironmentPath(binPath: string): void {
// Add BIN path to PATH
process.env.PATH = `${binPath}:${process.env.PATH}`;
}

View File

@ -0,0 +1,10 @@
import * as vscode from 'vscode';
export function regLanguageContext() {
const currentLocale = vscode.env.language;
if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') {
vscode.commands.executeCommand('setContext', 'isChineseLocale', true);
} else {
vscode.commands.executeCommand('setContext', 'isChineseLocale', false);
}
}

View File

@ -4,15 +4,18 @@ import { handleCodeSelected } from '../context/contextCodeSelected';
import { handleFileSelected } from '../context/contextFileSelected';
import { MessageHandler } from '../handler/messageHandler';
import { regInMessage, regOutMessage } from '../util/reg_messages';
import { logger } from '../util/logger';
regOutMessage({command: 'appendContext', context: ''});
export async function sendFileSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string): Promise<void> {
logger.channel()?.info(`Append context: ${filePath}`);
const codeContext = await handleFileSelected(filePath);
MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext });
}
regOutMessage({command: 'appendContext', context: ''});
export async function sendCodeSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string, codeBlock: string): Promise<void> {
logger.channel()?.info(`Append context: ${filePath}`);
const codeContext = await handleCodeSelected(filePath, codeBlock);
MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext });
}

21
src/contributes/views.ts Normal file
View File

@ -0,0 +1,21 @@
import * as vscode from 'vscode';
import { DevChatViewProvider } from '../panel/devchatView';
import { TopicTreeDataProvider } from '../panel/topicView';
import ExtensionContextHolder from '../util/extensionContext';
export function regDevChatView(context: vscode.ExtensionContext) {
ExtensionContextHolder.provider = new DevChatViewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, {
webviewOptions: { retainContextWhenHidden: true }
})
);
}
export function regTopicView(context: vscode.ExtensionContext) {
const yourTreeView = vscode.window.createTreeView('devchat-topicview', {
treeDataProvider: TopicTreeDataProvider.getInstance(),
});
context.subscriptions.push(yourTreeView);
}

View File

@ -1,358 +1,55 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import {
checkOpenaiApiKey,
checkDevChatDependency,
checkDependencyPackage,
registerOpenChatPanelCommand,
registerAddContextCommand,
registerAskForCodeCommand,
registerAskForFileCommand,
registerApiKeySettingCommand,
regTopicDeleteCommand,
regAddTopicCommand,
regDeleteSelectTopicCommand,
regSelectTopicCommand,
regReloadTopicCommand,
regApplyDiffResultCommand,
registerStatusBarItemClickCommand,
} from './contributes/commands';
import { regLanguageContext } from './contributes/context';
import { regDevChatView, regTopicView } from './contributes/views';
import ExtensionContextHolder from './util/extensionContext';
import { logger } from './util/logger';
import { DevChatViewProvider } from './panel/devchatView';
import path from 'path';
import { FilePairManager } from './util/diffFilePairs';
import { Topic, TopicManager } from './topic/topicManager';
class TopicTreeItem extends vscode.TreeItem {
id: string;
date: number | undefined;
constructor(label: string, id: string, date: number | undefined, collapsibleState: vscode.TreeItemCollapsibleState) {
super(label, collapsibleState);
this.id = id;
this.date = date;
this.iconPath = new vscode.ThemeIcon('symbol-variable');
this.contextValue = 'yourTreeItem'; // 添加这一行
}
}
class TopicTreeDataProvider implements vscode.TreeDataProvider<TopicTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<TopicTreeItem | undefined | null | void> = new vscode.EventEmitter<TopicTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<TopicTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
public selectedItem: TopicTreeItem | null = null;
private items: TopicTreeItem[] = [];
// reg listeners to TopicManager in constructor
constructor() {
TopicManager.getInstance().addOnCreateTopicListener(this.addItem.bind(this));
TopicManager.getInstance().addOnDeleteTopicListener(this.onDeleteTopic.bind(this));
TopicManager.getInstance().addOnReloadTopicsListener(this.onReloadTopics.bind(this));
TopicManager.getInstance().addOnUpdateTopicListener(this.onUpdateTopics.bind(this));
}
// sort items
private sortItems() {
this.items.sort((a, b) => {
if (a.date && b.date) {
return b.date - a.date;
} else if (!a.date) {
return -1;
} else if (!b.date) {
return 1;
} else {
return 0;
}
});
}
onUpdateTopics(topicId: string) {
const items = this.items.filter(i => i.id === topicId);
const topic = TopicManager.getInstance().getTopic(topicId);
items.map((item) => {
item.label = topic?.name;
item.date = topic?.lastUpdated;
});
this.sortItems();
this._onDidChangeTreeData.fire();
}
onReloadTopics(topics: Topic[]) {
const items = topics.map((topic) => {
return new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
});
this.items = items;
this.sortItems();
this._onDidChangeTreeData.fire();
}
onDeleteTopic(topicId: string) {
this.items = this.items.filter(i => i.id !== topicId);
this.sortItems();
this._onDidChangeTreeData.fire();
}
setSelectedItem(item: TopicTreeItem): void {
this.selectedItem = item;
}
getChildren(element?: TopicTreeItem): vscode.ProviderResult<TopicTreeItem[]> {
return this.items;
}
getTreeItem(element: TopicTreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
element.command = {
title: 'Select Item',
command: 'devchat-topicview.selectTopic',
arguments: [element],
};
return element;
}
reload(): void {
const topicList = TopicManager.getInstance().getTopicList();
this.onReloadTopics(topicList);
}
addItem(topic: Topic): void {
const newItem = new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
this.items.push(newItem);
this.sortItems();
this._onDidChangeTreeData.fire();
}
deleteItem(item: TopicTreeItem): void {
this.items = this.items.filter(i => i !== item);
this.sortItems();
this._onDidChangeTreeData.fire();
}
}
function getExtensionVersion(context: vscode.ExtensionContext): string {
const packageJsonPath = path.join(context.extensionUri.fsPath, 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
return packageJson.version;
}
import { LoggerChannelVscode } from './util/logger_vscode';
import { createStatusBarItem } from './panel/statusBarView';
import { UiUtilWrapper } from './util/uiUtil';
import { UiUtilVscode } from './util/uiUtil_vscode';
function activate(context: vscode.ExtensionContext) {
ExtensionContextHolder.context = context;
const extensionVersion = getExtensionVersion(context);
logger.init(context);
const secretStorage: vscode.SecretStorage = context.secrets;
vscode.commands.registerCommand('DevChat.OPENAI_API_KEY', async () => {
const passwordInput: string = await vscode.window.showInputBox({
password: true,
title: "OPENAI_API_KEY"
}) ?? '';
logger.init(LoggerChannelVscode.getInstance());
UiUtilWrapper.init(new UiUtilVscode());
secretStorage.store("devchat_OPENAI_API_KEY", passwordInput);
});
regLanguageContext();
const currentLocale = vscode.env.language;
if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') {
vscode.commands.executeCommand('setContext', 'isChineseLocale', true);
} else {
vscode.commands.executeCommand('setContext', 'isChineseLocale', false);
}
regDevChatView(context);
regTopicView(context);
registerApiKeySettingCommand(context);
registerOpenChatPanelCommand(context);
registerAddContextCommand(context);
registerAskForCodeCommand(context);
registerAskForFileCommand(context);
registerStatusBarItemClickCommand(context);
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
createStatusBarItem(context);
// Set the status bar item properties
// const iconPath = context.asAbsolutePath(path.join('assets', 'tank.png'));
// Set the status bar item properties
statusBarItem.text = `$(warning)DevChat`;
statusBarItem.tooltip = 'DevChat checking ..., please wait.';
statusBarItem.command = '';
// add a timer to update the status bar item
let devchatStatus = '';
let apiKeyStatus = '';
let isVersionChangeCompare: boolean|undefined = undefined;
setInterval(async () => {
const versionOld = await secretStorage.get("DevChatVersionOld");
const versionNew = extensionVersion;
const versionChanged = versionOld !== versionNew;
await secretStorage.store("DevChatVersionOld", versionNew!);
// status item has three status type
// 1. not in a folder
// 2. dependence is invalid
// 3. ready
if (devchatStatus === '' || devchatStatus === 'waiting install devchat') {
let bOk = true;
let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath');
if (!devChat) {
bOk = false;
}
if (!bOk) {
bOk = checkDevChatDependency();
}
if (bOk && versionChanged && !isVersionChangeCompare) {
logger.channel()?.info(`versionOld: ${versionOld}, versionNew: ${versionNew}, versionChanged: ${versionChanged}`);
bOk = false;
}
if (bOk) {
devchatStatus = 'ready';
TopicManager.getInstance().loadTopics();
} else {
if (devchatStatus === '') {
devchatStatus = 'not ready';
}
}
}
if (devchatStatus === 'not ready') {
// auto install devchat
const terminal = vscode.window.createTerminal("DevChat Install");
terminal.sendText(`python ${context.extensionUri.fsPath + "/tools/install.py"}`);
terminal.show();
devchatStatus = 'waiting install devchat';
isVersionChangeCompare = true;
}
if (devchatStatus !== 'ready') {
statusBarItem.text = `$(warning)DevChat`;
statusBarItem.tooltip = `${devchatStatus}`;
statusBarItem.command = undefined;
// set statusBarItem warning color
return;
}
// check api key
if (apiKeyStatus === '' || apiKeyStatus === 'please set api key') {
const bOk = await checkOpenaiApiKey();
if (bOk) {
apiKeyStatus = 'ready';
} else {
apiKeyStatus = 'please set api key';
}
}
if (apiKeyStatus !== 'ready') {
statusBarItem.text = `$(warning)DevChat`;
statusBarItem.tooltip = `${apiKeyStatus}`;
statusBarItem.command = 'DevChat.OPENAI_API_KEY';
return;
}
statusBarItem.text = `$(pass)DevChat`;
statusBarItem.tooltip = `ready to chat`;
statusBarItem.command = 'devcaht.onStatusBarClick';
}, 3000);
// Add the status bar item to the status bar
statusBarItem.show();
context.subscriptions.push(statusBarItem);
// Register the command
context.subscriptions.push(
vscode.commands.registerCommand('devcaht.onStatusBarClick', async () => {
await vscode.commands.executeCommand('devchat-view.focus');
})
);
ExtensionContextHolder.provider = new DevChatViewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, {
webviewOptions: { retainContextWhenHidden: true }
})
);
const yourTreeDataProvider = new TopicTreeDataProvider();
const yourTreeView = vscode.window.createTreeView('devchat-topicview', {
treeDataProvider: yourTreeDataProvider,
});
context.subscriptions.push(yourTreeView);
const topicDeleteCallback = async (item: TopicTreeItem) => {
const confirm = 'Delete';
const cancel = 'Cancel';
const label = typeof item.label === 'string' ? item.label : item.label!.label;
const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : '');
const result = await vscode.window.showWarningMessage(
`Are you sure you want to delete the topic "${truncatedLabel}"?`,
{ modal: true },
confirm,
cancel
);
if (result === confirm) {
TopicManager.getInstance().deleteTopic(item.id);
}
};
vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback);
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
{ pattern: '**', scheme: 'file' },
{
provideCodeActions: (document, range, context, token) => {
const deleteAction = new vscode.CodeAction('Delete Item', vscode.CodeActionKind.QuickFix);
deleteAction.command = {
title: 'Delete Item',
command: 'devchat-topicview.deleteTopic',
arguments: [context.diagnostics[0].code],
};
return [deleteAction];
},
},
{ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }
)
);
vscode.commands.registerCommand('devchat-topicview.addTopic', () => {
const topic = TopicManager.getInstance().createTopic();
TopicManager.getInstance().setCurrentTopic(topic.topicId);
});
vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => {
const selectedItem = yourTreeDataProvider.selectedItem;
if (selectedItem) {
topicDeleteCallback(selectedItem);
} else {
vscode.window.showErrorMessage('No item selected');
}
});
vscode.commands.registerCommand('devchat-topicview.selectTopic', (item: TopicTreeItem) => {
yourTreeDataProvider.setSelectedItem(item);
TopicManager.getInstance().setCurrentTopic(item.id);
});
vscode.commands.registerCommand('devchat-topicview.reloadTopic', async (item: TopicTreeItem) => {
TopicManager.getInstance().loadTopics();
});
context.subscriptions.push(
vscode.commands.registerCommand('devchat.applyDiffResult', async (data) => {
const activeEditor = vscode.window.activeTextEditor;
const fileName = activeEditor!.document.fileName;
const [leftUri, rightUri] = FilePairManager.getInstance().findPair(fileName) || [undefined, undefined];
if (leftUri && rightUri) {
// 获取对比的两个文件
const leftDoc = await vscode.workspace.openTextDocument(leftUri);
const rightDoc = await vscode.workspace.openTextDocument(rightUri);
// 将右边文档的内容替换到左边文档
const leftEditor = await vscode.window.showTextDocument(leftDoc);
await leftEditor.edit(editBuilder => {
const fullRange = new vscode.Range(0, 0, leftDoc.lineCount, 0);
editBuilder.replace(fullRange, rightDoc.getText());
});
// 保存左边文档
await leftDoc.save();
} else {
vscode.window.showErrorMessage('No file to apply diff result.');
}
})
);
regTopicDeleteCommand(context);
regAddTopicCommand(context);
regDeleteSelectTopicCommand(context);
regSelectTopicCommand(context);
regReloadTopicCommand(context);
regApplyDiffResultCommand(context);
}
exports.activate = activate;

View File

@ -2,15 +2,19 @@ import * as vscode from 'vscode';
import * as fs from 'fs';
import { MessageHandler } from './messageHandler';
import { regInMessage, regOutMessage } from '../util/reg_messages';
import { logger } from '../util/logger';
regInMessage({command: 'contextDetail', file: ''});
regOutMessage({command: 'contextDetailResponse', file: '', result: ''});
regInMessage({ command: 'contextDetail', file: '' });
regOutMessage({ command: 'contextDetailResponse', file: '', result: '' });
// message: { command: 'contextDetail', file: string }
// read detail context information from file
// return json string
export async function contextDetail(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
const fileContent = fs.readFileSync(message.file, 'utf-8');
MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file':message.file, result: fileContent });
return;
export async function contextDetail(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
try {
const fileContent = fs.readFileSync(message.file, 'utf-8');
MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file': message.file, result: fileContent });
} catch (error) {
logger.channel()?.error(`Error reading file ${ message.file }:, ${error}`);
}
}

View File

@ -1,141 +1,21 @@
import * as vscode from 'vscode';
import DevChat, { LogOptions, LogEntry } from '../toolwrapper/devchat';
import { MessageHandler } from './messageHandler';
import messageHistory from '../util/messageHistory';
import { regInMessage, regOutMessage } from '../util/reg_messages';
import { checkOpenaiApiKey } from '../contributes/commands';
import ExtensionContextHolder from '../util/extensionContext';
import { TopicManager } from '../topic/topicManager';
import { historyMessagesBase, onApiKeyBase } from './historyMessagesBase';
let isApiSet: boolean | undefined = undefined;
interface LoadHistoryMessages {
command: string;
entries: Array<LogEntry>;
}
function welcomeMessage(): LogEntry {
// create default logEntry to show welcome message
return {
hash: 'message',
parent: '',
user: 'system',
date: '',
request: 'How do I use DevChat?',
response: `
Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding.
Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting!
`,
context: []
} as LogEntry;
}
function apiKeyMissedMessage(): LogEntry {
// create default logEntry to show welcome message
return {
hash: 'message',
parent: '',
user: 'system',
date: '',
request: 'Is OPENAI_API_KEY ready?',
response: `
OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you.
`,
context: []
} as LogEntry;
}
regInMessage({command: 'historyMessages', options: { skip: 0, maxCount: 0 }});
regOutMessage({command: 'loadHistoryMessages', entries: [{hash: '',user: '',date: '',request: '',response: '',context: [{content: '',role: ''}]}]});
export async function historyMessages(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
const topicId = TopicManager.getInstance().currentTopicId;
let logEntriesFlat: Array<LogEntry> = [];
if (topicId) {
logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId);
const historyMessage = await historyMessagesBase();
if (historyMessage) {
MessageHandler.sendMessage(panel, historyMessage);
}
messageHistory.clear();
// TODO handle context
const logEntriesFlatFiltered = logEntriesFlat.map((entry) => {
return {
date: entry.date,
hash: entry.hash,
request: entry.request,
text: entry.response,
user: entry.user,
parentHash: '',
};
});
for (let i = 0; i < logEntriesFlat.length; i++) {
let entryOld = logEntriesFlat[i];
let entryNew = {
date: entryOld.date,
hash: entryOld.hash,
request: entryOld.request,
text: entryOld.response,
user: entryOld.user,
parentHash: '',
};
if (i > 0) {
entryNew.parentHash = logEntriesFlat[i - 1].hash;
}
messageHistory.add(entryNew);
}
const isApiKeyReady = await checkOpenaiApiKey();
isApiSet = true;
if (!isApiKeyReady) {
const startMessage = [ apiKeyMissedMessage() ];
isApiSet = false;
MessageHandler.sendMessage(panel, {
command: 'loadHistoryMessages',
entries: startMessage,
} as LoadHistoryMessages);
return;
}
const loadHistoryMessages: LoadHistoryMessages = {
command: 'loadHistoryMessages',
entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()],
};
MessageHandler.sendMessage(panel, loadHistoryMessages);
return;
}
export function isValidApiKey(apiKey: string) {
let apiKeyStrim = apiKey.trim();
if (apiKeyStrim.indexOf('sk-') !== 0 && apiKeyStrim.indexOf('DC.') !== 0) {
return false;
}
return true;
}
export async function isWaitForApiKey() {
if (isApiSet === undefined) {
isApiSet = await checkOpenaiApiKey();
}
return !isApiSet;
}
export async function onApiKey(apiKey: string, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
if (!isValidApiKey(apiKey)) {
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false });
return;
}
isApiSet = true;
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context?.secrets!;
secretStorage.store("devchat_OPENAI_API_KEY", apiKey);
const welcomeMessageText = welcomeMessage().response;
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false });
const resMessage = await onApiKeyBase(apiKey);
MessageHandler.sendMessage(panel, resMessage);
}

View File

@ -0,0 +1,143 @@
import { TopicManager } from '../topic/topicManager';
import { LogEntry } from '../toolwrapper/devchat';
import messageHistory from '../util/messageHistory';
import { ApiKeyManager } from '../util/apiKey';
import { logger } from '../util/logger';
let isApiSet: boolean | undefined = undefined;
interface LoadHistoryMessages {
command: string;
entries: Array<LogEntry>;
}
function welcomeMessage(): LogEntry {
// create default logEntry to show welcome message
return {
hash: 'message',
parent: '',
user: 'system',
date: '',
request: 'How do I use DevChat?',
response: `
Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding.
Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting!
`,
context: []
} as LogEntry;
}
function apiKeyMissedMessage(): LogEntry {
// create default logEntry to show welcome message
return {
hash: 'message',
parent: '',
user: 'system',
date: '',
request: 'Is OPENAI_API_KEY ready?',
response: `
OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you.
`,
context: []
} as LogEntry;
}
export function isValidApiKey(apiKey: string) {
let apiKeyStrim = apiKey.trim();
if (ApiKeyManager.getKeyType(apiKeyStrim) === undefined) {
return false;
}
return true;
}
export async function isWaitForApiKey() {
if (isApiSet === undefined) {
const apiKey = await ApiKeyManager.getApiKey();
isApiSet = apiKey !== undefined;
}
return !isApiSet;
}
export async function loadTopicHistoryLogs() : Promise<Array<LogEntry> | undefined> {
const topicId = TopicManager.getInstance().currentTopicId;
let logEntriesFlat: Array<LogEntry> = [];
if (topicId) {
logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId);
}
if (topicId !== TopicManager.getInstance().currentTopicId) {
logger.channel()?.info(`Current topic changed dure load topic hsitory!`)
return undefined;
}
return logEntriesFlat;
}
export function updateCurrentMessageHistory(logEntries: Array<LogEntry>): void {
messageHistory.clear();
for (let i = 0; i < logEntries.length; i++) {
let entryOld = logEntries[i];
let entryNew = {
date: entryOld.date,
hash: entryOld.hash,
request: entryOld.request,
text: entryOld.response,
user: entryOld.user,
parentHash: '',
};
if (i > 0) {
entryNew.parentHash = logEntries[i - 1].hash;
}
messageHistory.add(entryNew);
}
}
export async function apiKeyInvalidMessage(): Promise<LoadHistoryMessages|undefined> {
const apiKey = await ApiKeyManager.getApiKey();
isApiSet = true;
if (!apiKey) {
const startMessage = [ apiKeyMissedMessage() ];
isApiSet = false;
return {
command: 'loadHistoryMessages',
entries: startMessage,
} as LoadHistoryMessages;
} else {
return undefined;
}
}
export async function historyMessagesBase(): Promise<LoadHistoryMessages | undefined> {
const logEntriesFlat = await loadTopicHistoryLogs();
if (!logEntriesFlat) {
return undefined;
}
updateCurrentMessageHistory(logEntriesFlat);
const apiKeyMessage = await apiKeyInvalidMessage();
if (apiKeyMessage !== undefined) {
return apiKeyMessage;
}
return {
command: 'loadHistoryMessages',
entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()],
} as LoadHistoryMessages;
}
export async function onApiKeyBase(apiKey: string): Promise<{command: string, text: string, hash: string, user: string, date: string, isError: boolean}> {
if (!isValidApiKey(apiKey)) {
return { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false };
}
isApiSet = true;
ApiKeyManager.writeApiKeySecret(apiKey);
const welcomeMessageText = welcomeMessage().response;
return { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false };
}

View File

@ -5,9 +5,9 @@ import * as vscode from 'vscode';
import '../command/loadCommands';
import '../context/loadContexts';
import { logger } from '../util/logger';
import { on } from 'events';
import { isWaitForApiKey, onApiKey } from './historyMessages';
import { checkOpenaiApiKey } from '../contributes/commands';
import { isWaitForApiKey } from './historyMessagesBase';
import { onApiKey } from './historyMessages';
import { ApiKeyManager } from '../util/apiKey';
export class MessageHandler {
@ -20,6 +20,8 @@ export class MessageHandler {
this.handlers[command] = handler;
}
async handleMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
let isNeedSendResponse = false;
if (message.command === 'sendMessage') {
@ -34,7 +36,7 @@ export class MessageHandler {
}
}
if (message.command === 'sendMessage') {
if (await isWaitForApiKey() && !await checkOpenaiApiKey()) {
if (await isWaitForApiKey() && !await ApiKeyManager.getApiKey()) {
onApiKey(message.text, panel);
return;
}

View File

@ -1,76 +1,12 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import DevChat, { ChatResponse } from '../toolwrapper/devchat';
import CommandManager from '../command/commandManager';
import { logger } from '../util/logger';
import { MessageHandler } from './messageHandler';
import messageHistory from '../util/messageHistory';
import CustomCommands from '../command/customCommand';
import { regInMessage, regOutMessage } from '../util/reg_messages';
import { TopicManager } from '../topic/topicManager';
import { stopDevChatBase, sendMessageBase } from './sendMessageBase';
let _lastMessage: any = undefined;
// Add this function to messageHandler.ts
function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } {
const contextRegex = /\[context\|(.*?)\]/g;
const instructionRegex = /\[instruction\|(.*?)\]/g;
const referenceRegex = /\[reference\|(.*?)\]/g;
const contextPaths = [];
const instructionPaths = [];
const referencePaths = [];
let match;
// 提取 context
while ((match = contextRegex.exec(message)) !== null) {
contextPaths.push(match[1]);
}
// 提取 instruction
while ((match = instructionRegex.exec(message)) !== null) {
instructionPaths.push(match[1]);
}
// 提取 reference
while ((match = referenceRegex.exec(message)) !== null) {
referencePaths.push(match[1]);
}
// 移除标签,保留纯文本
const text = message
.replace(contextRegex, '')
.replace(instructionRegex, '')
.replace(referenceRegex, '')
.trim();
return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text };
}
function getInstructionFiles(): string[] {
const instructionFiles: string[] = [];
const customCommands = CustomCommands.getInstance().getCommands();
// visit customCommands, get default command
for (const command of customCommands) {
if (command.default) {
for (const instruction of command.instructions) {
instructionFiles.push(`./.chat/workflows/${command.name}/${instruction}`);
}
}
}
return instructionFiles;
}
const devChat = new DevChat();
let userStop = false;
regInMessage({command: 'sendMessage', text: '', hash: undefined});
regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'});
regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'});
@ -81,72 +17,12 @@ regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', dat
export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
_lastMessage = message;
const newText2 = await CommandManager.getInstance().processText(message.text);
const parsedMessage = parseMessage(newText2);
const chatOptions: any = {};
let parentHash = undefined;
logger.channel()?.info(`request message hash: ${message.hash}`);
if (message.hash) {
const hmessage = messageHistory.find(message.hash);
parentHash = hmessage ? message.parentHash : undefined;
} else {
const hmessage = messageHistory.findLast();
parentHash = hmessage ? hmessage.hash : undefined;
const responseMessage = await sendMessageBase(message, (data: { command: string, text: string, user: string, date: string}) => {
MessageHandler.sendMessage(panel, data, false);
});
if (responseMessage) {
MessageHandler.sendMessage(panel, responseMessage);
}
if (parentHash) {
chatOptions.parent = parentHash;
}
logger.channel()?.info(`parent hash: ${parentHash}`);
if (parsedMessage.context.length > 0) {
chatOptions.context = parsedMessage.context;
}
chatOptions.header = getInstructionFiles();
if (parsedMessage.instruction.length > 0) {
chatOptions.header = parsedMessage.instruction;
}
if (parsedMessage.reference.length > 0) {
chatOptions.reference = parsedMessage.reference;
}
let partialDataText = '';
const onData = (partialResponse: ChatResponse) => {
const responseText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
partialDataText = responseText;
MessageHandler.sendMessage(panel, { command: 'receiveMessagePartial', text: responseText, user: partialResponse.user, date: partialResponse.date }, false);
};
const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData);
if (!chatResponse.isError) {
messageHistory.add({request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date });
let topicId = TopicManager.getInstance().currentTopicId;
if (!topicId) {
// create new topic
const topic = TopicManager.getInstance().createTopic();
topicId = topic.topicId;
}
TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], Number(chatResponse.date), message.text, chatResponse.response);
}
let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
if (userStop) {
userStop = false;
if (responseText.indexOf('Exit code: undefined') >= 0) {
return;
}
}
if (chatResponse.isError) {
responseText = partialDataText + responseText;
}
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError });
return;
}
// regeneration last message again
@ -160,9 +36,7 @@ export async function regeneration(message: any, panel: vscode.WebviewPanel|vsco
regInMessage({command: 'stopDevChat'});
export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
logger.channel()?.info(`Stopping devchat`);
userStop = true;
devChat.stop();
stopDevChatBase(message);
}

View File

@ -0,0 +1,160 @@
import DevChat, { ChatResponse } from '../toolwrapper/devchat';
import CommandManager from '../command/commandManager';
import { logger } from '../util/logger';
import messageHistory from '../util/messageHistory';
import { TopicManager } from '../topic/topicManager';
import CustomCommands from '../command/customCommand';
// Add this function to messageHandler.ts
export function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } {
const contextRegex = /\[context\|(.*?)\]/g;
const instructionRegex = /\[instruction\|(.*?)\]/g;
const referenceRegex = /\[reference\|(.*?)\]/g;
const contextPaths = [];
const instructionPaths = [];
const referencePaths = [];
let match;
// 提取 context
while ((match = contextRegex.exec(message)) !== null) {
contextPaths.push(match[1]);
}
// 提取 instruction
while ((match = instructionRegex.exec(message)) !== null) {
instructionPaths.push(match[1]);
}
// 提取 reference
while ((match = referenceRegex.exec(message)) !== null) {
referencePaths.push(match[1]);
}
// 移除标签,保留纯文本
const text = message
.replace(contextRegex, '')
.replace(instructionRegex, '')
.replace(referenceRegex, '')
.trim();
return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text };
}
export function getInstructionFiles(): string[] {
const instructionFiles: string[] = [];
const customCommands = CustomCommands.getInstance().getCommands();
// visit customCommands, get default command
for (const command of customCommands) {
if (command.default) {
for (const instruction of command.instructions) {
instructionFiles.push(`./.chat/workflows/${command.name}/${instruction}`);
}
}
}
return instructionFiles;
}
const devChat = new DevChat();
let userStop = false;
// 将解析消息的部分提取到一个单独的函数中
export async function parseMessageAndSetOptions(message: any, chatOptions: any): Promise<{ context: string[]; instruction: string[]; reference: string[]; text: string }> {
const newText2 = await CommandManager.getInstance().processText(message.text);
const parsedMessage = parseMessage(newText2);
if (parsedMessage.context.length > 0) {
chatOptions.context = parsedMessage.context;
}
chatOptions.header = getInstructionFiles();
if (parsedMessage.instruction.length > 0) {
chatOptions.header = parsedMessage.instruction;
}
if (parsedMessage.reference.length > 0) {
chatOptions.reference = parsedMessage.reference;
}
return parsedMessage;
}
// 将处理父哈希的部分提取到一个单独的函数中
export function getParentHash(message: any): string|undefined {
let parentHash = undefined;
logger.channel()?.info(`request message hash: ${message.hash}`);
if (message.hash) {
const hmessage = messageHistory.find(message.hash);
parentHash = hmessage ? hmessage.parentHash : undefined;
} else {
const hmessage = messageHistory.findLast();
parentHash = hmessage ? hmessage.hash : undefined;
}
logger.channel()?.info(`parent hash: ${parentHash}`);
return parentHash;
}
export async function handleTopic(parentHash:string, message: any, chatResponse: ChatResponse) {
if (!chatResponse.isError) {
messageHistory.add({ request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date });
let topicId = TopicManager.getInstance().currentTopicId;
if (!topicId) {
// create new topic
const topic = TopicManager.getInstance().createTopic();
topicId = topic.topicId;
}
TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], Number(chatResponse.date), message.text, chatResponse.response);
}
}
export async function handlerResponseText(partialDataText: string, chatResponse: ChatResponse) : Promise<string|undefined> {
let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
if (userStop) {
userStop = false;
if (responseText == '' && chatResponse.isError) {
return undefined;
}
}
if (chatResponse.isError) {
responseText = partialDataText + '\n\n' + responseText;
}
return responseText;
}
// 重构后的sendMessage函数
export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> {
const chatOptions: any = {};
const parsedMessage = await parseMessageAndSetOptions(message, chatOptions);
const parentHash = getParentHash(message);
if (parentHash) {
chatOptions.parent = parentHash;
}
let partialDataText = '';
const onData = (partialResponse: ChatResponse) => {
partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date });
};
const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData);
await handleTopic(parentHash!, message, chatResponse);
const responseText = await handlerResponseText(partialDataText, chatResponse);
if (responseText === undefined) {
return;
}
return { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError };
}
export async function stopDevChatBase(message: any): Promise<void> {
logger.channel()?.info(`Stopping devchat`);
userStop = true;
devChat.stop();
}

View File

@ -8,6 +8,7 @@ import { regInMessage, regOutMessage } from '../util/reg_messages';
import { applyCodeChanges, isValidActionString } from '../util/appyDiff';
import { logger } from '../util/logger';
import { FilePairManager } from '../util/diffFilePairs';
import { UiUtilWrapper } from '../util/uiUtil';
@ -64,7 +65,7 @@ export async function diffView(code: string, tarFile: string) {
const tempFile = path.join(tempDir, fileName);
// save code to temp file
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(code));
await UiUtilWrapper.writeFile(tempFile, code);
// open diff view
FilePairManager.getInstance().addFilePair(curFile, tempFile);

View File

@ -1,9 +1,9 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as ncp from 'ncp';
import { logger } from '../util/logger';
import { UiUtilWrapper } from '../util/uiUtil';
function copyFileSync(source: string, target: string) {
@ -32,13 +32,11 @@ function copyFileSync(source: string, target: string) {
}
export function createChatDirectoryAndCopyInstructionsSync(extensionUri: vscode.Uri) {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
const workspaceRoot = UiUtilWrapper.workspaceFoldersFirstPath();
if (!workspaceRoot) {
return;
}
const workspaceRoot = workspaceFolders[0].uri.fsPath;
const chatWorkflowsDirPath = path.join(workspaceRoot, '.chat', 'workflows');
const instructionsSrcPath = path.join(extensionUri.fsPath, 'workflows');

View File

@ -9,6 +9,7 @@ import WebviewManager from './webviewManager';
import CustomCommands from '../command/customCommand';
import CommandManager from '../command/commandManager';
import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig';
import { UiUtilWrapper } from '../util/uiUtil';
export default class ChatPanel {
private static _instance: ChatPanel | undefined;
@ -20,7 +21,7 @@ export default class ChatPanel {
// 创建 .chat 目录并复制 workflows
createChatDirectoryAndCopyInstructionsSync(extensionUri);
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
if (workspaceDir) {
const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows');
CustomCommands.getInstance().parseCommands(workflowsDir);

View File

@ -8,6 +8,7 @@ import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig';
import ExtensionContextHolder from '../util/extensionContext';
import CustomCommands from '../command/customCommand';
import { TopicManager } from '../topic/topicManager';
import { UiUtilWrapper } from '../util/uiUtil';
export class DevChatViewProvider implements vscode.WebviewViewProvider {
@ -27,7 +28,7 @@ export class DevChatViewProvider implements vscode.WebviewViewProvider {
// 创建 .chat 目录并复制 workflows
createChatDirectoryAndCopyInstructionsSync(ExtensionContextHolder.context?.extensionUri!);
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
if (workspaceDir) {
const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows');
CustomCommands.getInstance().parseCommands(workflowsDir);

View File

@ -0,0 +1,42 @@
import * as vscode from 'vscode';
import { dependencyCheck } from './statusBarViewBase';
export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem {
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
// Set the status bar item properties
statusBarItem.text = `$(warning)DevChat`;
statusBarItem.tooltip = 'DevChat checking ..., please wait.';
statusBarItem.command = '';
// add a timer to update the status bar item
setInterval(async () => {
const [devchatStatus, apiKeyStatus] = await dependencyCheck();
if (devchatStatus !== 'ready') {
statusBarItem.text = `$(warning)DevChat`;
statusBarItem.tooltip = `${devchatStatus}`;
statusBarItem.command = undefined;
// set statusBarItem warning color
return;
}
if (apiKeyStatus !== 'ready') {
statusBarItem.text = `$(warning)DevChat`;
statusBarItem.tooltip = `${apiKeyStatus}`;
statusBarItem.command = 'DevChat.OPENAI_API_KEY';
return;
}
statusBarItem.text = `$(pass)DevChat`;
statusBarItem.tooltip = `ready to chat`;
statusBarItem.command = 'devcaht.onStatusBarClick';
}, 3000);
// Add the status bar item to the status bar
statusBarItem.show();
context.subscriptions.push(statusBarItem);
return statusBarItem;
}

View File

@ -0,0 +1,81 @@
import * as fs from 'fs';
import * as path from 'path';
import { logger } from "../util/logger";
import { UiUtilWrapper } from "../util/uiUtil";
import { TopicManager } from "../topic/topicManager";
import { checkDevChatDependency } from "../contributes/commandsBase";
import { ApiKeyManager } from '../util/apiKey';
function getExtensionVersion(): string {
const packageJsonPath = path.join(UiUtilWrapper.extensionPath(), 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
return packageJson.version;
}
let devchatStatus = '';
let apiKeyStatus = '';
let isVersionChangeCompare: boolean|undefined = undefined;
export async function dependencyCheck(): Promise<[string, string]> {
let versionChanged = false;
if (isVersionChangeCompare === undefined) {
const versionOld = await UiUtilWrapper.secretStorageGet("DevChatVersionOld");
const versionNew = getExtensionVersion();
const versionChanged = versionOld !== versionNew;
UiUtilWrapper.storeSecret("DevChatVersionOld", versionNew!);
isVersionChangeCompare = true;
logger.channel()?.info(`versionOld: ${versionOld}, versionNew: ${versionNew}, versionChanged: ${versionChanged}`);
}
// status item has three status type
// 1. not in a folder
// 2. dependence is invalid
// 3. ready
if (devchatStatus === '' || devchatStatus === 'waiting install devchat') {
let bOk = true;
let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath');
if (!devChat) {
bOk = false;
}
if (!bOk) {
bOk = checkDevChatDependency();
}
if (bOk && versionChanged) {
bOk = false;
}
if (bOk) {
devchatStatus = 'ready';
TopicManager.getInstance().loadTopics();
} else {
if (devchatStatus === '') {
devchatStatus = 'not ready';
}
}
}
if (devchatStatus === 'not ready') {
// auto install devchat
UiUtilWrapper.runTerminal('DevChat Install', `python ${UiUtilWrapper.extensionPath() + "/tools/install.py"}`);
devchatStatus = 'waiting install devchat';
isVersionChangeCompare = true;
}
// check api key
if (apiKeyStatus === '' || apiKeyStatus === 'please set api key') {
const bOk = await ApiKeyManager.getApiKey();
if (bOk) {
apiKeyStatus = 'ready';
} else {
apiKeyStatus = 'please set api key';
}
}
return [devchatStatus, apiKeyStatus];
}

129
src/panel/topicView.ts Normal file
View File

@ -0,0 +1,129 @@
import * as vscode from 'vscode';
import { TopicManager, Topic } from '../topic/topicManager';
export class TopicTreeItem extends vscode.TreeItem {
id: string;
date: number | undefined;
constructor(label: string, id: string, date: number | undefined, collapsibleState: vscode.TreeItemCollapsibleState) {
super(label, collapsibleState);
this.id = id;
this.date = date;
this.iconPath = new vscode.ThemeIcon('symbol-variable');
this.contextValue = 'yourTreeItem';
}
uncheck() {
this.iconPath = new vscode.ThemeIcon('symbol-variable');
}
check() {
this.iconPath = new vscode.ThemeIcon('check');
}
}
export class TopicTreeDataProvider implements vscode.TreeDataProvider<TopicTreeItem> {
private static instance: TopicTreeDataProvider;
public static getInstance(): TopicTreeDataProvider {
if (!TopicTreeDataProvider.instance) {
TopicTreeDataProvider.instance = new TopicTreeDataProvider();
}
return TopicTreeDataProvider.instance;
}
private _onDidChangeTreeData: vscode.EventEmitter<TopicTreeItem | undefined | null | void> = new vscode.EventEmitter<TopicTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<TopicTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
public selectedItem: TopicTreeItem | null = null;
private items: TopicTreeItem[] = [];
// reg listeners to TopicManager in constructor
private constructor() {
TopicManager.getInstance().addOnCreateTopicListener(this.addItem.bind(this));
TopicManager.getInstance().addOnDeleteTopicListener(this.onDeleteTopic.bind(this));
TopicManager.getInstance().addOnReloadTopicsListener(this.onReloadTopics.bind(this));
TopicManager.getInstance().addOnUpdateTopicListener(this.onUpdateTopics.bind(this));
}
// sort items
private sortItems() {
this.items.sort((a, b) => {
if (a.date && b.date) {
return b.date - a.date;
} else if (!a.date) {
return -1;
} else if (!b.date) {
return 1;
} else {
return 0;
}
});
}
onUpdateTopics(topicId: string) {
const items = this.items.filter(i => i.id === topicId);
const topic = TopicManager.getInstance().getTopic(topicId);
items.map((item) => {
item.label = topic?.name;
item.date = topic?.lastUpdated;
});
this.sortItems();
this._onDidChangeTreeData.fire();
}
onReloadTopics(topics: Topic[]) {
const items = topics.map((topic) => {
return new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
});
this.items = items;
this.sortItems();
this._onDidChangeTreeData.fire();
}
onDeleteTopic(topicId: string) {
this.items = this.items.filter(i => i.id !== topicId);
this.sortItems();
this._onDidChangeTreeData.fire();
}
setSelectedItem(item: TopicTreeItem): void {
this.items.map((item) => {
item.uncheck();
});
item.check();
this.selectedItem = item;
this._onDidChangeTreeData.fire();
}
getChildren(element?: TopicTreeItem): vscode.ProviderResult<TopicTreeItem[]> {
return this.items;
}
getTreeItem(element: TopicTreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
element.command = {
title: 'Select Item',
command: 'devchat-topicview.selectTopic',
arguments: [element],
};
return element;
}
reload(): void {
const topicList = TopicManager.getInstance().getTopicList();
this.onReloadTopics(topicList);
}
addItem(topic: Topic): void {
const newItem = new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
this.items.push(newItem);
this.sortItems();
this._onDidChangeTreeData.fire();
}
deleteItem(item: TopicTreeItem): void {
this.items = this.items.filter(i => i !== item);
this.sortItems();
this._onDidChangeTreeData.fire();
}
}

View File

@ -1,5 +1,4 @@
// devchat.ts
import * as vscode from 'vscode';
import * as dotenv from 'dotenv';
import * as path from 'path';
import * as fs from 'fs';
@ -7,6 +6,8 @@ import * as fs from 'fs';
import { logger } from '../util/logger';
import { CommandRun } from "../util/commonUtil";
import ExtensionContextHolder from '../util/extensionContext';
import { UiUtilWrapper } from '../util/uiUtil';
import { ApiKeyManager } from '../util/apiKey';
@ -84,18 +85,6 @@ class DevChat {
return args;
}
async getOpenaiApiKey(): Promise<string | undefined> {
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
let openaiApiKey = await secretStorage.get("devchat_OPENAI_API_KEY");
if (!openaiApiKey) {
openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY');
}
if (!openaiApiKey) {
openaiApiKey = process.env.OPENAI_API_KEY;
}
return openaiApiKey;
}
private parseOutData(stdout: string, isPartial: boolean): ChatResponse {
const responseLines = stdout.trim().split("\n");
@ -148,15 +137,7 @@ class DevChat {
}
apiEndpoint(apiKey: string | undefined): any {
let openAiApiBase: string | undefined = undefined;
if (apiKey?.startsWith("DC.")) {
// TODO add devchat proxy
openAiApiBase = "https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1";
}
if (vscode.workspace.getConfiguration('DevChat').get('API_ENDPOINT')) {
openAiApiBase = vscode.workspace.getConfiguration('DevChat').get('API_ENDPOINT');
}
const openAiApiBase = ApiKeyManager.getEndPoint(apiKey);
const openAiApiBaseObject = openAiApiBase ? { OPENAI_API_BASE: openAiApiBase } : {};
return openAiApiBaseObject;
@ -166,8 +147,8 @@ class DevChat {
const args = await this.buildArgs(options);
args.push(content);
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
let openaiApiKey = await this.getOpenaiApiKey();
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
let openaiApiKey = await ApiKeyManager.getApiKey();
if (!openaiApiKey) {
logger.channel()?.error('OpenAI key is invalid!');
logger.channel()?.show();
@ -177,13 +158,13 @@ class DevChat {
// 如果配置了devchat的TOKEN那么就需要使用默认的代理
let openAiApiBaseObject = this.apiEndpoint(openaiApiKey);
const openaiModel = vscode.workspace.getConfiguration('DevChat').get('OpenAI.model');
const openaiTemperature = vscode.workspace.getConfiguration('DevChat').get('OpenAI.temperature');
const openaiStream = vscode.workspace.getConfiguration('DevChat').get('OpenAI.stream');
const llmModel = vscode.workspace.getConfiguration('DevChat').get('llmModel');
const tokensPerPrompt = vscode.workspace.getConfiguration('DevChat').get('OpenAI.tokensPerPrompt');
const openaiModel = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.model');
const openaiTemperature = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.temperature');
const openaiStream = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.stream');
const llmModel = UiUtilWrapper.getConfiguration('DevChat', 'llmModel');
const tokensPerPrompt = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.tokensPerPrompt');
let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath');
let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath');
if (!devChat) {
devChat = 'devchat';
}
@ -227,12 +208,11 @@ class DevChat {
const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnAsyncOptions, onStdoutPartial, undefined, undefined, undefined);
if (stderr) {
const errorMessage = stderr.trim().match(/Error(.+)/)?.[1];
return {
"prompt-hash": "",
user: "",
date: "",
response: errorMessage ? `Error: ${errorMessage}` : "Unknown error",
response: stderr,
isError: true,
};
}
@ -253,7 +233,7 @@ class DevChat {
async log(options: LogOptions = {}): Promise<LogEntry[]> {
const args = this.buildLogArgs(options);
const devChat = this.getDevChatPath();
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
const openaiApiKey = process.env.OPENAI_API_KEY;
logger.channel()?.info(`Running devchat with args: ${args.join(" ")}`);
@ -286,7 +266,7 @@ class DevChat {
if (options.maxCount) {
args.push('--max-count', `${options.maxCount}`);
} else {
const maxLogCount = vscode.workspace.getConfiguration('DevChat').get('maxLogCount');
const maxLogCount = UiUtilWrapper.getConfiguration('DevChat', 'maxLogCount');
args.push('--max-count', `${maxLogCount}`);
}
@ -294,7 +274,7 @@ class DevChat {
}
private getDevChatPath(): string {
let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath');
let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath');
if (!devChat) {
devChat = 'devchat';
}

View File

@ -1,10 +1,10 @@
import { spawn } from "child_process";
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { logger } from "../util/logger";
import { CommandRun } from "../util/commonUtil";
import { UiUtilWrapper } from "../util/uiUtil";
interface DtmResponse {
status: number;
@ -17,7 +17,7 @@ class DtmWrapper {
private commandRun: CommandRun;
constructor() {
this.workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.';
this.workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() || '.';
this.commandRun = new CommandRun();
}

View File

@ -1,11 +1,11 @@
import { v4 as uuidv4 } from 'uuid';
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import DevChat, { LogEntry, LogOptions } from '../toolwrapper/devchat';
import { loadTopicList } from './loadTopics';
import { UiUtilWrapper } from '../util/uiUtil';
export class Topic {
name: string | undefined;
@ -194,7 +194,7 @@ export class TopicManager {
if (topic.firstMessageHash) {
// get ${WORKSPACE_ROOT}/.chat/.deletedTopics
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
// read ${WORKSPACE_ROOT}/.chat/.deletedTopics as String[]
@ -218,7 +218,7 @@ export class TopicManager {
}
isDeleteTopic(topicId: string) {
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
if (!fs.existsSync(deletedTopicsPath)) {

41
src/util/apiKey.ts Normal file
View File

@ -0,0 +1,41 @@
// src/apiKey.ts
import { UiUtilWrapper } from './uiUtil';
export class ApiKeyManager {
static async getApiKey(): Promise<string | undefined> {
let apiKey = await UiUtilWrapper.secretStorageGet("devchat_OPENAI_API_KEY");
if (!apiKey) {
apiKey = UiUtilWrapper.getConfiguration('DevChat', 'API_KEY');
}
if (!apiKey) {
apiKey = process.env.OPENAI_API_KEY;
}
return apiKey;
}
static getKeyType(apiKey: string): string | undefined {
if (apiKey.startsWith("sk.")) {
return "sk";
} else if (apiKey.startsWith("DC.")) {
return "DC";
} else {
return undefined;
}
}
static async writeApiKeySecret(apiKey: string): Promise<void> {
await UiUtilWrapper.storeSecret("devchat_OPENAI_API_KEY", apiKey);
}
static getEndPoint(apiKey: string | undefined): string | undefined {
let endPoint = UiUtilWrapper.getConfiguration('DevChat', 'API_ENDPOINT');
if (!endPoint) {
endPoint = process.env.OPENAI_API_BASE;
}
if (!endPoint && apiKey?.startsWith("DC.")) {
endPoint = "https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1";
}
return endPoint;
}
}

View File

@ -19,9 +19,10 @@ function findMatchingIndex(list1: string[], list2: string[]): number[] {
let isMatch = true;
for (let j = 0; j < list2.length; j++) {
if (list1[i + j].trim() !== list2[j].trim()) {
if (j > 0) {
//if (j > 0) {
logger.channel()?.info(`findMatchingIndex end at ${j} ${list1[i + j].trim()} != ${list2[j].trim()}`);
}
//}
isMatch = false;
break;

View File

@ -1,11 +1,13 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as childProcess from 'child_process';
import { parseArgsStringToArgv } from 'string-argv';
import { logger } from './logger';
import { spawn, exec } from 'child_process';
import { UiUtilWrapper } from './uiUtil';
export function createTempSubdirectory(subdir: string): string {
// 获取系统临时目录
@ -77,7 +79,7 @@ export class CommandRun {
if (code === 0) {
resolve({ exitCode: code, stdout, stderr });
} else {
reject({ exitCode: code, stdout, stderr });
resolve({ exitCode: code, stdout, stderr });
}
});
@ -90,7 +92,7 @@ export class CommandRun {
logger.channel()?.error(`Error occurred: ${error.message}`);
logger.channel()?.show();
}
reject({ exitCode: error.code, stdout: "", stderr: error.message });
resolve({ exitCode: error.code, stdout: "", stderr: error.message });
});
});
};
@ -110,7 +112,7 @@ export async function runCommandAndWriteOutput(
): Promise<CommandResult> {
const run = new CommandRun();
const options = {
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.',
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.',
};
return run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile);
@ -122,7 +124,7 @@ export async function runCommandStringAndWriteOutput(
): Promise<CommandResult> {
const run = new CommandRun();
const options = {
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.'
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.'
};
// Split the commandString into command and args array using string-argv
@ -143,13 +145,14 @@ export async function runCommandStringAndWriteOutput(
export async function getLanguageIdByFileName(fileName: string): Promise<string | undefined> {
try {
// 打开指定的文件
const document = await vscode.workspace.openTextDocument(fileName);
// 获取文件的语言标识符
const languageId = document.languageId;
const languageId = await UiUtilWrapper.languageId(fileName);
return languageId;
} catch (error) {
// 如果无法打开文件或发生其他错误返回undefined
return undefined;
}
}
export function runCommand(command: string): string {
return childProcess.execSync(command).toString();
}

View File

@ -1,12 +1,19 @@
import * as vscode from 'vscode'
export interface LogChannel {
info(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string | Error, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
show(): void;
}
export class logger {
private static _channel: vscode.LogOutputChannel | undefined;
public static init(context: vscode.ExtensionContext): void {
this._channel = vscode.window.createOutputChannel('DevChat', { log: true });
private static _channel: LogChannel | undefined;
public static init(channel: LogChannel): void {
this._channel = channel;
}
public static channel(): vscode.LogOutputChannel | undefined {
public static channel(): LogChannel | undefined {
return this._channel;
}
}

39
src/util/logger_vscode.ts Normal file
View File

@ -0,0 +1,39 @@
import { LogChannel } from "./logger";
import * as vscode from 'vscode';
export class LoggerChannelVscode implements LogChannel {
_channel: vscode.LogOutputChannel;
private static _instance: LoggerChannelVscode;
private constructor() {
this._channel = vscode.window.createOutputChannel('DevChat', { log: true });
}
public static getInstance(): LoggerChannelVscode {
if (!this._instance) {
this._instance = new LoggerChannelVscode();
}
return this._instance;
}
info(message: string, ...args: any[]): void {
this._channel.info(message, ...args);
}
warn(message: string, ...args: any[]): void {
this._channel.warn(message, ...args);
}
error(message: string | Error, ...args: any[]): void {
this._channel.error(message, ...args);
}
debug(message: string, ...args: any[]): void {
this._channel.debug(message, ...args);
}
show(): void {
this._channel.show();
}
}

View File

@ -1,6 +1,5 @@
import * as vscode from 'vscode';
class MessageHistory {
export class MessageHistory {
private history: any[];
private lastmessage: any | null;
@ -21,10 +20,6 @@ class MessageHistory {
return this.lastmessage;
}
remove() {
return;
}
clear() {
this.history = [];
this.lastmessage = null;

48
src/util/uiUtil.ts Normal file
View File

@ -0,0 +1,48 @@
export interface UiUtil {
languageId(uri: string): Promise<string>;
workspaceFoldersFirstPath(): string | undefined;
getConfiguration(key1: string, key2: string): string | undefined;
secretStorageGet(key: string): Promise<string | undefined>;
writeFile(uri: string, content: string): Promise<void>;
showInputBox(option: object): Promise<string | undefined>;
storeSecret(key: string, value: string): Promise<void>;
extensionPath(): string;
runTerminal(terminalName:string, command: string): void;
}
export class UiUtilWrapper {
private static _uiUtil: UiUtil | undefined;
public static init(uiUtil: UiUtil): void {
this._uiUtil = uiUtil;
}
public static async languageId(uri: string): Promise<string | undefined> {
return await this._uiUtil?.languageId(uri);
}
public static workspaceFoldersFirstPath(): string | undefined {
return this._uiUtil?.workspaceFoldersFirstPath();
}
public static getConfiguration(key1: string, key2: string): string | undefined {
return this._uiUtil?.getConfiguration(key1, key2);
}
public static async secretStorageGet(key: string): Promise<string | undefined> {
return await this._uiUtil?.secretStorageGet(key);
}
public static async writeFile(uri: string, content: string): Promise<void> {
return await this._uiUtil?.writeFile(uri, content);
}
public static async showInputBox(option: object): Promise<string | undefined> {
return await this._uiUtil?.showInputBox(option);
}
public static async storeSecret(key: string, value: string): Promise<void> {
return await this._uiUtil?.storeSecret(key, value);
}
public static extensionPath(): string {
return this._uiUtil?.extensionPath()!;
}
public static runTerminal(terminalName: string, command: string): void {
this._uiUtil?.runTerminal(terminalName, command);
}
}

42
src/util/uiUtil_vscode.ts Normal file
View File

@ -0,0 +1,42 @@
import * as vscode from 'vscode';
import ExtensionContextHolder from './extensionContext';
import { UiUtil } from './uiUtil';
export class UiUtilVscode implements UiUtil {
public async languageId(uri: string): Promise<string> {
const document = await vscode.workspace.openTextDocument(uri);
return document.languageId;
}
public workspaceFoldersFirstPath(): string | undefined {
return vscode.workspace.workspaceFolders?.[0].uri.fsPath;
}
public getConfiguration(key1: string, key2: string): string | undefined {
return vscode.workspace.getConfiguration(key1).get(key2);
}
public async secretStorageGet(key: string): Promise<string | undefined> {
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
let openaiApiKey = await secretStorage.get(key);
return openaiApiKey;
}
public async writeFile(uri: string, content: string): Promise<void> {
await vscode.workspace.fs.writeFile(vscode.Uri.file(uri), Buffer.from(content));
}
public async showInputBox(option: object): Promise<string | undefined> {
return vscode.window.showInputBox(option);
}
public async storeSecret(key: string, value: string): Promise<void> {
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
await secretStorage.store(key, value);
}
public extensionPath(): string {
return ExtensionContextHolder.context!.extensionUri.fsPath;
}
public runTerminal(terminalName: string, command: string): void {
const terminal = vscode.window.createTerminal(terminalName);
terminal.sendText(command);
terminal.show();
}
}

View File

@ -0,0 +1,76 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import CommandManager, { Command } from '../../src/command/commandManager';
import CustomCommands, { Command as CCommand} from '../../src/command/customCommand';
describe('CommandManager', () => {
let commandManager: CommandManager;
beforeEach(() => {
commandManager = CommandManager.getInstance();
});
afterEach(() => {
// Reset the command list after each test
commandManager['commands'] = [];
});
it('should register a command', () => {
const command: Command = {
name: 'test',
pattern: 'test',
description: 'Test command',
handler: async (commandName: string, userInput: string) => {
return 'Test result';
},
};
commandManager.registerCommand(command);
expect(commandManager['commands']).to.include(command);
});
it('should return the command list', () => {
const command: Command = {
name: 'test',
pattern: 'test',
description: 'Test command',
handler: async (commandName: string, userInput: string) => {
return 'Test result';
},
};
commandManager.registerCommand(command);
expect(commandManager.getCommandList()).to.include(command);
});
it('should process text with a command', async () => {
const command: Command = {
name: 'test',
pattern: 'test',
description: 'Test command',
handler: async (commandName: string, userInput: string) => {
return 'Test result';
},
};
commandManager.registerCommand(command);
const result = await commandManager.processText('/test');
expect(result).to.equal('Test result');
});
it('should process text with a custom command', async () => {
const customCommand: CCommand = {
name: 'customTest',
pattern: 'customTest',
description: 'Custom test command',
message: 'Custom test result',
show: true,
default: false,
instructions: []
};
CustomCommands.getInstance().regCommand(customCommand);
const result = await commandManager.processText('/customTest');
expect(result).to.equal(' Custom test result');
});
});

View File

@ -0,0 +1,106 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import mockFs from 'mock-fs';
import * as fs from 'fs';
import * as path from 'path';
import CustomCommands, { Command } from '../../src/command/customCommand';
describe('CustomCommands', () => {
let customCommands: CustomCommands;
beforeEach(() => {
customCommands = CustomCommands.getInstance();
});
afterEach(() => {
// Reset the command list after each test
customCommands['commands'] = [];
mockFs.restore();
});
it('should parse commands from workflows directory', () => {
// Mock the file system with two directories, one with _setting_.json and one without
mockFs({
'workflows': {
'command1': {
'_setting_.json': JSON.stringify({
pattern: 'command1',
description: 'Command 1',
message: 'Command 1 message',
default: false,
show: true,
instructions: ['instruction1', 'instruction2'],
}),
},
'command2': {
// No _setting_.json file
},
},
});
const workflowsDir = path.join(process.cwd(), 'workflows');
customCommands.parseCommands(workflowsDir);
const expectedResult: Command[] = [
{
name: 'command1',
pattern: 'command1',
description: 'Command 1',
message: 'Command 1 message',
default: false,
show: true,
instructions: ['instruction1', 'instruction2'],
},
];
expect(customCommands['commands']).to.deep.equal(expectedResult);
});
it('should register a custom command', () => {
const command: Command = {
name: 'test',
pattern: 'test',
description: 'Test command',
message: 'Test message',
default: false,
show: true,
instructions: ['instruction1', 'instruction2'],
};
customCommands.regCommand(command);
expect(customCommands['commands']).to.include(command);
});
it('should get a custom command by name', () => {
const command: Command = {
name: 'test',
pattern: 'test',
description: 'Test command',
message: 'Test message',
default: false,
show: true,
instructions: ['instruction1', 'instruction2'],
};
customCommands.regCommand(command);
const foundCommand = customCommands.getCommand('test');
expect(foundCommand).to.deep.equal(command);
});
it('should handle a custom command', () => {
const command: Command = {
name: 'test',
pattern: 'test',
description: 'Test command',
message: 'Test message',
default: false,
show: true,
instructions: ['instruction1', 'instruction2'],
};
customCommands.regCommand(command);
const result = customCommands.handleCommand('test');
expect(result).to.equal('[instruction|./.chat/workflows/test/instruction1] [instruction|./.chat/workflows/test/instruction2] Test message');
});
});

View File

@ -0,0 +1,43 @@
import { expect } from 'chai';
import { describe, it, afterEach, beforeEach } from 'mocha';
import { handleCodeSelected } from '../../src/context/contextCodeSelected';
import * as path from 'path';
import { UiUtilWrapper } from '../../src/util/uiUtil';
import sinon from 'sinon';
describe('handleCodeSelected', () => {
let languageIdStub: sinon.SinonStub;
let workspaceFoldersFirstPathStub: sinon.SinonStub;
let writeFileStub: sinon.SinonStub;
beforeEach(() => {
// Mock UiUtilWrapper functions
languageIdStub = sinon.stub(UiUtilWrapper, 'languageId').resolves('typescript');
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath').returns('test');
writeFileStub = sinon.stub(UiUtilWrapper, 'writeFile').resolves();
});
afterEach(() => {
// Restore the original functions after each test
languageIdStub.restore();
workspaceFoldersFirstPathStub.restore();
writeFileStub.restore();
});
it('should create a context file with the correct content', async () => {
const fileSelected = path.join(__dirname, 'testFile.ts');
const codeSelected = 'console.log("Hello, world!");';
const contextFile = await handleCodeSelected(fileSelected, codeSelected);
// Check if the mocked functions were called with the correct arguments
expect(languageIdStub.calledWith(fileSelected)).to.be.true;
expect(workspaceFoldersFirstPathStub.called).to.be.true;
expect(writeFileStub.called).to.be.true;
// Extract the temp file path from the context string
const tempFilePath = contextFile.match(/\[context\|(.*?)\]/)?.[1];
expect(tempFilePath).to.not.be.undefined;
});
});

View File

@ -0,0 +1,18 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import '../../src/context/loadContexts';
import ChatContextManager from '../../src/context/contextManager';
import { gitDiffCachedContext } from '../../src/context/contextGitDiffCached';
import { gitDiffContext } from '../../src/context/contextGitDiff';
import { customCommandContext } from '../../src/context/contextCustomCommand';
describe('loadContexts', () => {
it('should register all contexts', () => {
const chatContextManager = ChatContextManager.getInstance();
const contextList = chatContextManager.getContextList();
expect(contextList).to.include(gitDiffCachedContext);
expect(contextList).to.include(gitDiffContext);
expect(contextList).to.include(customCommandContext);
});
});

View File

@ -0,0 +1,84 @@
// test/commandsBase.test.ts
import { expect } from 'chai';
import * as commonUtil from '../../src/util/commonUtil';
import * as commandsBase from '../../src/contributes/commandsBase';
import sinon from 'sinon';
describe('commandsBase', () => {
afterEach(() => {
sinon.restore();
});
describe('checkDevChatDependency', () => {
it('should return true if DevChat is installed', () => {
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
if (command === 'python3 -m pipx environment') {
return 'PIPX_BIN_DIR=/path/to/bin';
} else if (command === 'devchat --help') {
return 'DevChat help text';
}
return '';
});
const result = commandsBase.checkDevChatDependency();
expect(result).to.be.true;
});
it('should return false if DevChat is not installed', () => {
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
if (command === 'python3 -m pipx environment') {
return 'PIPX_BIN_DIR=/path/to/bin';
} else if (command === 'devchat --help') {
throw new Error('Command not found');
}
return '';
});
const result = commandsBase.checkDevChatDependency();
expect(result).to.be.false;
});
it('should return false if pipx environment is not found', () => {
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
if (command === 'python3 -m pipx environment') {
return 'No pipx environment found';
}
return '';
});
const result = commandsBase.checkDevChatDependency();
expect(result).to.be.false;
});
});
describe('getPipxEnvironmentPath', () => {
afterEach(() => {
sinon.restore();
});
it('should return the pipx environment path if found', () => {
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
if (command === 'python3 -m pipx environment') {
return 'PIPX_BIN_DIR=/path/to/bin';
}
return '';
});
const result = commandsBase.getPipxEnvironmentPath();
expect(result).to.equal('/path/to/bin');
});
it('should return null if pipx environment path is not found', () => {
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
if (command === 'python3 -m pipx environment') {
return 'No pipx environment found';
}
return '';
});
const result = commandsBase.getPipxEnvironmentPath();
expect(result).to.be.null;
});
});
});

View File

@ -0,0 +1,309 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { Context } from 'mocha';
import sinon from 'sinon';
import { parseMessage, getInstructionFiles, parseMessageAndSetOptions, getParentHash, handleTopic, handlerResponseText, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase';
import DevChat, { ChatResponse } from '../../src/toolwrapper/devchat';
import CommandManager from '../../src/command/commandManager';
import messageHistory from '../../src/util/messageHistory';
import { TopicManager } from '../../src/topic/topicManager';
import CustomCommands from '../../src/command/customCommand';
import { UiUtilWrapper } from '../../src/util/uiUtil';
describe('sendMessageBase', () => {
let workspaceFoldersFirstPathStub: sinon.SinonStub;
let getConfigurationStub: sinon.SinonStub;
beforeEach(() => {
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath');
getConfigurationStub = sinon.stub(UiUtilWrapper, 'getConfiguration');
});
afterEach(() => {
workspaceFoldersFirstPathStub.restore();
getConfigurationStub.restore();
});
describe('parseMessage', () => {
it('should parse message correctly', () => {
const message = '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!';
const result = parseMessage(message);
expect(result.context).to.deep.equal(['path/to/context']);
expect(result.instruction).to.deep.equal(['path/to/instruction']);
expect(result.reference).to.deep.equal(['path/to/reference']);
expect(result.text).to.equal('Hello, world!');
});
});
describe('getInstructionFiles', () => {
it('should return instruction files', () => {
const result = getInstructionFiles();
expect(result).to.be.an('array');
});
});
describe('parseMessageAndSetOptions', () => {
it('should parse message and set options correctly', async () => {
const message = {
text: '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!'
};
const chatOptions: any = {};
const result = await parseMessageAndSetOptions(message, chatOptions);
expect(result.context).to.deep.equal(['path/to/context']);
expect(result.instruction).to.deep.equal(['path/to/instruction']);
expect(result.reference).to.deep.equal(['path/to/reference']);
expect(result.text).to.equal('Hello, world!');
expect(chatOptions.context).to.deep.equal(['path/to/context']);
expect(chatOptions.header).to.deep.equal(['path/to/instruction']);
expect(chatOptions.reference).to.deep.equal(['path/to/reference']);
});
});
describe('getParentHash', () => {
beforeEach(() => {
messageHistory.clear();
});
it('should return parent hash when message hash is provided and found in history', () => {
const message1 = {
hash: 'somehash1',
parentHash: 'parentHash1'
};
const message2 = {
hash: 'somehash2',
parentHash: 'parentHash2'
};
messageHistory.add(message1);
messageHistory.add(message2);
const message = {
hash: 'somehash1'
};
const result = getParentHash(message);
expect(result).to.equal('parentHash1');
});
it('should return undefined when message hash is provided but not found in history', () => {
const message1 = {
hash: 'somehash1',
parentHash: 'parentHash1'
};
const message2 = {
hash: 'somehash2',
parentHash: 'parentHash2'
};
messageHistory.add(message1);
messageHistory.add(message2);
const message = {
hash: 'nonexistenthash'
};
const result = getParentHash(message);
expect(result).to.be.undefined;
});
it('should return last message hash when message hash is not provided', () => {
const message1 = {
hash: 'somehash1',
parentHash: 'parentHash1'
};
const message2 = {
hash: 'somehash2',
parentHash: 'parentHash2'
};
messageHistory.add(message1);
messageHistory.add(message2);
const message = {};
const result = getParentHash(message);
expect(result).to.equal('somehash2');
});
it('should return undefined when message hash is not provided and history is empty', () => {
const message = {};
const result = getParentHash(message);
expect(result).to.be.undefined;
});
});
describe('handleTopic', () => {
it('should handle topic correctly', async () => {
const parentHash = 'somehash';
const message = {
text: 'Hello, world!'
};
const chatResponse: ChatResponse = {
response: 'Hello, user!',
isError: false,
user: 'user',
date: '2022-01-01T00:00:00.000Z',
'prompt-hash': 'responsehash'
};
await handleTopic(parentHash, message, chatResponse);
// Check if the topic was updated correctly
});
});
describe('handlerResponseText', () => {
it('should handle response text correctly when isError is false', async () => {
const partialDataText = 'Partial data';
const chatResponse: ChatResponse = {
response: 'Hello, user!',
isError: false,
user: 'user',
date: '2022-01-01T00:00:00.000Z',
'prompt-hash': 'responsehash'
};
const result = await handlerResponseText(partialDataText, chatResponse);
expect(result).to.equal('Hello, user!');
});
it('should handle response text correctly when isError is true', async () => {
const partialDataText = 'Partial data';
const chatResponse: ChatResponse = {
response: 'Error occurred!',
isError: true,
user: 'user',
date: '2022-01-01T00:00:00.000Z',
'prompt-hash': 'responsehash'
};
const result = await handlerResponseText(partialDataText, chatResponse);
expect(result).to.equal('Partial data\n\nError occurred!');
});
});
describe('sendMessageBase', async () => {
it('should send message correct with openai api key', async () => {
const message = {
text: 'Hello, world!'
};
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
// Handle partial data
};
workspaceFoldersFirstPathStub.returns('./');
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('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');
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000);
const result = await sendMessageBase(message, handlePartialData);
expect(result).to.be.an('object');
expect(result!.command).to.equal('receiveMessage');
expect(result!.text).to.be.a('string');
expect(result!.hash).to.be.a('string');
expect(result!.user).to.be.a('string');
expect(result!.date).to.be.a('string');
expect(result!.isError).to.be.false;
}).timeout(10000);
it('should send message correct with DevChat access key', async () => {
const message = {
text: 'Hello, world!'
};
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
// Handle partial data
};
workspaceFoldersFirstPathStub.returns('./');
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjY2MTI0NDU1ODE2LCJqdGkiOjcyMzc4ODIzMjI3Mjc4MzI2NTJ9.qGWJ_NyWjjj66oa5mbfi3Hjawe-Yp8syCDLkpyu4yS0');
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000);
const result = await sendMessageBase(message, handlePartialData);
expect(result).to.be.an('object');
expect(result!.command).to.equal('receiveMessage');
expect(result!.text).to.be.a('string');
expect(result!.hash).to.be.a('string');
expect(result!.user).to.be.a('string');
expect(result!.date).to.be.a('string');
expect(result!.isError).to.be.false;
}).timeout(10000);
it('should send message error with invalid api key', async () => {
const message = {
text: 'Hello, world!'
};
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
// Handle partial data
};
workspaceFoldersFirstPathStub.returns('./');
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('sk-KvH7ZCtHmFDCBTqH0jUv');
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns('0');
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns('9000');
const result = await sendMessageBase(message, handlePartialData);
expect(result).to.be.an('object');
expect(result!.command).to.equal('receiveMessage');
expect(result!.text).to.be.a('string');
expect(result!.hash).to.be.a('string');
expect(result!.user).to.be.a('string');
expect(result!.date).to.be.a('string');
expect(result!.isError).to.be.true;
}).timeout(10000);
});
describe('stopDevChatBase', () => {
it('should stop sendMessageBase correctly', async () => {
const message = {
text: 'Hello, world!'
};
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
// Handle partial data
};
workspaceFoldersFirstPathStub.returns('./');
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjY2MTI0NDU1ODE2LCJqdGkiOjcyMzc4ODIzMjI3Mjc4MzI2NTJ9.qGWJ_NyWjjj66oa5mbfi3Hjawe-Yp8syCDLkpyu4yS0');
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000);
// Start sendMessageBase in a separate Promise
const sendMessagePromise = sendMessageBase(message, handlePartialData);
// Wait for a short period to ensure sendMessageBase has started
await new Promise((resolve) => setTimeout(resolve, 100));
// Call stopDevChatBase
const stopMessage = {
text: 'stop'
};
await stopDevChatBase(stopMessage);
// Check if sendMessageBase has been stopped and returns an error
try {
const result = await sendMessagePromise;
expect(result).to.undefined;
} catch (error) {
expect(error).to.be.an('error');
}
});
});
});

View File

@ -0,0 +1,60 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import sinon from 'sinon';
import DevChat, { ChatOptions } from '../../src/toolwrapper/devchat';
import { CommandRun } from '../../src/util/commonUtil';
import { UiUtilWrapper } from '../../src/util/uiUtil';
describe('DevChat', () => {
let devChat: DevChat;
let spawnAsyncStub: sinon.SinonStub;
let workspaceFoldersFirstPathStub: sinon.SinonStub;
beforeEach(() => {
devChat = new DevChat();
spawnAsyncStub = sinon.stub(CommandRun.prototype, 'spawnAsync');
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath');
});
afterEach(() => {
spawnAsyncStub.restore();
workspaceFoldersFirstPathStub.restore();
});
describe('chat', () => {
it('should return a ChatResponse object with isError false when the chat is successful', async () => {
const content = 'Test chat content';
const options: ChatOptions = {
// Provide mock values for the options
parent: 'parent_value',
reference: ['ref1', 'ref2'],
header: ['header1', 'header2'],
context: ['context1', 'context2'],
};
const mockResponse = {
exitCode: 0,
stdout: 'User: Test user\nDate: 2022-01-01\nprompt-hash: 12345\nTest chat response',
stderr: '',
};
const mockWorkspacePath = './';
spawnAsyncStub.resolves(mockResponse);
workspaceFoldersFirstPathStub.returns(mockWorkspacePath);
const response = await devChat.chat(content, options, (data)=>{});
expect(response).to.have.property('prompt-hash', '12345');
expect(response).to.have.property('user', 'Test user');
expect(response).to.have.property('date', '2022-01-01');
expect(response).to.have.property('response', 'Test chat response');
expect(response).to.have.property('isError', false);
expect(spawnAsyncStub.calledOnce).to.be.true;
expect(workspaceFoldersFirstPathStub.calledOnce).to.be.true;
});
// Add more test cases for the chat method here
});
// ... other test cases
});

View File

@ -0,0 +1,64 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import sinon from 'sinon';
import DtmWrapper from '../../src/toolwrapper/dtm';
describe('DtmWrapper', () => {
let dtmWrapper: DtmWrapper;
let commitStub: sinon.SinonStub;
let commitAllStub: sinon.SinonStub;
beforeEach(() => {
dtmWrapper = new DtmWrapper();
commitStub = sinon.stub(dtmWrapper, 'commit');
commitAllStub = sinon.stub(dtmWrapper, 'commitall');
});
afterEach(() => {
commitStub.restore();
commitAllStub.restore();
});
describe('commit', () => {
it('should return a DtmResponse object with status 0 when the commit is successful', async () => {
const commitMsg = 'Test commit message';
const mockResponse = {
status: 0,
message: 'Commit successful',
log: 'Commit log',
};
commitStub.resolves(mockResponse);
const response = await dtmWrapper.commit(commitMsg);
expect(response).to.have.property('status', 0);
expect(response).to.have.property('message');
expect(response).to.have.property('log');
expect(commitStub.calledOnce).to.be.true;
});
// Add more test cases for the commit method here
});
describe('commitall', () => {
it('should return a DtmResponse object with status 0 when the commit is successful', async () => {
const commitMsg = 'Test commit message';
const mockResponse = {
status: 0,
message: 'Commit all successful',
log: 'Commit all log',
};
commitAllStub.resolves(mockResponse);
const response = await dtmWrapper.commitall(commitMsg);
expect(response).to.have.property('status', 0);
expect(response).to.have.property('message');
expect(response).to.have.property('log');
expect(commitAllStub.calledOnce).to.be.true;
});
});
});

View File

@ -0,0 +1,49 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { LogEntry } from '../../src/toolwrapper/devchat';
import { loadTopicList } from '../../src/topic/loadTopics';
describe('loadTopicList', () => {
it('should create topic lists from chat logs', () => {
const chatLogs: LogEntry[] = [
{ hash: '1', parent: '', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []},
{ hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []},
{ hash: '3', parent: '2', user: 'user3', date: '2022-01-03', request: 'request3', response: 'response3', context: []},
{ hash: '4', parent: '', user: 'user4', date: '2022-01-04', request: 'request4', response: 'response4', context: []},
{ hash: '5', parent: '4', user: 'user5', date: '2022-01-05', request: 'request5', response: 'response5', context: []},
];
const expectedTopicLists = {
'1': [
{ hash: '1', parent: '', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []},
{ hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []},
{ hash: '3', parent: '2', user: 'user3', date: '2022-01-03', request: 'request3', response: 'response3', context: []},
],
'4': [
{ hash: '4', parent: '', user: 'user4', date: '2022-01-04', request: 'request4', response: 'response4', context: []},
{ hash: '5', parent: '4', user: 'user5', date: '2022-01-05', request: 'request5', response: 'response5', context: []},
],
};
const topicLists = loadTopicList(chatLogs);
expect(topicLists).to.deep.equal(expectedTopicLists);
});
it('should handle empty chat logs', () => {
const chatLogs: LogEntry[] = [];
const expectedTopicLists = {};
const topicLists = loadTopicList(chatLogs);
expect(topicLists).to.deep.equal(expectedTopicLists);
});
it('should handle chat logs with no root entries', () => {
const chatLogs: LogEntry[] = [
{ hash: '1', parent: '0', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []},
{ hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []},
];
const expectedTopicLists = {};
const topicLists = loadTopicList(chatLogs);
expect(topicLists).to.deep.equal(expectedTopicLists);
});
});

View File

@ -0,0 +1,97 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { TopicManager } from '../../src/topic/topicManager';
describe('TopicManager', () => {
let topicManager: TopicManager;
beforeEach(() => {
topicManager = TopicManager.getInstance();
});
afterEach(() => {
// Reset topics and currentTopicId after each test
topicManager['_topics'] = {};
topicManager.currentTopicId = undefined;
});
it('getInstance should return a singleton instance', () => {
const instance1 = TopicManager.getInstance();
const instance2 = TopicManager.getInstance();
expect(instance1).to.equal(instance2);
});
it('createTopic should create a new topic', () => {
const topic = topicManager.createTopic();
expect(topic).to.be.not.undefined;
expect(topic.topicId).to.be.not.undefined;
expect(topicManager.getTopic(topic.topicId)).to.equal(topic);
});
it('getTopicList should return a list of topics', () => {
const topic1 = topicManager.createTopic();
const topic2 = topicManager.createTopic();
const topicList = topicManager.getTopicList();
expect(topicList).to.include(topic1);
expect(topicList).to.include(topic2);
});
it('setCurrentTopic should set the current topic ID', () => {
const topic = topicManager.createTopic();
topicManager.setCurrentTopic(topic.topicId);
expect(topicManager.currentTopicId).to.equal(topic.topicId);
});
it('updateTopic should update the topic with the given properties', () => {
const topic = topicManager.createTopic();
const newMessageHash = 'new-message-hash';
const messageDate = Date.now();
const requestMessage = 'Request message';
const responseMessage = 'Response message';
topicManager.updateTopic(topic.topicId, newMessageHash, messageDate, requestMessage, responseMessage);
const updatedTopic = topicManager.getTopic(topic.topicId);
expect(updatedTopic!.name).to.equal(`${requestMessage} - ${responseMessage}`);
expect(updatedTopic!.firstMessageHash).to.equal(newMessageHash);
expect(updatedTopic!.lastMessageHash).to.equal(newMessageHash);
expect(updatedTopic!.lastUpdated).to.equal(messageDate);
});
it('updateTopic should not update the topic if the topic does not exist', () => {
const nonExistentTopicId = 'non-existent-topic-id';
const newMessageHash = 'new-message-hash';
const messageDate = Date.now();
const requestMessage = 'Request message';
const responseMessage = 'Response message';
topicManager.updateTopic(nonExistentTopicId, newMessageHash, messageDate, requestMessage, responseMessage);
const nonExistentTopic = topicManager.getTopic(nonExistentTopicId);
expect(nonExistentTopic).to.be.undefined;
});
it('deleteTopic should delete the topic with the given ID', () => {
const topic = topicManager.createTopic();
topicManager.deleteTopic(topic.topicId);
expect(topicManager.getTopic(topic.topicId)).to.be.undefined;
});
it('deleteTopic should not throw an error if the topic does not exist', () => {
const nonExistentTopicId = 'non-existent-topic-id';
expect(() => {
topicManager.deleteTopic(nonExistentTopicId);
}).to.not.throw();
});
it('deleteTopic should set the currentTopicId to undefined if the deleted topic was the current topic', () => {
const topic = topicManager.createTopic();
topicManager.setCurrentTopic(topic.topicId);
topicManager.deleteTopic(topic.topicId);
expect(topicManager.currentTopicId).to.be.undefined;
});
// Add more test cases for other methods in TopicManager
});

91
test/util/apiKey.test.ts Normal file
View File

@ -0,0 +1,91 @@
// test/apiKey.test.ts
import { expect } from 'chai';
import { ApiKeyManager } from '../../src/util/apiKey';
import { UiUtilWrapper } from '../../src/util/uiUtil';
import sinon from 'sinon';
describe('ApiKeyManager', () => {
afterEach(() => {
sinon.restore();
delete process.env.OPENAI_API_KEY;
delete process.env.OPENAI_API_BASE;
});
describe('getApiKey', () => {
it('should return the secret storage API key', async () => {
sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves('sk.secret');
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
const apiKey = await ApiKeyManager.getApiKey();
expect(apiKey).to.equal('sk.secret');
});
it('should return the configuration API key', async () => {
sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves(undefined);
sinon.stub(UiUtilWrapper, 'getConfiguration').returns('sk.config');
const apiKey = await ApiKeyManager.getApiKey();
expect(apiKey).to.equal('sk.config');
});
it('should return the environment variable API key', async () => {
sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves(undefined);
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
process.env.OPENAI_API_KEY = 'sk.env';
const apiKey = await ApiKeyManager.getApiKey();
expect(apiKey).to.equal('sk.env');
});
});
describe('getEndPoint', () => {
it('should return the configuration endpoint', () => {
sinon.stub(UiUtilWrapper, 'getConfiguration').returns('https://config-endpoint.com');
const endPoint = ApiKeyManager.getEndPoint('sk.key');
expect(endPoint).to.equal('https://config-endpoint.com');
});
it('should return the environment variable endpoint', () => {
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
process.env.OPENAI_API_BASE = 'https://env-endpoint.com';
const endPoint = ApiKeyManager.getEndPoint('sk.key');
expect(endPoint).to.equal('https://env-endpoint.com');
});
it('should return the default endpoint for DC keys', () => {
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
const endPoint = ApiKeyManager.getEndPoint('DC.key');
expect(endPoint).to.equal('https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1');
});
});
describe('getKeyType', () => {
it('should return "sk" for sk keys', () => {
const keyType = ApiKeyManager.getKeyType('sk.key');
expect(keyType).to.equal('sk');
});
it('should return "DC" for DC keys', () => {
const keyType = ApiKeyManager.getKeyType('DC.key');
expect(keyType).to.equal('DC');
});
it('should return undefined for invalid keys', () => {
const keyType = ApiKeyManager.getKeyType('invalid.key');
expect(keyType).to.be.undefined;
});
});
describe('writeApiKeySecret', () => {
it('should store the API key in secret storage', async () => {
const storeSecretStub = sinon.stub(UiUtilWrapper, 'storeSecret').resolves();
await ApiKeyManager.writeApiKeySecret('sk.secret');
expect(storeSecretStub.calledWith('devchat_OPENAI_API_KEY', 'sk.secret')).to.be.true;
});
});
});

View File

@ -0,0 +1,138 @@
// test/commonUtil.test.ts
import { expect } from 'chai';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
createTempSubdirectory,
CommandRun,
runCommandAndWriteOutput,
runCommandStringAndWriteOutput,
getLanguageIdByFileName,
} from '../../src/util/commonUtil';
import { UiUtilWrapper } from '../../src/util/uiUtil';
import sinon from 'sinon';
describe('commonUtil', () => {
afterEach(() => {
sinon.restore();
});
describe('createTempSubdirectory', () => {
it('should create a temporary subdirectory', () => {
const tempDir = os.tmpdir();
const subdir = 'test-subdir';
const targetDir = createTempSubdirectory(subdir);
expect(targetDir.startsWith(path.join(tempDir, subdir))).to.be.true;
expect(fs.existsSync(targetDir)).to.be.true;
fs.rmdirSync(targetDir, { recursive: true });
});
});
describe('CommandRun', () => {
it('should run a command and capture stdout and stderr', async () => {
const command = 'echo';
const args = ['hello', 'world'];
const options = { shell: true };
const run = new CommandRun();
const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, undefined);
expect(result.exitCode).to.equal(0);
expect(result.stdout.trim()).to.equal('hello world');
expect(result.stderr).to.equal('');
});
it('should run a command and write output to a file', async () => {
const command = 'echo';
const args = ['hello', 'world'];
const options = { shell: true };
const outputFile = path.join(os.tmpdir(), 'test-output.txt');
const run = new CommandRun();
const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile);
expect(result.exitCode).to.equal(0);
expect(result.stdout.trim()).to.equal('hello world');
expect(result.stderr).to.equal('');
expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world');
fs.unlinkSync(outputFile);
});
it('should handle command not found error and output the error message', async () => {
const command = 'nonexistent-command';
const args: string[] = [];
const options = { shell: true };
const run = new CommandRun();
const result = await run.spawnAsync(
command,
args,
options,
undefined,
undefined,
undefined,
undefined
);
expect(result.exitCode).to.not.equal(0);
expect(result.stderr).to.include(`${command}: command not found`);
});
});
describe('runCommandAndWriteOutput', () => {
it('should run a command and write output to a file', async () => {
const command = 'echo';
const args: string[] = ['hello', 'world'];
const outputFile = path.join(os.tmpdir(), 'test-output.txt');
await runCommandAndWriteOutput(command, args, outputFile);
expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world');
fs.unlinkSync(outputFile);
});
});
describe('runCommandStringAndWriteOutput', () => {
it('should run a command string and write output to a file', async () => {
const commandString = 'echo hello world';
const outputFile = path.join(os.tmpdir(), 'test-output.txt');
await runCommandStringAndWriteOutput(commandString, outputFile);
const fileContent = fs.readFileSync(outputFile, 'utf-8').trim();
const parsedContent = JSON.parse(fileContent);
expect(parsedContent.command).to.equal(commandString);
expect(parsedContent.content.trim()).to.equal('hello world');
fs.unlinkSync(outputFile);
});
});
describe('getLanguageIdByFileName', () => {
beforeEach(() => {
sinon.stub(UiUtilWrapper, 'languageId').callsFake(async (fileName: string) => {
const languageIds: { [key: string]: string } = {
'test.py': 'python',
'test.js': 'javascript',
'test.ts': 'typescript',
};
return languageIds[fileName];
});
});
afterEach(() => {
sinon.restore();
});
it('should return the correct language ID for a given file name', async () => {
expect(await getLanguageIdByFileName('test.py')).to.equal('python');
expect(await getLanguageIdByFileName('test.js')).to.equal('javascript');
expect(await getLanguageIdByFileName('test.ts')).to.equal('typescript');
expect(await getLanguageIdByFileName('test.unknown')).to.equal(undefined);
});
});
});

View File

@ -0,0 +1,45 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { FilePairManager } from '../../src/util/diffFilePairs';
describe('FilePairManager', () => {
let filePairManager: FilePairManager;
beforeEach(() => {
filePairManager = FilePairManager.getInstance();
});
afterEach(() => {
// Clear the filePairs map after each test
(filePairManager as any).filePairs.clear();
});
it('add file pair', () => {
const file1 = 'file1.txt';
const file2 = 'file2.txt';
filePairManager.addFilePair(file1, file2);
expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]);
expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]);
});
it('find pair', () => {
const file1 = 'file1.txt';
const file2 = 'file2.txt';
const file3 = 'file3.txt';
const file4 = 'file4.txt';
filePairManager.addFilePair(file1, file2);
filePairManager.addFilePair(file3, file4);
expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]);
expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]);
expect(filePairManager.findPair(file3)).to.deep.equal([file3, file4]);
expect(filePairManager.findPair(file4)).to.deep.equal([file3, file4]);
});
it('find non-existent pair', () => {
const file1 = 'file1.txt';
const file2 = 'file2.txt';
const file3 = 'file3.txt';
filePairManager.addFilePair(file1, file2);
expect(filePairManager.findPair(file3)).to.be.undefined;
});
});

64
test/util/logger.test.ts Normal file
View File

@ -0,0 +1,64 @@
// test/util/logger.test.ts
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { logger, LogChannel } from '../../src/util/logger';
class MockLogChannel implements LogChannel {
logs: string[] = [];
info(message: string, ...args: any[]): void {
this.logs.push(`[INFO] ${message} ${args.join(' ')}`);
}
warn(message: string, ...args: any[]): void {
this.logs.push(`[WARN] ${message} ${args.join(' ')}`);
}
error(message: string | Error, ...args: any[]): void {
this.logs.push(`[ERROR] ${message} ${args.join(' ')}`);
}
debug(message: string, ...args: any[]): void {
this.logs.push(`[DEBUG] ${message} ${args.join(' ')}`);
}
show(): void {
// Do nothing
}
}
describe('logger', () => {
it('should initialize the logger and create a channel', () => {
// Arrange
const mockChannel = new MockLogChannel();
// Act
logger.init(mockChannel);
// Assert
const channel = logger.channel();
expect(channel).to.not.be.undefined;
expect(channel).to.equal(mockChannel);
});
it('should log messages using the initialized channel', () => {
// Arrange
const mockChannel = new MockLogChannel();
logger.init(mockChannel);
// Act
logger.channel()?.info('Test info message');
logger.channel()?.warn('Test warn message');
logger.channel()?.error('Test error message');
logger.channel()?.debug('Test debug message');
// Assert
expect(mockChannel.logs).to.deep.equal([
'[INFO] Test info message ',
'[WARN] Test warn message ',
'[ERROR] Test error message ',
'[DEBUG] Test debug message ',
]);
});
});

View File

@ -0,0 +1,45 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { MessageHistory } from '../../src/util/messageHistory';
describe('MessageHistory', () => {
let messageHistory: MessageHistory;
beforeEach(() => {
messageHistory = new MessageHistory();
});
it('add message', () => {
const message = { hash: '123', content: 'Hello' };
messageHistory.add(message);
expect(messageHistory.find('123')).to.deep.equal(message);
});
it('find message by hash', () => {
const message1 = { hash: '123', content: 'Hello' };
const message2 = { hash: '456', content: 'World' };
messageHistory.add(message1);
messageHistory.add(message2);
expect(messageHistory.find('123')).to.deep.equal(message1);
expect(messageHistory.find('456')).to.deep.equal(message2);
});
it('find last message', () => {
const message1 = { hash: '123', content: 'Hello' };
const message2 = { hash: '456', content: 'World' };
messageHistory.add(message1);
messageHistory.add(message2);
expect(messageHistory.findLast()).to.deep.equal(message2);
});
it('clear history', () => {
const message1 = { hash: '123', content: 'Hello' };
const message2 = { hash: '456', content: 'World' };
messageHistory.add(message1);
messageHistory.add(message2);
messageHistory.clear();
expect(messageHistory.find('123')).to.be.undefined;
expect(messageHistory.find('456')).to.be.undefined;
expect(messageHistory.findLast()).to.be.null;
});
});

13
test/util/utils.test.ts Normal file
View File

@ -0,0 +1,13 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
describe('yourFunction', () => {
it('should return the correct result for input 1', () => {
const input = 1;
const expectedResult = 'expectedResult';
const result = 'expectedResult';
expect(result).to.equal(expectedResult);
});
// Add more test cases here
});

View File

@ -13,5 +13,6 @@
"strict": true,
"jsx": "react",
"esModuleInterop": true
}
},
"exclude": ["test"]
}

9
tsconfig.test.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "out-test",
"skipLibCheck": true
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}