Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions extensions/vscode/__mocks__/vscode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
env: { language: 'en' },
Uri: { file: (p) => ({ fsPath: p, scheme: 'file' }), joinPath: (...args) => args.join('/') },
workspace: { workspaceFolders: [{ uri: { fsPath: '/mock' } }], openTextDocument: () => Promise.resolve({ lineCount: 10, lineAt: () => ({ text: '' }) }) },
window: { showErrorMessage: () => {}, showWarningMessage: () => Promise.resolve(undefined), createOutputChannel: () => ({ appendLine: () => {}, dispose: () => {} }), createWebviewPanel: () => ({ webview: { html: '', onDidReceiveMessage: () => ({ dispose: () => {} }), asWebviewUri: (u) => u }, onDidDispose: () => ({ dispose: () => {} }), reveal: () => {}, dispose: () => {} }) },
commands: { registerCommand: () => ({ dispose: () => {} }), executeCommand: () => Promise.resolve() },
extensions: { getExtension: () => null },
Disposable: { from: (...ds) => ({ dispose: () => ds.forEach((d) => d.dispose()) }) },
CommentMode: { Preview: 1 },
CommentThreadCollapsibleState: { Expanded: 0 },
MarkdownString: function (v) { this.value = v; this.isTrusted = false; },
Range: function (a, b, c, d) { this.start = { line: a, character: b }; this.end = { line: c, character: d }; },
WorkspaceEdit: function () { this.replace = () => {}; this.delete = () => {}; },
ThemeIcon: function () {},
ViewColumn: { One: 1 },
EventEmitter: function () { this.event = () => {}; this.fire = () => {}; },
CommentController: function () { this.createCommentThread = () => ({ canReply: false, label: '', collapsibleState: 0, contextValue: '', comments: [], dispose: () => {} }); },
WebviewViewProvider: function () {},
};
Binary file modified extensions/vscode/open-code-review-vscode-0.1.0.vsix
Binary file not shown.
18 changes: 9 additions & 9 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "open-code-review-vscode",
"displayName": "Open Code Review",
"description": "AI 代码审查 —— 基于 open-code-review CLI",
"description": "%ocr.description%",
"version": "0.1.0",
"publisher": "open-code-review",
"license": "Apache-2.0",
Expand All @@ -25,7 +25,7 @@
"activitybar": [
{
"id": "ocr-container",
"title": "Open Code Review",
"title": "%ocr.activitybar.title%",
"icon": "resources/icon.svg"
}
]
Expand All @@ -35,34 +35,34 @@
{
"id": "ocr.sidebar",
"type": "webview",
"name": "Code Review"
"name": "%ocr.sidebar.name%"
}
]
},
"commands": [
{
"command": "ocr.review.start",
"title": "OCR: 开始代码审查"
"title": "%ocr.review.start%"
},
{
"command": "ocr.review.cancel",
"title": "OCR: 取消审查"
"title": "%ocr.review.cancel%"
},
{
"command": "ocr.config.open",
"title": "OCR: 打开配置"
"title": "%ocr.config.open%"
},
{
"command": "ocr.comment.apply",
"title": "应用"
"title": "%ocr.comment.apply%"
},
{
"command": "ocr.comment.discard",
"title": "忽略"
"title": "%ocr.comment.discard%"
},
{
"command": "ocr.comment.falsePositive",
"title": "误报"
"title": "%ocr.comment.falsePositive%"
}
],
"menus": {
Expand Down
11 changes: 11 additions & 0 deletions extensions/vscode/package.nls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"ocr.activitybar.title": "Open Code Review",
"ocr.sidebar.name": "Code Review",
"ocr.review.start": "OCR: Start Code Review",
"ocr.review.cancel": "OCR: Cancel Review",
"ocr.config.open": "OCR: Open Configuration",
"ocr.comment.apply": "Apply",
"ocr.comment.discard": "Discard",
"ocr.comment.falsePositive": "False Positive",
"ocr.description": "AI Code Review — powered by open-code-review CLI"
}
11 changes: 11 additions & 0 deletions extensions/vscode/package.nls.zh-cn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"ocr.activitybar.title": "Open Code Review",
"ocr.sidebar.name": "Code Review",
"ocr.review.start": "OCR: 开始代码审查",
"ocr.review.cancel": "OCR: 取消审查",
"ocr.config.open": "OCR: 打开配置",
"ocr.comment.apply": "应用",
"ocr.comment.discard": "忽略",
"ocr.comment.falsePositive": "误报",
"ocr.description": "AI 代码审查 —— 基于 open-code-review CLI"
}
25 changes: 17 additions & 8 deletions extensions/vscode/src/extension/providers/CommentProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { t, resolveLocale, SupportedLocale } from '../../shared/i18n';
import * as vscode from 'vscode';
import { ReviewComment, CommentStatus, CommentSyncState } from '../../shared/types';
import { COMMENT_CONTROLLER_ID } from '../../shared/constants';
Expand All @@ -13,8 +14,11 @@ export class CommentProvider {
private offsets = new LineOffsetTracker();
private syncListeners: Array<(s: CommentSyncState[]) => void> = [];

private locale: SupportedLocale;

constructor(private extensionUri: vscode.Uri) {
this.controller = vscode.comments.createCommentController(COMMENT_CONTROLLER_ID, 'Open Code Review');
this.locale = resolveLocale(vscode.env.language);
this.controller = vscode.comments.createCommentController(COMMENT_CONTROLLER_ID, t(this.locale, 'ext.commentController'));
}

onSync(fn: (s: CommentSyncState[]) => void): void {
Expand Down Expand Up @@ -58,10 +62,10 @@ export class CommentProvider {
const body = this.renderBody(c, i, 'pending');
const thread = this.controller.createCommentThread(doc.uri, range, [{
body, mode: vscode.CommentMode.Preview,
author: { name: '⏳ [未处理]' },
author: { name: t(this.locale, 'ext.comment.pending') },
}]);
thread.canReply = false;
thread.label = `Code Review (${i + 1} / ${this.comments.length})`;
thread.label = `${t(this.locale, 'ext.comment.threadLabel')} (${i + 1} / ${this.comments.length})`;
// 有代码建议 → 'pending'(显示应用+忽略);无建议 → 'pendingNoSuggestion'(仅忽略)
thread.contextValue = this.hasSuggestion(c) ? 'pending' : 'pendingNoSuggestion';
thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
Expand All @@ -82,7 +86,7 @@ export class CommentProvider {
if (this.hasSuggestion(c)) {
md += `\n***\n\`\`\`diff\n${c.suggestionCode}\n\`\`\``;
} else {
md += `\n***\n_💡 无代码建议,请手动处理_`;
md += `\n***\n${t(this.locale, 'ext.comment.noSuggestion')}`;
}
const s = new vscode.MarkdownString(md);
s.isTrusted = true;
Expand All @@ -100,7 +104,7 @@ export class CommentProvider {
const start = Math.max(0, this.offsets.adjusted(c.path, c.startLine) - 1);
const end = Math.min(doc.lineCount - 1, this.offsets.adjusted(c.path, c.endLine) - 1);
if (end < start) {
vscode.window.showErrorMessage('应用失败:代码位置已失效,请刷新后重试。');
vscode.window.showErrorMessage(t(this.locale, 'ext.comment.applyFailedStale'));
return;
}
const range = new vscode.Range(start, 0, end, doc.lineAt(end).text.length);
Expand All @@ -113,7 +117,7 @@ export class CommentProvider {
else edit.delete(uri, range);
const ok = await vscode.workspace.applyEdit(edit);
if (!ok) {
vscode.window.showErrorMessage('应用失败:无法修改文件,请检查文件是否被占用或处于只读状态。');
vscode.window.showErrorMessage(t(this.locale, 'ext.comment.applyFailedLocked'));
return;
}
await doc.save();
Expand All @@ -129,7 +133,12 @@ export class CommentProvider {
this.status.set(index, status);
const thread = this.threads.get(index);
if (thread) {
const label = { applied: '✅ [已应用]', discarded: '✅ [已忽略]', falsePositive: '✅ [已误报]', pending: '⏳ [未处理]' }[status];
const label = {
applied: t(this.locale, 'ext.comment.statusApplied'),
discarded: t(this.locale, 'ext.comment.statusDiscarded'),
falsePositive: t(this.locale, 'ext.comment.statusFalsePositive'),
pending: t(this.locale, 'ext.comment.pending'),
}[status];
thread.comments = [{ ...thread.comments[0], author: { name: label }, body: this.renderBody(this.comments[index], index, status) }] as any;
thread.contextValue = status;
thread.collapsibleState = vscode.CommentThreadCollapsibleState.Collapsed;
Expand All @@ -141,7 +150,7 @@ export class CommentProvider {
const thread = this.threads.get(index);
if (!thread) {
const c = this.comments[index];
if (c) vscode.window.showWarningMessage(`无法定位到 ${c.path}:该路径不是可打开的文件。`);
if (c) vscode.window.showWarningMessage(`${t(this.locale, 'ext.comment.jumpFailed')}${c.path}${t(this.locale, 'ext.comment.jumpNotAFile')}`);
return;
}
await vscode.window.showTextDocument(thread.uri, { selection: thread.range, preview: false });
Expand Down
16 changes: 11 additions & 5 deletions extensions/vscode/src/extension/providers/ConfigPanelProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveLocale, t, toHtmlLang } from '../../shared/i18n';
import * as vscode from 'vscode';
import { ConfigPanelFocus, isConfigReady } from '../../shared/configUtils';
import { ConfigPanelHostToWebview, WebviewToHost } from '../../shared/messages';
Expand Down Expand Up @@ -30,7 +31,7 @@ export class ConfigPanelProvider implements vscode.Disposable {

this.panel = vscode.window.createWebviewPanel(
PANEL_VIEW_TYPE,
'模型配置',
t(resolveLocale(vscode.env.language), 'ext.configPanelTitle'),
vscode.ViewColumn.One,
{ enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [this.extensionUri] },
);
Expand Down Expand Up @@ -80,12 +81,14 @@ export class ConfigPanelProvider implements vscode.Disposable {
const config = this.config.read();
const cached = this.cli.getCachedEnvironment();
const skipEnvCheck = focus?.step === 2 || isConfigReady(config);
const locale = resolveLocale(vscode.env.language);
this.post({
type: 'configPanelInit',
config,
focus: focus ?? null,
env: cached,
skipEnvCheck,
locale,
});
break;
}
Expand All @@ -106,12 +109,13 @@ export class ConfigPanelProvider implements vscode.Disposable {
break;
}
case 'deleteCustomProvider': {
const locale = resolveLocale(vscode.env.language);
const confirmed = await vscode.window.showWarningMessage(
`确定删除自定义 Provider「${msg.name}」?`,
t(locale, 'ext.deleteProviderConfirm').replace('{name}', msg.name),
{ modal: true },
'删除',
t(locale, 'ext.deleteProviderConfirmBtn'),
);
if (confirmed !== '删除') break;
if (confirmed !== t(locale, 'ext.deleteProviderConfirmBtn')) break;
this.notifyConfig(this.config.deleteCustomProvider(msg.name));
break;
}
Expand Down Expand Up @@ -144,8 +148,10 @@ export class ConfigPanelProvider implements vscode.Disposable {
private html(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'out', 'configPanel.js'));
const nonce = String(Date.now());
const resolved = resolveLocale(vscode.env.language);
const lang = toHtmlLang(resolved);
return `<!DOCTYPE html>
<html lang="zh-CN"><head>
<html lang="${lang}"><head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
</head><body><div id="root"></div>
Expand Down
8 changes: 6 additions & 2 deletions extensions/vscode/src/extension/providers/SidebarProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveLocale, toHtmlLang } from '../../shared/i18n';
import * as vscode from 'vscode';
import { ConfigPanelFocus } from '../../shared/configUtils';
import { HostToWebview, WebviewToHost } from '../../shared/messages';
Expand Down Expand Up @@ -48,7 +49,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
case 'ready': {
const config = this.config.read();
const gitState = await this.git.getState('workspace');
this.post({ type: 'init', config, gitState });
const locale = resolveLocale(vscode.env.language);
this.post({ type: 'init', config, gitState, locale });
break;
}
case 'getGitState': {
Expand Down Expand Up @@ -108,8 +110,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
private html(webview: vscode.Webview): string {
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'out', 'webview.js'));
const nonce = String(Date.now());
const resolved = resolveLocale(vscode.env.language);
const lang = toHtmlLang(resolved);
return `<!DOCTYPE html>
<html lang="zh-CN"><head>
<html lang="${lang}"><head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
</head><body><div id="root"></div>
Expand Down
5 changes: 4 additions & 1 deletion extensions/vscode/src/extension/services/CliService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { t, resolveLocale } from '../../shared/i18n';
import * as vscode from 'vscode';
import { spawn } from 'child_process';
import { CliResult, CliRunOptions, EnvCheckResult, LogLine } from '../../shared/types';
import { buildReviewArgs, extractCliError, parseCliResult, parseLogLine } from './cliParse';
Expand Down Expand Up @@ -90,7 +92,8 @@ export class CliService {
proc.on('error', (err) => { onLog({ text: String(err), level: 'error' }); resolve(false); });
proc.on('close', (code) => {
emitLines('', 'info', true);
onLog({ text: code === 0 ? '✓ 安装完成' : `✗ 安装失败 (exit ${code})`, level: code === 0 ? 'info' : 'error' });
const locale = resolveLocale(vscode.env.language);
onLog({ text: code === 0 ? t(locale, 'ext.cli.installOk') : `${t(locale, 'ext.cli.installFail')}${code})`, level: code === 0 ? 'info' : 'error' });
if (code === 0) this.invalidateEnvironmentCache();
resolve(code === 0);
});
Expand Down
13 changes: 8 additions & 5 deletions extensions/vscode/src/extension/services/GitService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { t, resolveLocale } from '../../shared/i18n';
import * as vscode from 'vscode';
import { execFile } from 'child_process';
import { GitState, CommitInfo, FileChange, ReviewMode } from '../../shared/types';
Expand Down Expand Up @@ -167,7 +168,7 @@ export class GitService {
if (opts.mode === 'workspace') {
left = api.toGitUri(fileUri, opts.status === 'added' ? emptyRef : 'HEAD');
right = opts.status === 'deleted' ? api.toGitUri(fileUri, emptyRef) : fileUri;
label = '工作区 ↔ HEAD';
label = t(resolveLocale(vscode.env.language), 'ext.git.workspaceVsHead');
} else if (opts.mode === 'commit' && opts.commit) {
left = api.toGitUri(fileUri, opts.status === 'added' ? emptyRef : `${opts.commit}^`);
right = opts.status === 'deleted' ? api.toGitUri(fileUri, emptyRef) : api.toGitUri(fileUri, opts.commit);
Expand Down Expand Up @@ -211,11 +212,13 @@ function runGit(cwd: string, args: string[]): Promise<string> {

function formatRelative(date?: Date): string {
if (!date) return '';
const locale = resolveLocale(vscode.env.language);
const diff = Date.now() - date.getTime();
const h = Math.floor(diff / 3.6e6);
if (h < 1) return '刚刚';
if (h < 24) return `${h} 小时前`;
if (h < 1) return t(locale, 'ext.git.justNow');
if (h === 1) return t(locale, 'ext.git.hourAgo');
if (h < 24) return t(locale, 'ext.git.hoursAgo').replace('{h}', String(h));
Comment thread
zephyrq-z marked this conversation as resolved.
const d = Math.floor(h / 24);
if (d === 1) return '昨天';
return `${d} 天前`;
if (d === 1) return t(locale, 'ext.git.yesterday');
return t(locale, 'ext.git.daysAgo').replace('{d}', String(d));
}
2 changes: 1 addition & 1 deletion extensions/vscode/src/extension/services/shellEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function resolveBin(name: string): string {
let resolved = name;
try {
const shell = process.env.SHELL || '/bin/zsh';
if (!/^[a-zA-Z0-9._\/-]+$/.test(name)) return name;
if (!/^[a-zA-Z0-9._/-]+$/.test(name)) return name;
const res = spawnSync(shell, ['-ilc', `command -v '${name.replace(/'/g, "'\\''")}'`], {
encoding: 'utf8',
timeout: 5000,
Expand Down
Loading
Loading