Browse Source

初始化

master
review512jwy@163.com 3 months ago
commit
99b037eb28
  1. 5
      .gitignore
  2. 5
      .vscode-test.mjs
  3. 5
      .vscode/extensions.json
  4. 21
      .vscode/launch.json
  5. 13
      .vscode/settings.json
  6. 40
      .vscode/tasks.json
  7. 14
      .vscodeignore
  8. 9
      CHANGELOG.md
  9. 18
      README.md
  10. 34
      assets/icon3.svg
  11. 28
      eslint.config.mjs
  12. 4799
      package-lock.json
  13. 80
      package.json
  14. 199
      src/codeLensProvider.ts
  15. 21
      src/constants.ts
  16. 664
      src/extension.ts
  17. 15
      src/test/extension.test.ts
  18. 12
      tsconfig.json
  19. 48
      vsc-extension-quickstart.md
  20. 48
      webpack.config.js

5
.gitignore

@ -0,0 +1,5 @@
out
dist
node_modules
.vscode-test/
*.vsix

5
.vscode-test.mjs

@ -0,0 +1,5 @@
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'out/test/**/*.test.js',
});

5
.vscode/extensions.json

@ -0,0 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "ms-vscode.extension-test-runner"]
}

21
.vscode/launch.json

@ -0,0 +1,21 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

13
.vscode/settings.json

@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

40
.vscode/tasks.json

@ -0,0 +1,40 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$ts-webpack-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": "build"
},
{
"label": "tasks: watch-tests",
"dependsOn": [
"npm: watch",
"npm: watch-tests"
],
"problemMatcher": []
}
]
}

14
.vscodeignore

@ -0,0 +1,14 @@
.vscode/**
.vscode-test/**
out/**
node_modules/**
src/**
.gitignore
.yarnrc
webpack.config.js
vsc-extension-quickstart.md
**/tsconfig.json
**/eslint.config.mjs
**/*.map
**/*.ts
**/.vscode-test.*

9
CHANGELOG.md

@ -0,0 +1,9 @@
# Change Log
All notable changes to the "vscode-plugin-demo" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

18
README.md

@ -0,0 +1,18 @@
# Code Helper Extension
## 介绍
Code Helper Extension は、VS Code 用の拡張機能で、コードレビュー、コードのチューニングなどの機能を提供します。
## 機能
- コードの解釈
- 関数の注釈
- コードの最適化
## インストール
1. .vsix ファイルをダウンロード
2. VS Code で Extensions(拡張機能) ページを開く
3. Install from VSIX... を選択してインストール
## 使用方法
1. VS Code で任意のコードファイルを開く
2. Ctrl + Shift + P を押し、Code Helper を入力して機能を選択

34
assets/icon3.svg

@ -0,0 +1,34 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="360.000000pt" height="360.000000pt" viewBox="0 0 360.000000 360.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,360.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M249 3430 c-93 -16 -174 -77 -220 -168 l-24 -47 0 -1415 0 -1415 24
-47 c33 -65 86 -117 151 -147 l55 -26 1565 0 1565 0 55 26 c65 30 118 82 151
147 l24 47 0 1415 0 1415 -24 47 c-33 65 -86 117 -151 147 l-55 26 -1535 1
c-844 1 -1556 -2 -1581 -6z m252 -300 c10 -6 26 -25 35 -42 22 -43 11 -90 -28
-123 -27 -23 -39 -26 -72 -22 -56 8 -86 44 -86 102 0 36 5 47 31 69 33 28 85
34 120 16z m380 -19 c46 -47 35 -127 -22 -157 -36 -18 -63 -18 -99 1 -91 47
-53 185 50 185 33 0 48 -6 71 -29z m360 0 c56 -56 29 -153 -47 -167 -89 -17
-151 66 -109 146 19 37 41 49 88 50 29 0 45 -7 68 -29z m2059 -1516 l0 -1135
-1500 0 -1500 0 0 1135 0 1135 1500 0 1500 0 0 -1135z"/>
<path d="M1895 2212 c-16 -11 -35 -25 -42 -33 -10 -13 -94 -245 -270 -754 -24
-71 -59 -173 -78 -225 -46 -133 -48 -145 -29 -186 22 -46 60 -64 131 -64 65 0
116 22 145 61 9 13 101 266 204 562 204 589 202 582 143 631 -27 23 -40 26
-102 26 -52 0 -80 -5 -102 -18z"/>
<path d="M1045 2184 c-29 -16 -379 -374 -477 -490 -41 -47 -48 -61 -48 -97 1
-23 6 -52 13 -64 7 -11 91 -104 187 -205 343 -360 311 -330 366 -335 58 -6 90
14 135 86 32 50 37 97 15 139 -8 15 -86 104 -175 197 -150 158 -173 188 -153
197 4 2 80 80 169 175 169 179 185 205 169 269 -11 41 -63 112 -96 129 -37 19
-67 19 -105 -1z"/>
<path d="M2455 2188 c-41 -23 -89 -93 -99 -144 -11 -51 17 -100 121 -208 54
-56 128 -133 165 -171 l68 -71 -118 -124 c-210 -223 -223 -237 -232 -271 -12
-39 -5 -71 26 -126 35 -60 68 -83 122 -83 45 0 48 2 142 98 276 279 421 438
430 471 18 67 7 84 -199 299 -108 112 -218 228 -245 258 -70 79 -128 102 -181
72z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

