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:
@@ -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>
|
||||
Reference in New Issue
Block a user