feat: 实现 Task 3.2 Java 编译验证和 Task 4.4 前端代码编辑器

Task 3.2 - Java 编译验证:
- JavaCompilerValidator: 使用 javac 进行编译验证
- 支持临时文件编译和清理
- 集成到 ICompilerValidator 接口
- 单元测试 (需要 javac 环境)

Task 4.4 - 前端代码编辑器:
- CodeEditor.vue: Monaco Editor 集成
- 支持 C#/Java/C++ 语法高亮
- 智能代码补全 (main 方法,System.out.println 等)
- 支持主题切换 (vs-dark/vs/hc-black)
- 支持只读模式、最小化地图、自动布局
- 暴露 API: setValue, getValue, focus, layout

测试状态:
- 总测试数: 42
- 通过: 41 
- 跳过: 1 (Java 编译器测试需要 javac 环境)

前端依赖:
- 需安装 monaco-editor: npm install monaco-editor
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
This commit is contained in:
monkeycode-ai
2026-06-03 11:20:49 +00:00
parent 00570c129a
commit 4c94bdf666
5 changed files with 605 additions and 362 deletions
+238
View File
@@ -0,0 +1,238 @@
<template>
<div class="code-editor-container">
<div ref="editorContainer" class="editor-container"></div>
<div v-if="showMinimap" class="minimap"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
// 配置 Monaco Worker
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') return new jsonWorker()
if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker()
if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker()
if (label === 'typescript' || label === 'javascript') return new tsWorker()
return new editorWorker()
}
}
interface Props {
modelValue: string
language: 'csharp' | 'java' | 'cpp'
readOnly?: boolean
theme?: 'vs' | 'vs-dark' | 'hc-black'
showMinimap?: boolean
showLineNumbers?: boolean
automaticLayout?: boolean
}
const props = withDefaults(defineProps<Props>(), {
readOnly: false,
theme: 'vs-dark',
showMinimap: true,
showLineNumbers: true,
automaticLayout: true
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'change', value: string): void
(e: 'cursorChange', position: { lineNumber: number; column: number }): void
}>()
const editorContainer = ref<HTMLElement | null>(null)
let editor: monaco.editor.IStandaloneCodeEditor | null = null
const languageMap: Record<string, string> = {
csharp: 'csharp',
java: 'java',
cpp: 'cpp'
}
onMounted(() => {
if (!editorContainer.value) return
editor = monaco.editor.create(editorContainer.value, {
value: props.modelValue,
language: languageMap[props.language] || 'plaintext',
theme: props.theme,
readOnly: props.readOnly,
minimap: { enabled: props.showMinimap },
lineNumbers: props.showLineNumbers ? 'on' : 'off',
automaticLayout: props.automaticLayout,
fontSize: 14,
wordWrap: 'on',
scrollBeyondLastLine: false,
renderWhitespace: 'selection',
suggestOnTriggerCharacters: true,
quickSuggestions: true,
tabSize: 4,
insertSpaces: true,
formatOnPaste: true,
formatOnType: true,
autoClosingBrackets: 'always',
autoClosingQuotes: 'always',
bracketPairColorization: { enabled: true },
glyphMargin: true,
folding: true,
foldingStrategy: 'indentation'
})
editor.onDidChangeModelContent(() => {
const value = editor?.getValue() || ''
emit('update:modelValue', value)
emit('change', value)
})
editor.onDidChangeCursorPosition((e) => {
emit('cursorChange', {
lineNumber: e.position.lineNumber,
column: e.position.column
})
})
registerCustomLanguages()
})
onBeforeUnmount(() => {
editor?.dispose()
})
watch(() => props.modelValue, (newValue) => {
if (editor && newValue !== editor.getValue()) {
editor.setValue(newValue)
}
})
watch(() => props.language, (newLang) => {
if (editor) {
const model = editor.getModel()
if (model) {
monaco.editor.setModelLanguage(model, languageMap[newLang] || 'plaintext')
}
}
})
watch(() => props.theme, (newTheme) => {
if (editor) {
monaco.editor.setTheme(newTheme)
}
})
const registerCustomLanguages = () => {
monaco.languages.registerCompletionItemProvider('java', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
}
return {
suggestions: [
{
label: 'public static void main',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'public static void main(String[] args) {\n\t$0\n}',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range
},
{
label: 'System.out.println',
kind: monaco.languages.CompletionItemKind.Function,
insertText: 'System.out.println($0);',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range
}
]
}
}
})
monaco.languages.registerCompletionItemProvider('csharp', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
}
return {
suggestions: [
{
label: 'Console.WriteLine',
kind: monaco.languages.CompletionItemKind.Function,
insertText: 'Console.WriteLine($0);',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range
}
]
}
}
})
}
const setDecorations = (decorations: monaco.editor.IModelDeltaDecoration[]) => {
if (!editor) return
const model = editor.getModel()
if (!model) return
editor.createDecorationsCollection(decorations)
}
const setValue = (value: string) => {
editor?.setValue(value)
}
const getValue = (): string => {
return editor?.getValue() || ''
}
const focus = () => {
editor?.focus()
}
const layout = () => {
editor?.layout()
}
defineExpose({
setDecorations,
setValue,
getValue,
focus,
layout
})
</script>
<style scoped>
.code-editor-container {
width: 100%;
height: 100%;
display: flex;
position: relative;
}
.editor-container {
flex: 1;
height: 100%;
min-height: 300px;
}
.minimap {
width: 50px;
background: rgba(0, 0, 0, 0.1);
}
</style>