28
eslint.config.mjs

@ -0,0 +1,28 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
export default [{
files: ["**/*.ts"],
}, {
plugins: {
"@typescript-eslint": typescriptEslint,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: "module",
},
rules: {
"@typescript-eslint/naming-convention": ["warn", {
selector: "import",
format: ["camelCase", "PascalCase"],
}],
curly: "warn",
eqeqeq: "warn",
"no-throw-literal": "warn",
semi: "warn",
},
}];

4799
package-lock.json

File diff suppressed because it is too large

80
package.json

@ -0,0 +1,80 @@
{
"name": "cloudscale-code-helper-extension",
"displayName": "Mini Code Helper",
"publisher": "Mini-Solution",
"description": "コード上でコードの解釈、関数の注釈、最適化提案を提供",
"version": "1.0.0",
"engines": {
"vscode": "^1.97.0"
},
"categories": [
"Programming Languages"
],
"activationEvents": [
"*"
],
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "code-helper-sidebar",
"title": "Code Helper",
"icon": "./assets/icon3.svg"
}
]
},
"views": {
"code-helper-sidebar": [
{
"id": "cloudscale-codeHelperView",
"name": "Code Helper",
"type": "webview"
}
]
},
"commands": [
{
"command": "extension.showExplanation",
"title": "Cloudscale Explanation"
},
{
"command": "extension.showComments",
"title": "Cloudscale Comments"
},
{
"command": "extension.showOptimization",
"title": "Cloudscale Optimization"
}
]
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test"
},
"devDependencies": {
"@types/eventsource": "^3.0.0",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/vscode": "^1.97.0",
"@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"eslint": "^9.19.0",
"ts-loader": "^9.5.2",
"typescript": "^5.7.3",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"marked": "^15.0.7"
}
}

199
src/codeLensProvider.ts

