Skip to content

Commit 754b22e

Browse files
committed
feat(ai): 新增「AI 修复诊断」——把当前文件 LSP 诊断交给 AI 修复
AiCodeAction 增加 fix 模式并接收诊断文本;App 收集当前文件诊断 + 整篇代码送入, 结果经替换应用;编辑器右键(支持 LSP/Git 时)与命令面板均可触发。
1 parent 4d356a1 commit 754b22e

4 files changed

Lines changed: 38 additions & 7 deletions

File tree

src/App.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@
399399
<DebugPanel/>
400400

401401
<!-- AI 代码操作(解释/重构/生成测试) -->
402-
<AiCodeAction v-if="aiCodeCtx" :language="currentLanguage" :code="aiCodeCtx.code" :action="aiCodeCtx.action"
402+
<AiCodeAction v-if="aiCodeCtx" :language="currentLanguage" :code="aiCodeCtx.code" :action="aiCodeCtx.action" :diagnostics="aiCodeCtx.diagnostics"
403403
@replace="onAiReplace" @insert="onAiInsert" @close="aiCodeCtx = null"/>
404404

405405
<!-- .gitignore 模板 -->
@@ -463,6 +463,7 @@
463463
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="aiCodeAction('explain')">{{ t('aiCode.title.explain') }}</button>
464464
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="aiCodeAction('refactor')">{{ t('aiCode.title.refactor') }}</button>
465465
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="aiCodeAction('test')">{{ t('aiCode.title.test') }}</button>
466+
<button v-if="canBlame || editorCtx.lsp" class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="aiFixDiagnostics">{{ t('aiCode.title.fix') }}</button>
466467
<div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
467468
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="sendToTerminal">
468469
{{ t('app.sendToTerminal') }}
@@ -543,6 +544,7 @@ import {useWorkspace} from './composables/useWorkspace'
543544
import {useTextCommands} from './composables/useTextCommands'
544545
import {useBookmarks} from './composables/useBookmarks'
545546
import {foldAll, unfoldAll, matchBrackets} from '@codemirror/language'
547+
import {diagnostics} from './editor/lspDiagnostics'
546548
import {useGitPermalink} from './composables/useGitPermalink'
547549
import {useRevealInTree} from './composables/useRevealInTree'
548550
import {useWorkspaceRoots} from './composables/useWorkspaceRoots'
@@ -1420,7 +1422,21 @@ const runTests = async () => {
14201422
}
14211423
14221424
// C2:对选区(无选区则整篇)执行 AI 操作:解释 / 重构 / 生成测试
1423-
const aiCodeCtx = ref<{action: 'explain' | 'refactor' | 'test'; code: string; from: number; to: number} | null>(null)
1425+
const aiCodeCtx = ref<{action: 'explain' | 'refactor' | 'test' | 'fix'; code: string; from: number; to: number; diagnostics?: string} | null>(null)
1426+
// AI 修复诊断:把当前文件的 LSP 诊断交给 AI 修复整篇
1427+
const aiFixDiagnostics = () => {
1428+
closeEditorCtx()
1429+
const view = editorView.value
1430+
if (!view) {
1431+
return
1432+
}
1433+
if (!diagnostics.value.length) {
1434+
toast.info(t('app.noDiagnostics'))
1435+
return
1436+
}
1437+
const diagText = diagnostics.value.map(d => `[${d.severity}] L${d.line}:${d.col} ${d.message}`).join('\n')
1438+
aiCodeCtx.value = {action: 'fix', code: view.state.doc.toString(), from: 0, to: view.state.doc.length, diagnostics: diagText}
1439+
}
14241440
const aiCodeAction = (action: 'explain' | 'refactor' | 'test') => {
14251441
closeEditorCtx()
14261442
const view = editorView.value
@@ -2228,6 +2244,7 @@ const paletteCommands = computed<PaletteCommand[]>(() => [
22282244
{id: 'explainCode', label: t('command.explainCode'), icon: Sparkles, run: () => explainCode()},
22292245
{id: 'generateTests', label: t('command.generateTests'), icon: Sparkles, run: () => generateTests()},
22302246
{id: 'formatWithAi', label: t('command.formatWithAi'), icon: Sparkles, run: () => formatWithAi()},
2247+
{id: 'aiFixDiagnostics', label: t('command.aiFixDiagnostics'), icon: Sparkles, run: () => aiFixDiagnostics()},
22312248
{id: 'history', label: t('command.history'), icon: History, run: () => { showHistory.value = true }},
22322249
{id: 'diff', label: t('command.diff'), icon: GitCompare, run: () => openDiff()},
22332250
{id: 'compareClipboard', label: t('command.compareClipboard'), icon: GitCompare, run: () => compareWithClipboard()},

src/components/AiCodeAction.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<div class="flex items-center justify-end gap-2 px-4 py-2.5 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
2323
<button class="text-xs px-3 py-1.5 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="emit('close')">{{ t('aiCode.close') }}</button>
2424
<button v-if="!loading && result" class="text-xs px-3 py-1.5 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click="copy">{{ t('aiCode.copy') }}</button>
25-
<button v-if="!loading && result && action === 'refactor'" class="text-xs px-3 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer" @click="apply('replace')">{{ t('aiCode.replace') }}</button>
25+
<button v-if="!loading && result && (action === 'refactor' || action === 'fix')" class="text-xs px-3 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer" @click="apply('replace')">{{ t('aiCode.replace') }}</button>
2626
<button v-if="!loading && result && action === 'test'" class="text-xs px-3 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 cursor-pointer" @click="apply('insert')">{{ t('aiCode.insert') }}</button>
2727
</div>
2828
</div>
@@ -37,7 +37,7 @@ import {useI18n} from 'vue-i18n'
3737
import {useAiConfig} from '../composables/useAiConfig'
3838
import {useToast} from '../plugins/toast'
3939
40-
const props = defineProps<{ language: string; code: string; action: 'explain' | 'refactor' | 'test' }>()
40+
const props = defineProps<{ language: string; code: string; action: 'explain' | 'refactor' | 'test' | 'fix'; diagnostics?: string }>()
4141
const emit = defineEmits<{ replace: [code: string]; insert: [code: string]; close: [] }>()
4242
4343
const toast = useToast()
@@ -62,9 +62,17 @@ const systemFor = (): string => {
6262
return `你是代码助手。重构给定的 ${lang} 代码以提升可读性与质量,保持行为不变。只输出重构后的完整代码,不要解释,不要使用 Markdown 代码块标记。`
6363
case 'test':
6464
return `你是测试工程师。为给定的 ${lang} 代码生成单元测试。只输出测试代码,不要解释,不要使用 Markdown 代码块标记。`
65+
case 'fix':
66+
return `你是代码助手。修复给定 ${lang} 代码中的错误与警告(用户消息附带诊断信息),保持其余行为不变。只输出修复后的完整代码,不要解释,不要使用 Markdown 代码块标记。`
6567
}
6668
}
6769
70+
// 'fix' 把诊断附在用户消息里
71+
const userContent = (): string =>
72+
props.action === 'fix' && props.diagnostics
73+
? `${props.code}\n\n--- 待修复的诊断 ---\n${props.diagnostics}`
74+
: props.code
75+
6876
const run = async () => {
6977
reload()
7078
if (!active.value.apiKey) {
@@ -80,7 +88,7 @@ const run = async () => {
8088
apiKey: active.value.apiKey,
8189
model: active.value.model,
8290
system: systemFor(),
83-
messages: [{role: 'user', content: props.code}]
91+
messages: [{role: 'user', content: userContent()}]
8492
})
8593
result.value = props.action === 'explain' ? reply.trim() : stripFences(reply)
8694
}

src/i18n/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@
683683
"explainCode": "AI explain code (selection or all)",
684684
"generateTests": "AI generate tests (selection or all)",
685685
"formatWithAi": "AI format code",
686+
"aiFixDiagnostics": "AI fix diagnostics",
686687
"history": "Run history",
687688
"diff": "Diff (current vs saved)",
688689
"preview": "Live preview (Markdown / HTML)",
@@ -1128,7 +1129,8 @@
11281129
"title": {
11291130
"explain": "AI: Explain code",
11301131
"refactor": "AI: Refactor code",
1131-
"test": "AI: Generate tests"
1132+
"test": "AI: Generate tests",
1133+
"fix": "AI: Fix diagnostics"
11321134
},
11331135
"thinking": "AI is thinking…",
11341136
"replace": "Replace selection",
@@ -1338,6 +1340,7 @@
13381340
"copiedMarkdown": "Copied as Markdown code block",
13391341
"indentConverted": "Indentation converted",
13401342
"noMatchingBracket": "No matching bracket at cursor",
1343+
"noDiagnostics": "No diagnostics in the current file",
13411344
"clipboardEmpty": "Clipboard is empty",
13421345
"clipboardReadFailed": "Failed to read clipboard: ",
13431346
"copyFailed": "Copy failed: ",

src/i18n/locales/zh-CN.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@
683683
"explainCode": "AI 解释代码(选中或全文)",
684684
"generateTests": "AI 生成测试(选中或全文)",
685685
"formatWithAi": "AI 格式化代码",
686+
"aiFixDiagnostics": "AI 修复诊断",
686687
"history": "执行历史",
687688
"diff": "差异对比(当前 vs 已保存)",
688689
"preview": "实时预览(Markdown / HTML)",
@@ -1128,7 +1129,8 @@
11281129
"title": {
11291130
"explain": "AI 解释代码",
11301131
"refactor": "AI 重构代码",
1131-
"test": "AI 生成测试"
1132+
"test": "AI 生成测试",
1133+
"fix": "AI 修复诊断"
11321134
},
11331135
"thinking": "AI 思考中…",
11341136
"replace": "替换选区",
@@ -1338,6 +1340,7 @@
13381340
"copiedMarkdown": "已复制为 Markdown 代码块",
13391341
"indentConverted": "已转换缩进",
13401342
"noMatchingBracket": "光标处没有可匹配的括号",
1343+
"noDiagnostics": "当前文件没有诊断问题",
13411344
"clipboardEmpty": "剪贴板为空",
13421345
"clipboardReadFailed": "读取剪贴板失败: ",
13431346
"copyFailed": "复制失败: ",

0 commit comments

Comments
 (0)