diff --git a/package-lock.json b/package-lock.json index b772e2b..9a5e67a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "oj-next", "version": "1.8.0", "dependencies": { + "@codemirror/autocomplete": "^6.20.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-python": "^6.2.1", "@vue-flow/background": "^1.3.2", @@ -177,9 +178,9 @@ "license": "Apache-2.0" }, "node_modules/@codemirror/autocomplete": { - "version": "6.19.0", - "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz", - "integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==", + "version": "6.20.0", + "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", diff --git a/package.json b/package.json index 94d52ac..e9b4379 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "fmt": "prettier --write src *.ts" }, "dependencies": { + "@codemirror/autocomplete": "^6.20.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-python": "^6.2.1", "@vue-flow/background": "^1.3.2", diff --git a/src/shared/components/CodeEditor.vue b/src/shared/components/CodeEditor.vue index 8a3bbff..6e1070d 100644 --- a/src/shared/components/CodeEditor.vue +++ b/src/shared/components/CodeEditor.vue @@ -3,10 +3,13 @@ import { cpp } from "@codemirror/lang-cpp" import { python } from "@codemirror/lang-python" import { EditorView } from "@codemirror/view" import { Codemirror } from "vue-codemirror" -import type { Extension } from "@codemirror/state" +import { autocompletion } from "@codemirror/autocomplete" import { LANGUAGE } from "utils/types" import { oneDark } from "../themes/oneDark" import { smoothy } from "../themes/smoothy" +import { enhanceCompletion } from "shared/extensions/autocompletion" + + interface Props { language?: LANGUAGE fontSize?: number @@ -36,13 +39,16 @@ const styleTheme = EditorView.baseTheme({ }, }) -const lang = computed((): Extension => { +const langExtension = computed(() => { return ["Python2", "Python3"].includes(props.language) ? python() : cpp() }) const extensions = computed(() => [ styleTheme, - lang.value, + langExtension.value, + autocompletion({ + override: [enhanceCompletion(props.language)], + }), isDark.value ? oneDark : smoothy, ]) diff --git a/src/shared/components/SyncCodeEditor.vue b/src/shared/components/SyncCodeEditor.vue index 9390e76..330d71a 100644 --- a/src/shared/components/SyncCodeEditor.vue +++ b/src/shared/components/SyncCodeEditor.vue @@ -3,12 +3,15 @@ import { cpp } from "@codemirror/lang-cpp" import { python } from "@codemirror/lang-python" import { EditorView } from "@codemirror/view" import { Codemirror } from "vue-codemirror" +import { autocompletion } from "@codemirror/autocomplete" import type { Extension } from "@codemirror/state" import { LANGUAGE } from "utils/types" import { oneDark } from "../themes/oneDark" import { smoothy } from "../themes/smoothy" import { useCodeSync, SYNC_ERROR_CODES } from "../composables/sync" import { useBreakpoints } from "../composables/breakpoints" +import { enhanceCompletion } from "shared/extensions/autocompletion" + const isDark = useDark() interface EditorReadyPayload { @@ -55,14 +58,17 @@ const styleTheme = EditorView.baseTheme({ }, }) -const lang = computed((): Extension => { +const langExtension = computed((): Extension => { return ["Python2", "Python3"].includes(props.language) ? python() : cpp() }) const extensions = computed(() => [ styleTheme, - lang.value, + langExtension.value, isDark.value ? oneDark : smoothy, + autocompletion({ + override: [enhanceCompletion(props.language)], + }), getInitialExtension(), ]) diff --git a/src/shared/extensions/autocompletion.ts b/src/shared/extensions/autocompletion.ts new file mode 100644 index 0000000..3a267fa --- /dev/null +++ b/src/shared/extensions/autocompletion.ts @@ -0,0 +1,670 @@ +import type { + Completion, + CompletionContext, + CompletionResult, + CompletionSource, +} from "@codemirror/autocomplete" +import type { EditorView } from "@codemirror/view" +import { LANGUAGE } from "utils/types" + +type ChineseCompletion = Pick< + Completion, + "label" | "detail" | "type" | "info" | "boost" | "apply" +> & { apply?: string | Completion["apply"] } + +// 中文注释提示 +const chineseAnnotations: Record = { + python: [ + { + label: "print", + detail: "打印输出", + type: "function", + info: "内置函数,将对象输出到标准输出,可用 sep 和 end 指定分隔符与结尾字符", + boost: 100, + apply: "print()", + }, + { + label: "input", + detail: "读取输入", + type: "function", + info: "内置函数,读取一行输入并返回字符串,可传入提示信息", + boost: 95, + apply: "input()", + }, + { + label: "len", + detail: "获取长度", + type: "function", + info: "返回对象的长度,常用于列表、字符串、字典等序列或集合类型", + boost: 90, + apply: "len()", + }, + { + label: "range", + detail: "生成整数序列", + type: "function", + info: "返回不可变的整数序列,支持 start、stop、step,常用于 for 循环", + boost: 85, + apply: "range()", + }, + { + label: "enumerate", + detail: "枚举索引与元素", + type: "function", + info: "遍历可迭代对象时同时得到索引和值,可通过 start 指定起始索引", + boost: 82, + apply: "enumerate()", + }, + { + label: "zip", + detail: "并行遍历", + type: "function", + info: "将多个可迭代对象聚合为元组迭代器,长度以最短序列为准", + boost: 80, + apply: "zip()", + }, + { + label: "map", + detail: "映射函数", + type: "function", + info: "对可迭代对象的每个元素应用函数,返回惰性迭代器", + boost: 78, + apply: "map()", + }, + { + label: "filter", + detail: "过滤元素", + type: "function", + info: "保留函数返回真值的元素,返回惰性迭代器", + boost: 76, + apply: "filter()", + }, + { + label: "sorted", + detail: "排序", + type: "function", + info: "返回排序后的新列表,支持 key 与 reverse 参数", + boost: 74, + apply: "sorted()", + }, + { + label: "sum", + detail: "求和", + type: "function", + info: "对可迭代对象元素求和,可指定起始值", + boost: 72, + apply: "sum()", + }, + { + label: "open", + detail: "文件读写", + type: "function", + info: "打开文件并返回文件对象,常与 with 语句搭配确保自动关闭", + boost: 70, + apply: "open()", + }, + { + label: "abs", + detail: "绝对值", + type: "function", + info: "返回数字的绝对值", + boost: 68, + apply: "abs()", + }, + { + label: "round", + detail: "四舍五入", + type: "function", + info: "按指定精度进行四舍五入,默认到整数", + boost: 66, + apply: "round()", + }, + { + label: "isinstance", + detail: "类型检查", + type: "function", + info: "判断对象是否为某个类型或类型元组的实例", + boost: 64, + apply: "isinstance()", + }, + { + label: "type", + detail: "获取类型", + type: "function", + info: "返回对象的类型,或在三个参数形式下动态创建类型", + boost: 62, + apply: "type()", + }, + { + label: "list", + detail: "列表构造", + type: "function", + info: "将可迭代对象转换为列表,或创建空列表", + boost: 60, + apply: "list()", + }, + { + label: "dict", + detail: "字典构造", + type: "function", + info: "根据映射或键值对序列创建字典", + boost: 58, + apply: "dict()", + }, + { + label: "set", + detail: "集合构造", + type: "function", + info: "根据可迭代对象创建集合,自动去重", + boost: 56, + apply: "set()", + }, + { + label: "tuple", + detail: "元组构造", + type: "function", + info: "将可迭代对象转换为元组,或创建空元组", + boost: 54, + apply: "tuple()", + }, + { + label: "int", + detail: "转整数", + type: "function", + info: "将参数转换为整型,支持基数转换", + boost: 74, + apply: "int()", + }, + { + label: "float", + detail: "转浮点数", + type: "function", + info: "将参数转换为浮点数,支持字符串与数字", + boost: 72, + apply: "float()", + }, + { + label: "str", + detail: "转字符串", + type: "function", + info: "将对象转换为字符串表示,常用于输出", + boost: 70, + apply: "str()", + }, + { + label: "bool", + detail: "转布尔值", + type: "function", + info: "根据真值测试转换为 True/False,空对象为 False", + boost: 68, + apply: "bool()", + }, + { + label: "def", + detail: "定义函数", + type: "keyword", + info: "定义可复用的函数块,支持位置参数、关键字参数与默认值", + boost: 52, + }, + { + label: "class", + detail: "定义类", + type: "keyword", + info: "定义自定义类型,支持继承与魔术方法", + boost: 50, + }, + { + label: "with", + detail: "上下文管理", + type: "keyword", + info: "进入上下文管理器,自动处理资源的进入与退出", + boost: 48, + }, + { + label: "try", + detail: "异常捕获", + type: "keyword", + info: "开始异常处理块,与 except/finally/else 结合使用", + boost: 46, + }, + { + label: "except", + detail: "处理异常", + type: "keyword", + info: "捕获特定异常类型并处理,常配合 try 使用", + boost: 44, + }, + { + label: "finally", + detail: "清理收尾", + type: "keyword", + info: "无论是否发生异常都执行的收尾代码块", + boost: 42, + }, + { + label: "import", + detail: "导入模块", + type: "keyword", + info: "导入模块或包中的名称,可与 as 指定别名", + boost: 40, + }, + { + label: "from", + detail: "按需导入", + type: "keyword", + info: "从模块中按名称导入对象,可结合 import 与 as", + boost: 38, + }, + { + label: "return", + detail: "返回值", + type: "keyword", + info: "结束函数并返回值,未指定值时返回 None", + boost: 36, + }, + { + label: "append", + detail: "列表追加", + type: "method", + info: "在列表尾部添加新元素,等价于 list.append(value)", + boost: 48, + apply: "append()", + }, + { + label: "insert", + detail: "列表插入", + type: "method", + info: "在指定位置插入元素,list.insert(index, value)", + boost: 46, + apply: "insert()", + }, + { + label: "remove", + detail: "删除匹配值", + type: "method", + info: "删除列表中第一次出现的指定值,若不存在将抛出异常", + boost: 44, + apply: "remove()", + }, + { + label: "pop", + detail: "弹出元素", + type: "method", + info: "移除并返回列表指定位置(默认尾部)的元素", + boost: 42, + apply: "pop()", + }, + { + label: "count", + detail: "统计次数", + type: "method", + info: "返回某个对象在列表中出现的次数", + boost: 40, + apply: "count()", + }, + { + label: "reverse", + detail: "反转列表", + type: "method", + info: "原地反转列表中元素的顺序", + boost: 38, + apply: "reverse()", + }, + { + label: "sort", + detail: "列表排序", + type: "method", + info: "对列表进行原地排序,可指定 key 与 reverse", + boost: 36, + apply: "sort()", + }, + { + label: "add", + detail: "集合添加", + type: "method", + info: "向集合添加单个元素,若已存在则忽略", + boost: 50, + apply: "add()", + }, + { + label: "clear", + detail: "清空集合", + type: "method", + info: "移除集合中所有元素,变为空集", + boost: 48, + apply: "clear()", + }, + { + label: "keys", + detail: "字典键", + type: "method", + info: "返回字典键的可迭代视图", + boost: 42, + apply: "keys()", + }, + { + label: "values", + detail: "字典值", + type: "method", + info: "返回字典值的可迭代视图", + boost: 40, + apply: "values()", + }, + { + label: "split", + detail: "字符串切割", + type: "method", + info: "按分隔符切分字符串,返回列表,默认按空白字符", + boost: 52, + apply: "split()", + }, + { + label: "replace", + detail: "字符串替换", + type: "method", + info: "将字符串中的子串替换为新内容,可限定次数", + boost: 50, + apply: "replace()", + }, + { + label: "format", + detail: "格式化字符串", + type: "method", + info: "使用占位符或命名参数进行字符串格式化", + boost: 48, + apply: "format()", + }, + { + label: "strip", + detail: "去首尾指定字符", + type: "method", + info: "移除字符串首尾指定字符,默认移除空白", + boost: 46, + apply: "strip()", + }, + { + label: "lower", + detail: "转小写", + type: "method", + info: "将字符串中的字母转换为小写形式", + boost: 44, + apply: "lower()", + }, + { + label: "upper", + detail: "转大写", + type: "method", + info: "将字符串中的字母转换为大写形式", + boost: 42, + apply: "upper()", + }, + { + label: "swapcase", + detail: "大小写互换", + type: "method", + info: "将字符串中的大小写字母互换", + boost: 40, + apply: "swapcase()", + }, + { + label: "find", + detail: "查找子串位置", + type: "method", + info: "返回子串首次出现的索引,未找到返回 -1", + boost: 38, + apply: "find()", + }, + { + label: "index", + detail: "查找子串索引", + type: "method", + info: "返回子串首次出现的索引,未找到抛出异常", + boost: 36, + apply: "index()", + }, + { + label: "startswith", + detail: "前缀判断", + type: "method", + info: "判断字符串是否以指定前缀开头,可指定范围", + boost: 34, + apply: "startswith()", + }, + { + label: "endswith", + detail: "后缀判断", + type: "method", + info: "判断字符串是否以指定后缀结尾,可指定范围", + boost: 32, + apply: "endswith()", + }, + { + label: "isalnum", + detail: "是否字母数字", + type: "method", + info: "检测字符串是否只由字母和数字组成", + boost: 30, + apply: "isalnum()", + }, + { + label: "isalpha", + detail: "是否字母", + type: "method", + info: "检测字符串是否只由字母组成", + boost: 28, + apply: "isalpha()", + }, + { + label: "isdigit", + detail: "是否数字", + type: "method", + info: "检测字符串是否只由数字组成", + boost: 26, + apply: "isdigit()", + }, + { + label: "islower", + detail: "是否全小写", + type: "method", + info: "检测字符串是否全部由小写字母组成且至少有一个字母", + boost: 24, + apply: "islower()", + }, + { + label: "isupper", + detail: "是否全大写", + type: "method", + info: "检测字符串中所有字母是否都为大写且至少有一个字母", + boost: 22, + apply: "isupper()", + }, + ], + c: [ + { + label: "printf", + detail: "格式化输出", + type: "function", + info: "标准输出函数,格式化打印字符串,常配合 %d/%s 等占位符", + boost: 90, + apply: "printf();", + }, + { + label: "scanf", + detail: "格式化输入", + type: "function", + info: "标准输入函数,按格式读取数据,使用地址符 & 接收变量", + boost: 88, + apply: "scanf();", + }, + { + label: "puts", + detail: "输出字符串", + type: "function", + info: "输出以 \\0 结尾的字符串并自动换行,比 printf 简洁", + boost: 84, + apply: "puts();", + }, + { + label: "gets", + detail: "读取字符串", + type: "function", + info: "读取一行字符串到缓冲区(不安全,建议使用 fgets)", + boost: 60, + apply: "gets();", + }, + { + label: "fgets", + detail: "安全读行", + type: "function", + info: "从文件流读取一行到缓冲区,限制读取长度,避免溢出", + boost: 82, + apply: "fgets();", + }, + { + label: "memset", + detail: "内存填充", + type: "function", + info: "将一段内存按字节填充为指定值,常用于初始化数组/结构体", + boost: 80, + apply: "memset();", + }, + { + label: "memcpy", + detail: "内存拷贝", + type: "function", + info: "从源地址复制指定字节到目标地址,注意避免重叠", + boost: 78, + apply: "memcpy();", + }, + { + label: "strlen", + detail: "字符串长度", + type: "function", + info: "计算以 \\0 结尾的字符串长度(不含终止符)", + boost: 76, + apply: "strlen();", + }, + { + label: "strcmp", + detail: "字符串比较", + type: "function", + info: "按字典序比较两个字符串,相等返回 0,小于返回负数", + boost: 74, + apply: "strcmp();", + }, + { + label: "strcpy", + detail: "字符串拷贝", + type: "function", + info: "将源字符串复制到目标(包含终止符),目标需有足够空间", + boost: 72, + apply: "strcpy();", + }, + { + label: "int main", + detail: "程序入口", + type: "keyword", + info: "C 程序入口函数,通常返回 0 表示正常退出", + boost: 70, + }, + { + label: "for", + detail: "循环语句", + type: "keyword", + info: "for (init; condition; step) 结构,用于固定次数循环", + boost: 68, + }, + { + label: "while", + detail: "条件循环", + type: "keyword", + info: "while (condition) 循环,条件为真时重复执行", + boost: 66, + }, + { + label: "if", + detail: "条件判断", + type: "keyword", + info: "if (condition) 分支,可搭配 else / else if", + boost: 64, + }, + { + label: "struct", + detail: "结构体定义", + type: "keyword", + info: "定义自定义数据结构,可组合不同类型的成员", + boost: 62, + }, + { + label: "typedef", + detail: "类型别名", + type: "keyword", + info: "为已有类型定义别名,提升可读性", + boost: 60, + }, + { + label: "const", + detail: "只读限定", + type: "keyword", + info: "声明常量或只读指针,防止意外修改", + boost: 58, + }, + { + label: "return", + detail: "返回值", + type: "keyword", + info: "结束函数并返回值,main 返回 0 表示成功", + boost: 56, + }, + ], +} + +export function enhanceCompletion(language: LANGUAGE): CompletionSource { + return async function ( + context: CompletionContext, + ): Promise { + const word = context.matchBefore(/\w+/) + if (!word) return null + + const trulyLanguage = language.startsWith("Python") ? "python" : "c" + const completions: Completion[] = ( + chineseAnnotations[trulyLanguage] || [] + ).map((completion) => { + const insertText = + typeof completion.apply === "string" + ? completion.apply + : completion.label + const cursorOffset = insertText.includes("(") + ? insertText.indexOf("(") + 1 + : insertText.length + + if ( + (completion.type === "function" || completion.type === "method") && + insertText.includes(")") + ) { + return { + ...completion, + apply: ( + view: EditorView, + _c: Completion, + from: number, + to: number, + ) => { + view.dispatch({ + changes: { from, to, insert: insertText }, + selection: { anchor: from + cursorOffset }, + }) + }, + } + } + + return completion + }) + + return { + from: word.from, + options: completions, + validFor: /^\w+$/, + } + } +}