@ -0,0 +1,199 @@
import * as vscode from 'vscode';
import * as ts from 'typescript';
import { CONSTANTS } from './constants';
export class CodeLensProvider implements vscode.CodeLensProvider {
public async provideCodeLenses(document: vscode.TextDocument, _token: vscode.CancellationToken): Promise<vscode.CodeLens[]> {
let functionRanges: { range: vscode.Range, code: string }[] = [];
// 1. 使用 VS Code 内置的 DocumentSymbolProvider 获取符号
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider', document.uri
);
if (symbols && symbols.length > 0) {
const flattenedSymbols = this.flattenSymbols(symbols);
const providerFunctions = flattenedSymbols.filter(symbol =>
symbol.kind === vscode.SymbolKind.Function || symbol.kind === vscode.SymbolKind.Method
);
providerFunctions.forEach(symbol => {
const code = document.getText(symbol.range);
functionRanges.push({ range: symbol.range, code });
});
}
// 2. 针对 JavaScript/TypeScript,额外尝试 AST 解析
if (document.languageId === 'javascript' || document.languageId === 'typescript') {
const astFunctions = this.extractFunctionsFromAST(document);
astFunctions.forEach(astFunc => {
if (!this.isOverlapped(astFunc.range, functionRanges)) {
functionRanges.push(astFunc);
}
});
}
// 3. 如果上述方法未获取到函数(或某些语言符号解析失败),使用正则表达式进行兜底匹配
if (functionRanges.length === 0) {
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const lineText = line.text.trim();
if (this.isFunctionDefinition(lineText)) {
const range = new vscode.Range(i, 0, i, line.text.length);
const functionCode = this.extractFunctionCode(document, i);
functionRanges.push({ range, code: functionCode });
}
}
}
// 根据每个函数范围创建 CodeLens(每个函数显示三个操作)
const codeLenses: vscode.CodeLens[] = [];
functionRanges.forEach(item => {
codeLenses.push(new vscode.CodeLens(item.range, {
title: CONSTANTS.CODE_INTERPRETATION,
command: "extension.showExplanation",
arguments: [item.code]
}));
codeLenses.push(new vscode.CodeLens(item.range, {
title: CONSTANTS.FUNCTION_ANNOTATION,
command: "extension.showComments",
arguments: [item.code]
}));
codeLenses.push(new vscode.CodeLens(item.range, {
title: CONSTANTS.TUNING_RECOMMENDATION,
command: "extension.showOptimization",
arguments: [item.code]
}));
});
return codeLenses;
}
// 递归扁平化 DocumentSymbol 树
private flattenSymbols(symbols: vscode.DocumentSymbol[]): vscode.DocumentSymbol[] {
let result: vscode.DocumentSymbol[] = [];
for (const symbol of symbols) {
result.push(symbol);
if (symbol.children && symbol.children.length > 0) {
result = result.concat(this.flattenSymbols(symbol.children));
}
}
return result;
}
// 判断一个范围是否与已有的范围有重叠(简单根据起始和结束行判断)
private isOverlapped(range: vscode.Range, ranges: { range: vscode.Range, code: string }[]): boolean {
return ranges.some(item =>
item.range.start.line === range.start.line && item.range.end.line === range.end.line
);
}
// 针对 JavaScript/TypeScript,使用 TypeScript AST 解析函数、方法、箭头函数等
private extractFunctionsFromAST(document: vscode.TextDocument): { range: vscode.Range, code: string }[] {
const results: { range: vscode.Range, code: string }[] = [];
const sourceCode = document.getText();
const sourceFile = ts.createSourceFile(document.fileName, sourceCode, ts.ScriptTarget.Latest, true);
const visit = (node: ts.Node) => {
// 检查函数声明、方法声明、箭头函数等
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) ||
ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
const start = document.positionAt(node.getStart());
const end = document.positionAt(node.getEnd());
const range = new vscode.Range(start, end);
const code = document.getText(range);
// 确保我们抓取的是完整的函数定义,而不仅仅是函数体
results.push({ range, code });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return results;
}
// 兜底正则匹配函数定义,支持多种语言(包括 Java 的泛型、数组等)
private isFunctionDefinition(lineText: string): boolean {
// Python
if (lineText.startsWith("def ")) return true;
// JavaScript/TypeScript的函数定义(包括箭头函数和方法)
if (lineText.match(/^\s*(async\s+)?function\s+\w+\s*\(.*\)\s*\{?/)) return true;
if (lineText.match(/^\s*(const|let|var)\s+\w+\s*=\s*\(?\s*\w*\s*\)?\s*=>\s*\{?/)) return true;
if (lineText.match(/^\s*\w+\s*\(.*\)\s*\{?$/)) return true;
// Java:支持 public/private/protected/static, 泛型、数组等
if (lineText.match(/^\s*(public|private|protected|static|final|synchronized|abstract|native|strictfp)(\s+\S+)*\s+(\w+)\s*\(/m)) return true;
// C#、C++:可能带返回类型
if (lineText.match(/^\s*(public|private|protected|static|virtual|override|\w+)\s+\w+\s*\(.*\)\s*\{?$/)) return true;
// PHP
if (lineText.match(/^\s*function\s+\w+\s*\(.*\)\s*\{?$/)) return true;
// Swift
if (lineText.match(/^\s*func\s+\w+\s*\(.*\)\s*\{?$/)) return true;
// Go
if (lineText.match(/^\s*func\s+\w+\s*\(.*\)\s*\{?$/)) return true;
return false;
}
// 简单基于缩进提取函数体(正则兜底方案)
// 针对所有语言的提取函数,根据语言类型选择合适的提取策略
private extractFunctionCode(document: vscode.TextDocument, startLine: number): string {
if (document.languageId === 'python') {
return this.extractPythonFunctionCode(document, startLine);
} else {
// 针对大括号语言(如 Java、JavaScript 等)的提取逻辑
let code = '';
let openBraces = 0;
let started = false;
for (let i = startLine; i < document.lineCount; i++) {
const line = document.lineAt(i).text;
// 如果还没开始并且当前行存在大括号,则标记开始
if (!started && line.indexOf('{') !== -1) {
started = true;
}
// 累加当前行代码
code += line + "\n";
if (started) {
// 统计当前行中 { 和 } 的数量
const opens = (line.match(/{/g) || []).length;
const closes = (line.match(/}/g) || []).length;
openBraces += opens - closes;
// 当大括号全部匹配,则认为函数体结束
if (openBraces <= 0) {
break;
}
} else {
// 如果函数体还未开始(即没有 {),则继续读取下一行
continue;
}
}
return code;
}
}
// 专门针对 Python 的函数体提取(基于缩进判断)
private extractPythonFunctionCode(document: vscode.TextDocument, startLine: number): string {
let code = '';
// 获取定义行的缩进(空格数)
const defIndent = document.lineAt(startLine).firstNonWhitespaceCharacterIndex;
for (let i = startLine; i < document.lineCount; i++) {
const line = document.lineAt(i).text;
// 如果不是空行,且缩进小于等于定义行缩进,则认为函数体结束
if (i > startLine && line.trim() !== '') {
const currentIndent = document.lineAt(i).firstNonWhitespaceCharacterIndex;
if (currentIndent <= defIndent) break;
}
code += line + "\n";
}
return code;
}
}

21
src/constants.ts

@ -0,0 +1,21 @@
// constants.ts
export const MESSAGES = {
SIDEBAR_OPEN_FAILED: "サイドバーの開放に失敗しました",//"侧边栏打开失败",
PLEASE_OPEN_SIDEBAR: "内容を確認するには、まずサイドバーを開いてください",//"请先打开侧边栏查看内容",
REQUEST_FAILED: "リクエストに失敗しました",//"请求失败",
PARSE_DATA_ERROR: "データの解析中にエラーが発生しました:",//"解析数据时发生错误:",
FAILED_OBTAIN_LOCAL_MODEL: "ローカルモデルの取得に失敗しました",//"获取本地模型失败",
NO_AVAILABLE_MODELS: "利用可能なモデルがありません",//"没有可用的模型"
ANSWER_IN_JAPANESE: "【返信】返信は必ず日本語で行ってください。"//"请用日语回答"
};
export const CONSTANTS = {
CODE_INTERPRETATION: "コードレビュー",
FUNCTION_ANNOTATION: "関数注釈",
TUNING_RECOMMENDATION: "チューニング推奨"
};
export const MODELLIST = {
DIFY: "dify",
OLLAMA: "ollama"
};

664
src/extension.ts

@ -0,0 +1,664 @@
import * as vscode from 'vscode';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import { CodeLensProvider } from './codeLensProvider';
import { CONSTANTS, MESSAGES, MODELLIST} from './constants'; // 导入
const modelType = 'ollama';
// 新增:侧边栏内容管理器
class SidebarContentProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'cloudscale-codeHelperView';
public _view?: vscode.WebviewView;
private _currentContent: { [key: string]: string } = {};
private _contentBackup: Map<string, string> = new Map(); // 用于保存内容
private _isInitialized = false;
public _selectedModel = "";
// 添加视图准备状态跟踪
private _viewReadyResolver!: (value: vscode.WebviewView) => void;
private readonly _viewReady = new Promise<vscode.WebviewView>(resolve => {
this._viewReadyResolver = resolve;
});
constructor(private readonly _extensionUri: vscode.Uri) {}
// 获取本地模型列表
private async fetchModels(): Promise<{ name: string }[]> {
try {
const response = await fetch('http://localhost:11434/api/tags');
if (!response.ok) {
throw new Error(`HTTP Error! Code: ${response.status}`);
}
const dataResp = await response.json() as { models?: { name: string }[] };
return dataResp.models || [];
} catch (error) {
console.error(MESSAGES.FAILED_OBTAIN_LOCAL_MODEL, error);
vscode.window.showInformationMessage(MESSAGES.FAILED_OBTAIN_LOCAL_MODEL);
return [];
}
}
// 发送模型列表到 Webview
public async sendModelsToWebview(): Promise<void> {
if (!this._view) return;
try {
const models = await this.fetchModels();
//这里初始化_selectedModel为默认值,因为_getHtml那里的下拉选择器来不及传默认值给_selectedModel
if (this.strIsEmpty(this._selectedModel)) {
if(models.length){
this._selectedModel = models[0].name;
}
}
this._view.webview.postMessage({
type: 'updateModels',
models: models,
selectedModel: this._selectedModel
});
} catch (error) {
console.error(MESSAGES.FAILED_OBTAIN_LOCAL_MODEL + ':', error);
vscode.window.showInformationMessage(MESSAGES.FAILED_OBTAIN_LOCAL_MODEL);
this._view.webview.postMessage({
type: 'updateModels',
models: []
});
}
}
// 修改后的 resolveWebviewView 方法
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
this._isInitialized = true;
this._viewReadyResolver(webviewView);
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtml();
// 监听 Webview
webviewView.onDidChangeVisibility(() => {
if (webviewView.visible) {
this._restoreContent();
this.sendModelsToWebview();
if (!this._isInitialized) {
console.log('View became visible and initialized');
this._isInitialized = true;
}
}
});
// 监听 Webview 消息
webviewView.webview.onDidReceiveMessage((message) => {
if (message.type === 'resize') {
this._view?.webview.postMessage({
type: 'resize',
width: message.width
});
} else if (message.type === 'modelChange') {
// 更新 model
this.updateModel(message.model);
}
});
// 这里要刷新下拉列表,以防没有点击指令,那这里就要初始化
if (modelType === MODELLIST.OLLAMA && this.strIsEmpty(this._selectedModel)){
this.sendModelsToWebview();
}
}
private updateModel(model: string) {
// 更新 model
this._selectedModel = model;
}
private _restoreContent() {
// 恢复之前保存的内容
// 将 Map 转换为数组并按照时间进行排序
const sortedEntries = Array.from(this._contentBackup.entries()).sort((a, b) => {
// 提取时间部分(假设时间部分在标题的最后)
const timeRegex = /(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})$/;
const timeA = a[0].match(timeRegex)?.[1];
const timeB = b[0].match(timeRegex)?.[1];
// 如果找不到时间部分,使用默认顺序
if (!timeA || !timeB) return 0;
const dateA = new Date(timeA.replace(/\//g, "-"));
const dateB = new Date(timeB.replace(/\//g, "-"));
// 如果日期解析失败,使用默认顺序
if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) return 0;
// 按时间升序排序,若时间相同则按标题字母顺序排序
return dateA.getTime() - dateB.getTime() || a[0].localeCompare(b[0]);
});
// 按照排序后的顺序恢复内容
sortedEntries.forEach(([title, content]) => {
this._view?.webview.postMessage({
type: 'update',
title: title,
content: content,
isAppend: true
});
});
}
// 新增:确保视图可见方法
public async ensureVisible(): Promise<void> {
try {
// 触发视图显示
await vscode.commands.executeCommand(`${SidebarContentProvider.viewType}.focus`);
// 等待视图准备就绪
await this._viewReady;
} catch (error) {
vscode.window.showErrorMessage(MESSAGES.SIDEBAR_OPEN_FAILED + ': ' + error);
throw error; // 抛出错误,供外部处理
}
}
// 更新内容的方法(保留原有SSE效果)
public updateContent(title: string, content: string, isAppend: boolean = true) {
if(MODELLIST.DIFY === modelType){
this.updateContentForDify(title, content, isAppend);
} else if(MODELLIST.OLLAMA === modelType){
this.updateContentForOllama(title, content, isAppend);
}
}
public updateContentForOllama(title: string, content: string, isAppend: boolean = true) {
if (!this._view) {
vscode.window.showInformationMessage(MESSAGES.PLEASE_OPEN_SIDEBAR);
return;
}
if (!isAppend) {
this._currentContent[title] = '';
}
const parsedObject = JSON.parse(content); // 解析 JSON
if ('message' in parsedObject) {
const message = parsedObject.message;
if ('content' in message) {
this._currentContent[title] = message.content;
// 将当前内容备份
if (!this._contentBackup.has(title)) {
this._contentBackup.set(title, '');// 使用 set() 添加键值对
}
this._contentBackup.set(title, (this._contentBackup.get(title) ?? '') + this._currentContent[title]); // 使用 get() 获取值,更新并重新设置
this._view.webview.postMessage({
type: 'update',
title: title,
content: this._currentContent[title],
isAppend: isAppend
});
}
}
}
public updateContentForDify(title: string, content: string, isAppend: boolean = true) {
if (!this._view) {
vscode.window.showInformationMessage(MESSAGES.PLEASE_OPEN_SIDEBAR);
return;
}
if (!isAppend) {
this._currentContent[title] = '';
}
//这里可能会出现多条data: 开头数据,所以用正则匹配出每一条,然后循环遍历
const dataRegex = /data:\s*(\{.*\})/g; //确保匹配完整的JSON对象
let match;
while ((match = dataRegex.exec(content)) !== null) {
try {
const jsonString = match[1]; // 提取数据部分
const parsedObject = JSON.parse(jsonString); // 解析 JSON
if ((parsedObject.event === 'message' || parsedObject.event === 'message_end') && 'answer' in parsedObject) {
this._currentContent[title] = parsedObject.answer;
// Promise.resolve(marked(parsedObject.answer)).then((result) => {
// console.log(result);
// });
// 将当前内容备份
if (!this._contentBackup.has(title)) {
this._contentBackup.set(title, '');// 使用 set() 添加键值对
}
this._contentBackup.set(title, (this._contentBackup.get(title) ?? '') + this._currentContent[title]); // 使用 get() 获取值,更新并重新设置
this._view.webview.postMessage({
type: 'update',
title: title,
content: this._currentContent[title],
isAppend: isAppend
});
} else {
// 处理不是以 'data:' 开头的情况
console.info('Content does not start with "data:"', content);
}
} catch (e) {
console.error(MESSAGES.PARSE_DATA_ERROR, e);
}
}
}
public async fetchSSE(type: string, code: string, onData: (data: string) => void) {
let params = { apiUrl: "", requestBody: {}, headers: {} }; // 先定义默认值
switch (modelType) {
case MODELLIST.DIFY:
params = this.createParamsForDify(type, code);
break;
case MODELLIST.OLLAMA:
params = this.createParamsForOllama(type, code);
break;
default:
// 不做任何修改,保持默认值
break;
}
try {
const response = await fetch(params.apiUrl, {
method: 'POST',
headers: params.headers,
body: JSON.stringify(params.requestBody)
});
if (!response.body) {
throw new Error('Response body is empty');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
// SSE结束,通知停止更新
this.stopAppendingContent(type);
break;
}
const text = decoder.decode(value, { stream: true });
onData(text);
}
} catch (error) {
console.error('Fetch Error:', error);
throw error; // 抛出错误,供外部处理
}
}
public strIsEmpty(str: string | null | undefined): boolean {
return !str || str.trim() === "";
}
public createParamsForOllama(type: string, code: string) {
if(this.strIsEmpty(this._selectedModel)){
vscode.window.showInformationMessage(MESSAGES.NO_AVAILABLE_MODELS);
}
const apiUrl = 'http://localhost:11434/api/chat';
const requestBody = {
"model": this._selectedModel,
"messages": [
{ "role": "user", "content": MESSAGES.ANSWER_IN_JAPANESE + "\n" + `${type}: ${code}`}
]
};
const headers = {
'Content-Type': 'application/json'
}
return { apiUrl: apiUrl, requestBody: requestBody, headers: headers};
}
public createParamsForDify(type: string, code: string) {
const apiUrl = 'https://dify.cloudscale.jp/v1/chat-messages';
const apiKey = 'app-hMuLiedinKFqvntOPQOwtZJg'; //API Key
const requestBody = {
query: MESSAGES.ANSWER_IN_JAPANESE + "\n" + `${type}: ${code}`,
response_mode: "streaming",
conversation_id: "",
user: "vscode-test-1",
inputs: {}
};
const headers = {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
return { apiUrl: apiUrl, requestBody: requestBody, headers: headers};
}
// 停止追加内容
public stopAppendingContent(title: string) {
if (!this._view) {
vscode.window.showInformationMessage(MESSAGES.PLEASE_OPEN_SIDEBAR);
return;
}
this._view.webview.postMessage({
type: 'stop',
title: title
});
}
private _getHtml(): string {
const showModelSelector = modelType === MODELLIST.OLLAMA;
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<style>
body {
margin: 0;
padding: 4px; /* 减少内边距 */
color: var(--vscode-editor-foreground);
background: var(--vscode-editor-background);
overflow-x: hidden;
width: 100%;
}
#contentContainer {
width: 100%;
max-width: 100vw; /* 新增 */
}
.content-title {
font-size: 14px;
margin: 12px 0 6px 4px;
color: #F4A460;
}
.content-section {
margin-bottom: 2px; /* 减小段落之间的间距 */
width: 100%;
box-sizing: border-box; /* 新增 */
}
pre {
width: 100%;
max-width: 100%;
overflow-x: auto; /* 避免内容溢出 */
word-wrap: break-word;
white-space: pre-wrap; /* 确保代码换行 */
background: var(--vscode-editorWidget-background);
padding: 10px;
border-radius: 4px;
border: 1px solid var(--vscode-editorWidget-border);
margin: 0;
box-sizing: border-box;
}
code {
font-family: Consolas, Monaco, 'Courier New', monospace;
display: inline !important; /* 保持代码块行内显示 ,如果用block会导致代码单独占一行*/
width: 100%;
max-width: 100%;
white-space: pre-wrap !important; /* 强制换行 */
word-wrap: break-word;
overflow-wrap: break-word;
color: inherit;
}
p {
margin: 0; /* 如果有段落标签,取消默认的上下间距 */
}
ul, ol {
margin: -10px 0 -16px 0 !important; /* 强制缩小列表的上下间距 */
}
li {
margin: -6px 0 !important; /* 强制减少 li 之间的间距 */
}
/* Prism.js 代码块样式修正 */
pre[class*="language-"] {
white-space: pre-wrap !important;
word-break: break-word;
}
/* 下拉选择器样式 */
.model-selector-container {
display: ${showModelSelector ? 'flex' : 'none'}; /* 根据 modelType 决定是否显示 */
align-items: center;
margin-bottom: 12px;
padding: 8px;
background: var(--vscode-editorWidget-background);
border-radius: 4px;
border: 1px solid var(--vscode-editorWidget-border);
}
.model-selector-label {
font-size: 14px;
color: var(--vscode-editor-foreground);
margin-right: 8px;
}
#modelSelector {
flex: 1;
padding: 6px;
font-size: 14px;
color: var(--vscode-editor-foreground);
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-editorWidget-border);
border-radius: 4px;
outline: none;
cursor: pointer;
}
#modelSelector:hover {
border-color: var(--vscode-focusBorder);
}
#modelSelector:focus {
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 2px var(--vscode-focusBorder);
}
</style>
</head>
<body>
${showModelSelector ? `
<div class="model-selector-container">
<span class="model-selector-label">Model: </span>
<select id="modelSelector">
</select>
</div>
` : ''}
<div id="contentContainer"></div>
<script>
const vscode = acquireVsCodeApi();
const container = document.getElementById('contentContainer');
const modelSelector = document.getElementById('modelSelector');
// 监听下拉选择列表的变化
if (modelSelector) {
modelSelector.addEventListener('change', (event) => {
const selectedModel = event.target.value;
vscode.postMessage({
type: 'modelChange',
model: selectedModel
});
});
}
window.addEventListener('message', event => {
const { type, title, content, isAppend, models, selectedModel} = event.data;
if (type === 'update') {
let section = document.getElementById(title);
if (!section) {
section = document.createElement('div');
section.className = 'content-section';
section.id = title;
section.innerHTML = \`
<div class="content-title">\${title}</div>
<pre><code id="\${title}-code" class="language-javascript"></code></pre>
<div style="display: none;"><rawCode id="\${title}-raw-code"></rawCode></div>
\`;
container.appendChild(section);
}
const rawCodeElement = section.querySelector('rawCode');
let rawContent = '';
if (isAppend) {
rawContent = rawCodeElement.textContent + content;
} else {
rawContent = content;
}
//特殊符号需要加个\转义
// rawContent = rawContent.replace(/\\n+/g, '\\n');
rawCodeElement.textContent = rawContent;
// 配置高亮
marked.setOptions({
highlight: function (code, lang) {
const language = Prism.languages[lang] || Prism.languages.javascript; // 默认 JavaScript
return Prism.highlight(code, language, lang || 'javascript');
},
gfm: true,
breaks: true
});
let htmlContent = marked.parse(rawContent);
const finalContent = htmlContent
const showCodeElement = section.querySelector('code');
showCodeElement.innerHTML = finalContent;
// 重新高亮代码
// Prism.highlightAll();
} else if (type === 'updateModels') {
if (modelSelector) {
modelSelector.innerHTML = ''; // 清空加载中的提示
if (models.length > 0) {
models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name;
modelSelector.appendChild(option);
});
// 默认选中第一个模型
modelSelector.value = selectedModel;
// 发送默认选中的模型给扩展
vscode.postMessage({
type: 'modelChange',
model: selectedModel
});
} else {
// 如果没有模型,显示提示
const option = document.createElement('option');
option.value = '';
option.textContent = '';
modelSelector.appendChild(option);
}
}
}
});
</script>
</body>
</html>`;
}
}
// 修改后的 activate 函数
export function activate(context: vscode.ExtensionContext) {
// 注册侧边栏
const sidebarProvider = new SidebarContentProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
SidebarContentProvider.viewType,
sidebarProvider
)
);
// 保留原有 CodeLens 注册
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ scheme: 'file' },
new CodeLensProvider()
)
);
// 通用命令注册函数
const registerCommand = (command: string, title: string) => {
context.subscriptions.push(
vscode.commands.registerCommand(command, async (code) => {
try {
// 自动显示侧边栏,并等待侧边栏完全初始化
await sidebarProvider.ensureVisible();
// 这里要刷新下拉列表,以防没有提前打开工具栏
if (modelType === MODELLIST.OLLAMA && sidebarProvider.strIsEmpty(sidebarProvider._selectedModel)){
await sidebarProvider.sendModelsToWebview();
}
// 加入时间,这样 _getHtml 那里 section,每次都可以初始化新的 div
const now = new Date();
const year = now.getFullYear(); // 获取年份
const month = String(now.getMonth() + 1).padStart(2, '0'); // 获取月份(注意月份从0开始,所以加1)
const day = String(now.getDate()).padStart(2, '0'); // 获取日期
const hours = String(now.getHours()).padStart(2, '0'); // 获取小时
const minutes = String(now.getMinutes()).padStart(2, '0'); // 获取分钟
const seconds = String(now.getSeconds()).padStart(2, '0'); // 获取秒钟
// 格式化输出
const formattedTime = `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
const titleWithTime = title + "&nbsp;&nbsp;&nbsp;" + formattedTime;
// 执行 SSE 请求
await sidebarProvider.fetchSSE(title, code, (data) => {
sidebarProvider.updateContent(titleWithTime, data);
});
} catch (error) {
let errorMessage = MESSAGES.REQUEST_FAILED;
if (error instanceof Error) {
errorMessage += `: ${error.message}`;
} else if (typeof error === 'string') {
errorMessage += `: ${error}`;
} else {
errorMessage += ': Unknown error';
}
vscode.window.showErrorMessage(errorMessage);
}
})
);
};
// 注册原有命令(保留所有功能)
registerCommand('extension.showExplanation', CONSTANTS.CODE_INTERPRETATION);
registerCommand('extension.showComments', CONSTANTS.FUNCTION_ANNOTATION);
registerCommand('extension.showOptimization', CONSTANTS.TUNING_RECOMMENDATION);
}
// 保留 deactivate 函数
export function deactivate() {}

15
src/test/extension.test.ts

@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

12
tsconfig.json

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es6"],
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"exclude": ["node_modules", ".vscode"]
}

48
vsc-extension-quickstart.md

@ -0,0 +1,48 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
## Setup
* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint)
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
## Run tests
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
* See the output of the test result in the Test Results view.
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).

48
webpack.config.js

@ -0,0 +1,48 @@
//@ts-check
'use strict';
const path = require('path');
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const extensionConfig = {
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
},
externals: {
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
// modules added here also need to be added in the .vscodeignore file
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
};
module.exports = [ extensionConfig ];
Loading…
Cancel
Save