yh-codemirror 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CodeMirror.vue CHANGED
@@ -174,6 +174,7 @@ import {ref, onMounted, onBeforeUnmount, nextTick, toRaw} from 'vue';
174
174
  import type {Extension} from 'codemirror';
175
175
  import {EditorView, basicSetup} from 'codemirror';
176
176
  import {ViewUpdate, keymap} from '@codemirror/view';
177
+ import {indentWithTab} from '@codemirror/commands';
177
178
  import {EditorState, EditorSelection} from '@codemirror/state';
178
179
  import {javascriptLanguage} from '@codemirror/lang-javascript';
179
180
  import {solarizedDark} from 'cm6-theme-solarized-dark';
@@ -181,6 +182,7 @@ import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
181
182
  import {tags} from '@lezer/highlight';
182
183
  import {getCodeLanguagePlugin} from './util';
183
184
  import type {AiCodeEditParams, AiCodeEditRequest} from './util';
185
+ import {createArgCompletion} from './arg-completion';
184
186
  import {aiHistory, pushAiHistory, clearAiHistory} from './ai-history';
185
187
  import {watch, computed} from 'vue';
186
188
  // @ts-ignore
@@ -238,6 +240,21 @@ const props = withDefaults(
238
240
  * 组件只负责原样记进历史并在问题前显示,不解析它的含义
239
241
  */
240
242
  aiLocation?: string;
243
+ /**
244
+ * 方法参数候选:给某个方法的第 N 个字符串参数提供候选值。
245
+ * 组件不认识任何业务概念,方法名 / 参数下标 / 候选均由宿主配置;
246
+ * 不传或传空数组时不提示,也不影响语言自带的关键字 / 变量补全。
247
+ */
248
+ argCompletions?: Array<{
249
+ /** 方法名,如 showModal */
250
+ fn: string;
251
+ /** 参数下标(0 基),1 表示第 2 个参数 */
252
+ arg: number;
253
+ /** 候选值;字符串数组或带说明的对象数组 */
254
+ options: Array<string | {label: string; detail?: string}>;
255
+ /** 候选项的统一说明(可选),显示在列表右侧 */
256
+ detail?: string;
257
+ }>;
241
258
  }>(),
242
259
  {
243
260
  modelValue: '',
@@ -250,6 +267,7 @@ const props = withDefaults(
250
267
  aiPanelSize: 'normal',
251
268
  aiWindowId: '',
252
269
  aiLocation: '',
270
+ argCompletions: () => [],
253
271
  }
254
272
  );
255
273
 
@@ -577,6 +595,10 @@ const onEditorBtnClick = (type) => {
577
595
  onMounted(() => {
578
596
  const extensions: Extension[] = [
579
597
  basicSetup,
598
+ // Tab 缩进必须显式加:CodeMirror 6 为了不让键盘用户被困在编辑区,默认不把 Tab
599
+ // 绑给缩进,事件冒泡到浏览器就变成「跳到下一个表单项」。
600
+ // 缩进宽度用默认 indentUnit(2 空格),与 codeBeautifyConfig.indent_size 一致。
601
+ keymap.of([indentWithTab]),
580
602
  props.theme === 'light' ? lightThemeExtension : solarizedDark,
581
603
  EditorView.lineWrapping,
582
604
  EditorView.updateListener.of((v: ViewUpdate) => {
@@ -613,6 +635,16 @@ onMounted(() => {
613
635
  }
614
636
  }
615
637
 
638
+ // 方法参数补全:只在宿主配置过的「方法 + 参数下标」的字符串字面量内出候选
639
+ // (showModal 的 vuePath、setValue 的字段名、success 的 type 都走这里)。
640
+ // 宿主没配时补全源第一行就返回 null,不扫文档,因此这里无条件挂上,
641
+ // 配置异步就绪也不用重建编辑器。
642
+ extensions.push(
643
+ javascriptLanguage.data.of({
644
+ autocomplete: createArgCompletion(() => props.argCompletions),
645
+ })
646
+ );
647
+
616
648
  if (aiEnabled.value) {
617
649
  extensions.push(
618
650
  keymap.of([
package/README.md CHANGED
@@ -1,111 +1,162 @@
1
- # yh-codemirror
2
-
3
- Vue3 的 CodeMirror6 封装,在全屏 / 美化 / 全选 / 复制 / 清空 等基础能力之上,提供 **AI 提示编辑**(选中或整篇代码 → 自然语言描述 → AI 改写回写)。
4
-
5
- ## 基础用法
6
-
7
- ```vue
8
- <yh-codemirror
9
- v-model="code"
10
- :height="500"
11
- language="javascript"
12
- :autocomplete="{javascript: myCompletionSource}">
13
- </yh-codemirror>
14
- ```
15
-
16
- `language` 支持 `javascript / sql / css / java / html / json`,可传数组;不在列表内会取不到语言插件。
17
- `autocomplete` 只对 `javascript` 生效(按语言名取,如 `{javascript: myCompletionSource}`)。
18
- 不传 `aiRequest` 就是一个纯编辑器,不会出现任何 AI 入口。
19
-
20
- ## AI 提示编辑
21
-
22
- 组件本身**不内置任何 AI 服务**(保持零业务依赖),由宿主通过 `aiRequest` 注入请求实现。传入 `aiRequest` 即自动启用:
23
-
24
- - 编辑器右下角出现 `AI` 入口按钮
25
- - 快捷键 `Ctrl+I`(Mac `Cmd+I`,可通过 `aiHotkey` 改)唤起输入框
26
- - 有选区 → 只改写选中代码;无选区 → 按 `aiScope` 作用于整个文档或光标处插入
27
- - `Enter` 发送、`Esc` 取消;请求中按钮变「停止」,可中断
28
- - 支持流式:调用 `onChunk` 逐段返回,编辑器实时写入;非流式则直接返回完整替换文本
29
- - 自动剥离模型常见的 ` ```lang ... ``` ` 包裹
30
- - 通过 dispatch 写入,保留 CodeMirror 撤销栈,可 `Ctrl+Z` 回退
31
- - **历史问答**:输入框右上角「历史」可查看本轮会话问过的问题与 AI 的完整回答(可展开、可复制、可清空),
32
- 每条前面会显示宿主传入的 `aiLocation`,建议格式「区域名称 · <按钮/查询条件/字段名称 + 类型> · 编辑器字段名」
33
- (如「区域信息 · 事件按钮 · 点击事件」「区域信息 · 名称字段 · 字段值更改」;
34
- 直接在区域事件里提问时没有中间层,就是「区域信息 · 数据初始化」)
35
-
36
- 历史记录存在 `ai-history.js` 的模块级单例里(上限 50 条,最新在前)。之所以不放组件内:
37
- 配置面板里的编辑器会随切换配置项反复卸载重建,组件内的 ref 一卸载就没了。
38
- 代价是同一页面所有编辑器共用一份(未做分组),所以每条都带时间、问题原文和 `aiLocation`——
39
- 没有归属信息时,一堆「把名称标红」的提问根本分不清是哪块配置。
40
-
41
- ### Props
42
-
43
- | 参数 | 类型 | 默认值 | 说明 |
44
- | --------------- | --------------------------------- | ---------- | ------------------------------------------------------ |
45
- | `aiRequest` | `AiCodeEditRequest` | - | AI 请求函数,传入即启用 AI 编辑 |
46
- | `aiHotkey` | `string` | `'Mod-i'` | 唤起输入框的快捷键(CodeMirror 键位语法) |
47
- | `aiPlaceholder` | `string` | 见源码 | 输入框占位提示 |
48
- | `aiScope` | `'document' \| 'cursor'` | `'document'` | 无选区时的作用范围 |
49
- | `aiPanelSize` | `'normal' \| 'large'` | `'normal'` | AI 输入框 / 历史面板的尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 `large` |
50
- | `aiWindowId` | `string \| number` | `''` | AI 上下文用的「所属窗口 ID」。组件不认识任何业务接口,只在 AI 被唤起时通过 `ai-activate` 把它抛给宿主,由宿主去取窗口配置 |
51
- | `aiLocation` | `string` | `''` | 这条提问来自哪里(建议「区域名 · 按钮/查询条件/字段名 + 类型 · 编辑器字段名」,如「区域信息 · 事件按钮 · 点击事件」,名称一律取 NAME)。组件只原样记进历史并显示在问题前,不解析其含义 |
52
-
53
- ### Events
54
-
55
- | 事件 | 参数 | 说明 |
56
- | ---------------- | ----------------------------- | ------------------------ |
57
- | `update:modelValue` | `string` | 代码变更(原有) |
58
- | `ai-activate` | `string \| number` | AI 面板刚打开(携带 `aiWindowId`),宿主可趁此时预取上下文;`aiWindowId` 为空时不触发 |
59
- | `ai-apply` | `{prompt, text}` | AI 结果已写入编辑器 |
60
- | `ai-error` | `Error` | AI 请求失败 |
61
-
62
- ### Expose
63
-
64
- `openAi(prompt?)`、`closeAi()`、`stopAi()`、`getView()`。
65
-
66
- ### 接入示例(非流式)
67
-
68
- ```ts
69
- import {YhCodemirror} from 'yh-codemirror';
70
- import type {AiCodeEditRequest} from 'yh-codemirror';
71
-
72
- const aiRequest: AiCodeEditRequest = async ({prompt, code, selectedCode, language, signal}) => {
73
- const res = await fetch('/api/ai/code-edit', {
74
- method: 'POST',
75
- signal,
76
- headers: {'Content-Type': 'application/json'},
77
- body: JSON.stringify({prompt, code, selectedCode, language}),
78
- });
79
- if (!res.ok) {
80
- throw new Error('AI 请求失败');
81
- }
82
- const data = await res.json();
83
- // 返回「作用范围的完整替换文本」,组件负责写入
84
- return data.code;
85
- };
86
- ```
87
-
88
- ### 接入示例(流式 SSE)
89
-
90
- ```ts
91
- const aiRequest: AiCodeEditRequest = async ({prompt, selectedCode, signal, onChunk}) => {
92
- const res = await fetch('/api/ai/code-edit/stream', {
93
- method: 'POST',
94
- signal,
95
- headers: {'Content-Type': 'application/json'},
96
- body: JSON.stringify({prompt, code: selectedCode}),
97
- });
98
- const reader = res.body!.getReader();
99
- const decoder = new TextDecoder();
100
- while (true) {
101
- const {done, value} = await reader.read();
102
- if (done) {
103
- break;
104
- }
105
- onChunk?.(decoder.decode(value, {stream: true}));
106
- }
107
- // 走 onChunk 时返回值可为空,组件已实时写入编辑器
108
- };
109
- ```
110
-
111
- > 提示:流式场景下 `onChunk` 传入的是增量片段,组件按累积文本整体覆盖作用区间,因此服务端无需自行做 diff。
1
+ # yh-codemirror
2
+
3
+ Vue3 的 CodeMirror6 封装,在全屏 / 美化 / 全选 / 复制 / 清空 等基础能力之上,提供 **AI 提示编辑**(选中或整篇代码 → 自然语言描述 → AI 改写回写)。
4
+
5
+ ## 基础用法
6
+
7
+ ```vue
8
+ <yh-codemirror
9
+ v-model="code"
10
+ :height="500"
11
+ language="javascript"
12
+ :autocomplete="{javascript: myCompletionSource}">
13
+ </yh-codemirror>
14
+ ```
15
+
16
+ `language` 支持 `javascript / sql / css / java / html / json`,可传数组;不在列表内会取不到语言插件。
17
+ `autocomplete` 只对 `javascript` 生效(按语言名取,如 `{javascript: myCompletionSource}`)。
18
+ 不传 `aiRequest` 就是一个纯编辑器,不会出现任何 AI 入口。
19
+
20
+ ## 方法参数补全
21
+
22
+ ```vue
23
+ <yh-codemirror
24
+ v-model="code"
25
+ language="javascript"
26
+ :arg-completions="argCompletions" />
27
+ ```
28
+
29
+ 某个方法的第 N 个字符串参数要给候选值,就配一条 `{fn, arg, options}`。组件按「调用名 + 参数下标」匹配,只在对应的字符串字面量内弹候选,其他位置一律不介入,JS 关键字与变量补全照旧:
30
+
31
+ ```js
32
+ const argCompletions = [
33
+ // showModal(title, vuePath, params):第 2 个参数是页面路径
34
+ {fn: 'showModal', arg: 1, options: vuePathOptions, detail: 'vue 页面路径'},
35
+ // setValue(fieldName, value):第 1 个参数是表单字段名
36
+ {fn: 'setValue', arg: 0, options: fieldNames, detail: '表单字段'},
37
+ // success(msg, type):第 2 个参数是枚举值
38
+ {
39
+ fn: 'success',
40
+ arg: 1,
41
+ options: ['info', 'success', 'warning', 'error', 'loading'],
42
+ detail: '提示类型',
43
+ },
44
+ ];
45
+ ```
46
+
47
+ | 字段 | 类型 | 说明 |
48
+ | --------- | -------------------------------------- | ------------------------------------------------------------ |
49
+ | `fn` | `string` | 方法名,如 `showModal`;兼容 `this.openModal(...)` 这类写法 |
50
+ | `arg` | `number` | 参数下标,从 0 开始;`1` 表示第 2 个参数 |
51
+ | `options` | `(string \| {label, detail})[]` | 候选值 |
52
+ | `detail` | `string` | 可选,候选项右侧的统一说明 |
53
+
54
+ 候选项写成 `{label, detail}` 时可以逐条给不同的右侧说明,`detail` 为空或没写才回落到外层那条统一说明。适合「值本身是英文、但用户认的是中文」的场景,比如字段名:
55
+
56
+ ```js
57
+ // 右侧显示「单价(元)」这类表单标题,插入的仍然是 PRICE
58
+ {fn: 'setValue', arg: 0, detail: '表单字段', options: [
59
+ {label: 'PRICE', detail: '单价(元)'},
60
+ {label: 'QTY', detail: '数量'},
61
+ ]},
62
+ ```
63
+
64
+ > 注意:过滤(敲字缩小候选)只按 `label` 匹配,`detail` 只负责展示,输入中文不会筛出对应字段。
65
+
66
+ - 候选清单由宿主注入,组件不认识 `$components`、字段、枚举这些业务概念;清单要按配置里该填的形式给(如 `basic/sysBasePage/orgManage.vue`,不含 `/src/view` 前缀)
67
+ - 传空数组或不传即不提示;清单异步就绪也生效,不需要重建编辑器
68
+ - 支持单双引号、模板字符串、跨行调用;`//`、`/* */` 注释里出现的同名文本不会触发
69
+ - 已输入的字符作为过滤词(模糊匹配),选中后替换引号内的内容
70
+
71
+ ## AI 提示编辑
72
+
73
+ 组件本身**不内置任何 AI 服务**(保持零业务依赖),由宿主通过 `aiRequest` 注入请求实现。传入 `aiRequest` 即自动启用:
74
+
75
+ - 编辑器右下角出现 `AI` 入口按钮
76
+ - 快捷键 `Ctrl+I`(Mac `Cmd+I`,可通过 `aiHotkey` 改)唤起输入框
77
+ - 有选区 → 只改写选中代码;无选区 → 按 `aiScope` 作用于整个文档或光标处插入
78
+ - `Enter` 发送、`Esc` 取消;请求中按钮变「停止」,可中断
79
+ - 支持流式:调用 `onChunk` 逐段返回,编辑器实时写入;非流式则直接返回完整替换文本
80
+ - 自动剥离模型常见的 ` ```lang ... ``` ` 包裹
81
+ - 通过 dispatch 写入,保留 CodeMirror 撤销栈,可 `Ctrl+Z` 回退
82
+ - **历史问答**:输入框右上角「历史」可查看本轮会话问过的问题与 AI 的完整回答(可展开、可复制、可清空),
83
+ 每条前面会显示宿主传入的 `aiLocation`,建议格式「区域名称 · <按钮/查询条件/字段名称 + 类型> · 编辑器字段名」
84
+ (如「区域信息 · 事件按钮 · 点击事件」「区域信息 · 名称字段 · 字段值更改」;
85
+ 直接在区域事件里提问时没有中间层,就是「区域信息 · 数据初始化」)
86
+
87
+ 历史记录存在 `ai-history.js` 的模块级单例里(上限 50 条,最新在前)。之所以不放组件内:
88
+ 配置面板里的编辑器会随切换配置项反复卸载重建,组件内的 ref 一卸载就没了。
89
+ 代价是同一页面所有编辑器共用一份(未做分组),所以每条都带时间、问题原文和 `aiLocation`——
90
+ 没有归属信息时,一堆「把名称标红」的提问根本分不清是哪块配置。
91
+
92
+ ### Props
93
+
94
+ | 参数 | 类型 | 默认值 | 说明 |
95
+ | --------------- | --------------------------------- | ---------- | ------------------------------------------------------ |
96
+ | `aiRequest` | `AiCodeEditRequest` | - | AI 请求函数,传入即启用 AI 编辑 |
97
+ | `aiHotkey` | `string` | `'Mod-i'` | 唤起输入框的快捷键(CodeMirror 键位语法) |
98
+ | `aiPlaceholder` | `string` | 见源码 | 输入框占位提示 |
99
+ | `aiScope` | `'document' \| 'cursor'` | `'document'` | 无选区时的作用范围 |
100
+ | `aiPanelSize` | `'normal' \| 'large'` | `'normal'` | AI 输入框 / 历史面板的尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 `large` |
101
+ | `aiWindowId` | `string \| number` | `''` | AI 上下文用的「所属窗口 ID」。组件不认识任何业务接口,只在 AI 被唤起时通过 `ai-activate` 把它抛给宿主,由宿主去取窗口配置 |
102
+ | `aiLocation` | `string` | `''` | 这条提问来自哪里(建议「区域名 · 按钮/查询条件/字段名 + 类型 · 编辑器字段名」,如「区域信息 · 事件按钮 · 点击事件」,名称一律取 NAME)。组件只原样记进历史并显示在问题前,不解析其含义 |
103
+
104
+ ### Events
105
+
106
+ | 事件 | 参数 | 说明 |
107
+ | ---------------- | ----------------------------- | ------------------------ |
108
+ | `update:modelValue` | `string` | 代码变更(原有) |
109
+ | `ai-activate` | `string \| number` | AI 面板刚打开(携带 `aiWindowId`),宿主可趁此时预取上下文;`aiWindowId` 为空时不触发 |
110
+ | `ai-apply` | `{prompt, text}` | AI 结果已写入编辑器 |
111
+ | `ai-error` | `Error` | AI 请求失败 |
112
+
113
+ ### Expose
114
+
115
+ `openAi(prompt?)`、`closeAi()`、`stopAi()`、`getView()`。
116
+
117
+ ### 接入示例(非流式)
118
+
119
+ ```ts
120
+ import {YhCodemirror} from 'yh-codemirror';
121
+ import type {AiCodeEditRequest} from 'yh-codemirror';
122
+
123
+ const aiRequest: AiCodeEditRequest = async ({prompt, code, selectedCode, language, signal}) => {
124
+ const res = await fetch('/api/ai/code-edit', {
125
+ method: 'POST',
126
+ signal,
127
+ headers: {'Content-Type': 'application/json'},
128
+ body: JSON.stringify({prompt, code, selectedCode, language}),
129
+ });
130
+ if (!res.ok) {
131
+ throw new Error('AI 请求失败');
132
+ }
133
+ const data = await res.json();
134
+ // 返回「作用范围的完整替换文本」,组件负责写入
135
+ return data.code;
136
+ };
137
+ ```
138
+
139
+ ### 接入示例(流式 SSE)
140
+
141
+ ```ts
142
+ const aiRequest: AiCodeEditRequest = async ({prompt, selectedCode, signal, onChunk}) => {
143
+ const res = await fetch('/api/ai/code-edit/stream', {
144
+ method: 'POST',
145
+ signal,
146
+ headers: {'Content-Type': 'application/json'},
147
+ body: JSON.stringify({prompt, code: selectedCode}),
148
+ });
149
+ const reader = res.body!.getReader();
150
+ const decoder = new TextDecoder();
151
+ while (true) {
152
+ const {done, value} = await reader.read();
153
+ if (done) {
154
+ break;
155
+ }
156
+ onChunk?.(decoder.decode(value, {stream: true}));
157
+ }
158
+ // 走 onChunk 时返回值可为空,组件已实时写入编辑器
159
+ };
160
+ ```
161
+
162
+ > 提示:流式场景下 `onChunk` 传入的是增量片段,组件按累积文本整体覆盖作用区间,因此服务端无需自行做 diff。
package/ai-history.js CHANGED
@@ -1,40 +1,40 @@
1
- /**
2
- * AI 问答历史(模块级单例)。
3
- *
4
- * 同一页面里所有编辑器共用一份历史(未做分组),所以每条都要带上:
5
- * - location:这条提问来自哪里(宿主传入,如「区域名称 · 数据初始化」)
6
- * - time / prompt:时间与问题原文。
7
- */
8
- import {ref} from 'vue';
9
-
10
- /** 最多保留多少条(超出丢最旧的) */
11
- const MAX_HISTORY = 50;
12
-
13
- /**
14
- * @type {import('vue').Ref<Array<{id:number, time:string, prompt:string, scope:string, location:string, answer:string}>>}
15
- * 最新的排在最前面
16
- */
17
- export const aiHistory = ref([]);
18
-
19
- let seed = 0;
20
-
21
- const nowText = () => {
22
- const d = new Date();
23
- const pad = (n) => String(n).padStart(2, '0');
24
- return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(
25
- d.getSeconds()
26
- )}`;
27
- };
28
-
29
- /** 追加一条:{prompt, scope, location, answer} */
30
- export const pushAiHistory = ({prompt, scope = '', location = '', answer = ''}) => {
31
- if (!prompt && !answer) return;
32
- aiHistory.value = [
33
- {id: ++seed, time: nowText(), prompt, scope, location, answer},
34
- ...aiHistory.value,
35
- ].slice(0, MAX_HISTORY);
36
- };
37
-
38
- export const clearAiHistory = () => {
39
- aiHistory.value = [];
40
- };
1
+ /**
2
+ * AI 问答历史(模块级单例)。
3
+ *
4
+ * 同一页面里所有编辑器共用一份历史(未做分组),所以每条都要带上:
5
+ * - location:这条提问来自哪里(宿主传入,如「区域名称 · 数据初始化」)
6
+ * - time / prompt:时间与问题原文。
7
+ */
8
+ import {ref} from 'vue';
9
+
10
+ /** 最多保留多少条(超出丢最旧的) */
11
+ const MAX_HISTORY = 50;
12
+
13
+ /**
14
+ * @type {import('vue').Ref<Array<{id:number, time:string, prompt:string, scope:string, location:string, answer:string}>>}
15
+ * 最新的排在最前面
16
+ */
17
+ export const aiHistory = ref([]);
18
+
19
+ let seed = 0;
20
+
21
+ const nowText = () => {
22
+ const d = new Date();
23
+ const pad = (n) => String(n).padStart(2, '0');
24
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(
25
+ d.getSeconds()
26
+ )}`;
27
+ };
28
+
29
+ /** 追加一条:{prompt, scope, location, answer} */
30
+ export const pushAiHistory = ({prompt, scope = '', location = '', answer = ''}) => {
31
+ if (!prompt && !answer) return;
32
+ aiHistory.value = [
33
+ {id: ++seed, time: nowText(), prompt, scope, location, answer},
34
+ ...aiHistory.value,
35
+ ].slice(0, MAX_HISTORY);
36
+ };
37
+
38
+ export const clearAiHistory = () => {
39
+ aiHistory.value = [];
40
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * 方法参数补全:给「某个方法的第 N 个字符串参数」提供候选值。
3
+ *
4
+ * 组件不认识任何业务概念——哪些方法、第几个参数、候选取自哪里,全部由宿主通过
5
+ * argCompletions 传进来。vuePath、字段名、枚举值都走同一条通道,加新方法只需加一条配置。
6
+ */
7
+
8
+ /** 候选弹出后继续输入这些字符不必重新查询,CodeMirror 本地过滤即可 */
9
+ const VALID_FOR = /^[\w$./\\-]*$/;
10
+
11
+ const IDENT_RE = /[\w$]/;
12
+
13
+ /** 取 `(` 前面的调用名(兼容 this.openModal / a.b.openModal 这类写法) */
14
+ function readCallee(text, openParen) {
15
+ let i = openParen - 1;
16
+ while (i >= 0 && /\s/.test(text[i])) {
17
+ i--;
18
+ }
19
+ const end = i;
20
+ while (i >= 0 && IDENT_RE.test(text[i])) {
21
+ i--;
22
+ }
23
+ return i === end ? '' : text.slice(i + 1, end + 1);
24
+ }
25
+
26
+ /**
27
+ * 从文档开头扫到光标,定位光标所在的字符串字面量属于哪个调用的第几个实参。
28
+ * 之所以全量扫而不是只看当前行:参数多时手写换行很常见
29
+ * (showModal( 换行 "报表", 换行 "basic/xxx.vue")。
30
+ *
31
+ * @param {string} text 文档全文(或 0 到光标的片段)
32
+ * @param {number} pos 光标偏移
33
+ * @returns {null | {fn: string, arg: number, from: number}}
34
+ * fn 调用名;arg 参数下标(0 基);from 字符串内容起点(引号后一位)
35
+ */
36
+ export function locateStringArg(text, pos) {
37
+ // 括号栈:name 非空表示是调用括号,comma 记录它已过的实参个数
38
+ const stack = [];
39
+ let inString = false;
40
+ let quote = '';
41
+ let stringStart = -1;
42
+ let lineComment = false;
43
+ let blockComment = false;
44
+
45
+ for (let i = 0; i < pos; i++) {
46
+ const c = text[i];
47
+
48
+ if (lineComment) {
49
+ if (c === '\n') {
50
+ lineComment = false;
51
+ }
52
+ continue;
53
+ }
54
+ if (blockComment) {
55
+ if (c === '*' && text[i + 1] === '/') {
56
+ blockComment = false;
57
+ i++;
58
+ }
59
+ continue;
60
+ }
61
+ if (inString) {
62
+ if (c === '\\') {
63
+ // 转义字符:连同下一个字符一起跳过,否则 "a\"b" 会被当成提前闭合
64
+ i++;
65
+ continue;
66
+ }
67
+ if (c === quote) {
68
+ inString = false;
69
+ quote = '';
70
+ stringStart = -1;
71
+ }
72
+ continue;
73
+ }
74
+ if (c === '/' && text[i + 1] === '/') {
75
+ lineComment = true;
76
+ i++;
77
+ continue;
78
+ }
79
+ if (c === '/' && text[i + 1] === '*') {
80
+ blockComment = true;
81
+ i++;
82
+ continue;
83
+ }
84
+ if (c === '"' || c === "'" || c === '`') {
85
+ inString = true;
86
+ quote = c;
87
+ stringStart = i;
88
+ continue;
89
+ }
90
+ if (c === '(') {
91
+ stack.push({name: readCallee(text, i), comma: 0});
92
+ continue;
93
+ }
94
+ if (c === '[' || c === '{') {
95
+ // 只压栈不认名:多维数组 / 对象字面量里的逗号不该算到外层调用头上
96
+ stack.push({name: '', comma: 0});
97
+ continue;
98
+ }
99
+ if (c === ')' || c === ']' || c === '}') {
100
+ stack.pop();
101
+ continue;
102
+ }
103
+ if (c === ',' && stack.length) {
104
+ stack[stack.length - 1].comma++;
105
+ }
106
+ }
107
+
108
+ if (!inString || stringStart < 0) {
109
+ return null;
110
+ }
111
+ const top = stack[stack.length - 1];
112
+ if (!top || !top.name) {
113
+ return null;
114
+ }
115
+ return {fn: top.name, arg: top.comma, from: stringStart + 1};
116
+ }
117
+
118
+ /** 把宿主给的候选统一成 CodeMirror 的选项结构 */
119
+ function toOptions(item) {
120
+ const detail = item.detail;
121
+ return (item.options || []).map((opt) =>
122
+ typeof opt === 'string'
123
+ ? detail
124
+ ? {label: opt, type: 'text', detail}
125
+ : {label: opt, type: 'text'}
126
+ : {label: opt.label, type: 'text', detail: opt.detail || detail}
127
+ );
128
+ }
129
+
130
+ /**
131
+ * 生成 CodeMirror 补全源。
132
+ * @param {() => Array<{fn: string, arg: number, options: any[], detail?: string}>} getItems
133
+ * 每次触发实时读取,宿主异步就绪也生效
134
+ */
135
+ export function createArgCompletion(getItems) {
136
+ return (context) => {
137
+ const items = (typeof getItems === 'function' ? getItems() : getItems) || [];
138
+ // 宿主没配任何候选:连文档都不扫
139
+ if (!items.length) {
140
+ return null;
141
+ }
142
+ const hit = locateStringArg(context.state.doc.sliceString(0, context.pos), context.pos);
143
+ if (!hit) {
144
+ return null;
145
+ }
146
+ const item = items.find((it) => it.fn === hit.fn && it.arg === hit.arg);
147
+ if (!item) {
148
+ return null;
149
+ }
150
+ const options = toOptions(item);
151
+ if (!options.length) {
152
+ return null;
153
+ }
154
+ return {from: hit.from, options, validFor: VALID_FOR};
155
+ };
156
+ }
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import YhCodemirror from './CodeMirror.vue';
2
2
  export {YhCodemirror};
3
3
  export type {AiCodeEditParams, AiCodeEditRequest} from './util';
4
+ export {createArgCompletion, locateStringArg} from './arg-completion';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yh-codemirror",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "wraper codemirror with vue3",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@codemirror/autocomplete": "^6.16.3",
17
+ "@codemirror/commands": "^6.0.0",
17
18
  "@codemirror/lang-css": "^6.2.1",
18
19
  "@codemirror/lang-java": "^6.0.1",
19
20
  "@codemirror/lang-javascript": "^6.2.2",
@@ -23,7 +24,7 @@
23
24
  "@codemirror/language": "^6.10.2",
24
25
  "@codemirror/view": "^6.28.2",
25
26
  "@lezer/highlight": "^1.2.0",
26
- "cm6-theme-solarized-dark": "^0.1.0",
27
+ "cm6-theme-solarized-dark": "^0.2.0",
27
28
  "codemirror": "^6.0.1",
28
29
  "js-beautify": "^1.15.1"
29
30
  },
@@ -1,19 +0,0 @@
1
- # 2026-09-28
2
-
3
- ## yh-codemirror 1.0.7 接入 AI 提示代码编辑
4
-
5
- - 设计取舍:该包必须保持零业务依赖(deps 仅 codemirror 系列 + js-beautify,peer 仅 vue/element-plus),
6
- 因此**不直接依赖 yh-ai**(会拖入 axios/yh-client/yh-pub),改为宿主通过 `aiRequest` 回调注入 AI 能力。
7
- 传入 `aiRequest` 即启用,未传时行为与旧版完全一致(向后兼容)。
8
- - 实现要点:
9
- - 作用范围:有选区→选区;无选区→按 `aiScope`(默认 document)整篇或光标插入。
10
- - 写回用 `view.dispatch({changes:{from,to,insert}})`,保留撤销栈;流式时区间 to 随写入长度前移。
11
- - 流式:宿主调 `onChunk(chunk)` 增量返回,组件累积后整体覆盖区间;非流式则取返回值字符串。
12
- - `AbortController` + signal 支持中断(停止按钮 / Esc / 卸载)。
13
- - 自动剥离模型输出的 ```lang 围栏。
14
- - 快捷键用 `keymap.of`(注意:`keymap` 不从 `codemirror` 导出,必须从 `@codemirror/view` 导入)。
15
- - 新增事件 `ai-apply` / `ai-error`,expose `openAi/closeAi/stopAi/getView`。
16
- - 新增 README.md(接入示例:非流式 + SSE 流式)、CHANGELOG.md;version 1.0.6 → 1.0.7。
17
- - 验证:临时 .smoke 工程用 vite 4 + @vitejs/plugin-vue 构建通过(1663 modules)。
18
- 注意:root node_modules 的 vue 是 3.2.47,与 element-plus 内部 @vueuse 不兼容(hasInjectionContext 缺失),
19
- **冒烟构建要把 vue alias 指向 web-new/node_modules/vue(3.5.13)**。
package/CHANGELOG.md DELETED
@@ -1,51 +0,0 @@
1
- # CHANGELOG
2
-
3
- ## 1.0.10
4
-
5
- - 2026-10-09 15:51 feature: `language` 新增 `json`;未知语言回落 `javascript`
6
- - slw 的图表配置编辑器要用 JSON 语法高亮,之前只能靠宿主额外传 extensions
7
- - `getCodeLanguagePlugin` 原来直接 `plugins[language]()`,未知语言(含 undefined)返回 undefined,
8
- CM6 会抛 `Unrecognized extension value in extension set`,改为回落 javascript
9
-
10
- ## 1.0.7
11
-
12
- - 2026-10-08 15:06 feature: 新增 `aiWindowId` 入参与 `ai-activate` 事件
13
- - AI 面板打开时把 `aiWindowId` 通过 `ai-activate` 抛给宿主,宿主可趁用户还没打字时预取上下文
14
- - 组件本身不认识任何业务接口(保持零依赖),窗口配置怎么取、怎么缓存完全由宿主决定
15
- - `aiWindowId` 为空时不触发事件
16
- - 2026-09-30 18:54 feature: 新增 `aiPanelSize` 尺寸档位(`'normal' | 'large'`)
17
- - 放大场景(window-form 的「放大」抽屉)传 `large`:面板内边距 6/8 → 10/12、字号 12 → 14px、
18
- 输入框 2 行 → 4 行、历史面板 max-height 62% → 72%(代码区 170px → 300px)
19
- - 抽屉里的编辑区撑满整个抽屉,AI 面板还是 12px / 2 行会显得很局促
20
- - 2026-09-30 18:45 feature: 新增「历史问答」查看入口(AI 输入框右上角「历史」)
21
- - 每次 AI 成功写入后记录 `{时间, 问题, 作用范围, 回答}`,列表可展开查看完整问答、一键复制回答、清空
22
- - 历史存在新增的 `ai-history.js` **模块级单例**(上限 50 条,最新在前);
23
- 不放组件内是因为配置面板里的编辑器会随切换配置项反复卸载重建,组件 ref 一卸载历史就没了
24
- - 2026-09-30 18:26 fix: 选中区被当前行整块盖住(真正的根因是**绘制层级**,不是配色)
25
- - CM6 的选区画在 `.cm-selectionLayer` 上:`drawSelection` 内部 `above:false` → z-index 取 `-1-pos`(负数),
26
- 它和 `.cm-content` 同在 `.cm-scroller`(`position:relative; z-index:0`)这个层叠上下文里
27
- - 按 CSS 绘制顺序「负 z-index 后代 → in-flow 块 → 行内内容」,`.cm-line` 作为普通 in-flow 块
28
- **画在选区层之上**;只要给 `.cm-line` 设不透明背景,选区就被完全遮住
29
- - baseTheme 自带的 `&light .cm-activeLine:#cceeff44` / `&dark:#99eeff33` 是半透明的才不会遮,
30
- solarizedDark 的 `.cm-activeLine:#173541` 和它的 `selection:#173541` 一样是不透明的同色 —— 这才是最初「看不出来」的根源
31
- - 解法:当前行高亮挪到 `.cm-line.cm-activeLine::before`(`z-index:-100`),
32
- 同时把 `.cm-activeLine` 自身清成 `transparent`;绘制顺序变成「当前行底(-100) → 选区(-2) → 文字」
33
- - 2026-09-28 10:04 feature: 新增 AI 提示代码编辑能力(回调注入 `aiRequest`,不引入任何 AI 服务依赖)
34
- - 支持选区 / 整篇 / 光标插入三种作用范围(`aiScope`)
35
- - 支持流式回写(`onChunk` 实时写入编辑器)
36
- - 支持中断(`AbortSignal` + 停止按钮 / Esc)
37
- - 新增 `ai-apply`、`ai-error` 事件与 `openAi / closeAi / stopAi / getView` 暴露方法
38
- - 新增快捷键 `Mod-i` 唤起 AI 输入框(`aiHotkey` 可配)
39
- - 回写走 dispatch,保留撤销栈;自动剥离 ```lang 代码围栏
40
- - 2026-09-28 10:04 docs: 新增 README.md 说明 AI 接入方式(非流式 / SSE 流式示例)
41
- - 2026-09-30 18:08 fix: 拖选/双击选词仍然和当前行同色(上一版权重不够)
42
- - CM 6.39 的 baseTheme 用 `&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground`
43
- (5 个类 + 子代选择器)给了默认色 `#d7d4f0` / `#233`,权重高于组件规则,堆类名盖不住
44
- - 选区色改带 `!important`(不跟它的 DOM 结构绑死),当前行仍用选择器权重覆盖
45
- - 补回浅色 `.cm-selectionMatch`(双击选词后其它同名 occurrence),改用暖色 `#ffe9c2` 与蓝色选区区分
46
- - solarizedDark 的 `selection` 与 `activeLine` 都是 `#173541`,浅色主题又是 `#b3d8ff` / `#e8f4ff` 同一蓝色系
47
- - 改为「当前行 = 中性色,选中 = 蓝色」,并区分聚焦/失焦两档选中色
48
- - 配色统一挪到组件 `<style>`(带 `.cm-editor` / `.cm-editor.cm-focused` 前缀,权重高于外部主题),
49
- `lightTheme` 里不再重复维护
50
- - 2026-09-30 17:54 feature: 编辑器未聚焦时隐藏右下角功能按钮栏(`.editor-operation-btn.is-hidden`)
51
- - 按钮栏加 `@mousedown.prevent`,避免点按钮瞬间编辑器失焦导致按钮隐藏、click 落空