yh-codemirror 1.0.8 → 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 +81 -30
- package/README.md +162 -102
- package/ai-history.js +40 -39
- package/arg-completion.js +156 -0
- package/index.ts +1 -0
- package/package.json +4 -2
- package/util.ts +4 -1
- package/.workbuddy/memory/2026-09-28.md +0 -19
- package/CHANGELOG.md +0 -44
package/CodeMirror.vue
CHANGED
|
@@ -101,6 +101,12 @@
|
|
|
101
101
|
class="ai-history-q"
|
|
102
102
|
@click="toggleHistoryItem(item.id)">
|
|
103
103
|
<span class="ai-history-time">{{ item.time }}</span>
|
|
104
|
+
<span
|
|
105
|
+
v-if="item.location"
|
|
106
|
+
class="ai-history-loc"
|
|
107
|
+
:title="item.location"
|
|
108
|
+
>{{ item.location }}</span
|
|
109
|
+
>
|
|
104
110
|
<span class="ai-history-text">{{ item.prompt }}</span>
|
|
105
111
|
<span class="ai-history-toggle">{{ expandedHistoryId === item.id ? '收起' : '展开' }}</span>
|
|
106
112
|
</div>
|
|
@@ -138,10 +144,10 @@
|
|
|
138
144
|
@click="openAi()"
|
|
139
145
|
>AI</span
|
|
140
146
|
>
|
|
141
|
-
<i
|
|
147
|
+
<!-- <i
|
|
142
148
|
title="全屏"
|
|
143
149
|
class="iconfont icon-fullscreen"
|
|
144
|
-
@click="fullscreen"></i>
|
|
150
|
+
@click="fullscreen"></i> -->
|
|
145
151
|
<i
|
|
146
152
|
title="美化"
|
|
147
153
|
class="iconfont icon-magic-stick"
|
|
@@ -168,6 +174,7 @@ import {ref, onMounted, onBeforeUnmount, nextTick, toRaw} from 'vue';
|
|
|
168
174
|
import type {Extension} from 'codemirror';
|
|
169
175
|
import {EditorView, basicSetup} from 'codemirror';
|
|
170
176
|
import {ViewUpdate, keymap} from '@codemirror/view';
|
|
177
|
+
import {indentWithTab} from '@codemirror/commands';
|
|
171
178
|
import {EditorState, EditorSelection} from '@codemirror/state';
|
|
172
179
|
import {javascriptLanguage} from '@codemirror/lang-javascript';
|
|
173
180
|
import {solarizedDark} from 'cm6-theme-solarized-dark';
|
|
@@ -175,6 +182,7 @@ import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
|
|
|
175
182
|
import {tags} from '@lezer/highlight';
|
|
176
183
|
import {getCodeLanguagePlugin} from './util';
|
|
177
184
|
import type {AiCodeEditParams, AiCodeEditRequest} from './util';
|
|
185
|
+
import {createArgCompletion} from './arg-completion';
|
|
178
186
|
import {aiHistory, pushAiHistory, clearAiHistory} from './ai-history';
|
|
179
187
|
import {watch, computed} from 'vue';
|
|
180
188
|
// @ts-ignore
|
|
@@ -218,8 +226,7 @@ const props = withDefaults(
|
|
|
218
226
|
/** 编辑器主题:dark(默认)或 light */
|
|
219
227
|
theme?: 'dark' | 'light';
|
|
220
228
|
/**
|
|
221
|
-
* AI 面板尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 'large'
|
|
222
|
-
* 否则 12px 字号 + 2 行输入框在大面板里会显得很局促。
|
|
229
|
+
* AI 面板尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 'large'
|
|
223
230
|
*/
|
|
224
231
|
aiPanelSize?: 'normal' | 'large';
|
|
225
232
|
/**
|
|
@@ -228,6 +235,26 @@ const props = withDefaults(
|
|
|
228
235
|
* 由宿主去拉配置;不传就不触发该事件。
|
|
229
236
|
*/
|
|
230
237
|
aiWindowId?: string | number;
|
|
238
|
+
/**
|
|
239
|
+
* 这条提问来自哪里(宿主传入,如「区域名称 · 数据初始化」)。
|
|
240
|
+
* 组件只负责原样记进历史并在问题前显示,不解析它的含义
|
|
241
|
+
*/
|
|
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
|
+
}>;
|
|
231
258
|
}>(),
|
|
232
259
|
{
|
|
233
260
|
modelValue: '',
|
|
@@ -239,6 +266,8 @@ const props = withDefaults(
|
|
|
239
266
|
theme: 'dark',
|
|
240
267
|
aiPanelSize: 'normal',
|
|
241
268
|
aiWindowId: '',
|
|
269
|
+
aiLocation: '',
|
|
270
|
+
argCompletions: () => [],
|
|
242
271
|
}
|
|
243
272
|
);
|
|
244
273
|
|
|
@@ -487,7 +516,7 @@ const submitAi = async () => {
|
|
|
487
516
|
view.focus();
|
|
488
517
|
const appliedText = view.state.sliceDoc(range.from, range.to);
|
|
489
518
|
// 记录历史问答(含 AI 最终写入编辑器的完整文本,撤销了也能在这里找回原文)
|
|
490
|
-
pushAiHistory({prompt, scope: aiScopeText.value, answer: appliedText});
|
|
519
|
+
pushAiHistory({prompt, scope: aiScopeText.value, location: props.aiLocation, answer: appliedText});
|
|
491
520
|
emits('ai-apply', {prompt, text: appliedText});
|
|
492
521
|
ElMessage({
|
|
493
522
|
message: 'AI 修改已应用',
|
|
@@ -566,6 +595,10 @@ const onEditorBtnClick = (type) => {
|
|
|
566
595
|
onMounted(() => {
|
|
567
596
|
const extensions: Extension[] = [
|
|
568
597
|
basicSetup,
|
|
598
|
+
// Tab 缩进必须显式加:CodeMirror 6 为了不让键盘用户被困在编辑区,默认不把 Tab
|
|
599
|
+
// 绑给缩进,事件冒泡到浏览器就变成「跳到下一个表单项」。
|
|
600
|
+
// 缩进宽度用默认 indentUnit(2 空格),与 codeBeautifyConfig.indent_size 一致。
|
|
601
|
+
keymap.of([indentWithTab]),
|
|
569
602
|
props.theme === 'light' ? lightThemeExtension : solarizedDark,
|
|
570
603
|
EditorView.lineWrapping,
|
|
571
604
|
EditorView.updateListener.of((v: ViewUpdate) => {
|
|
@@ -602,6 +635,16 @@ onMounted(() => {
|
|
|
602
635
|
}
|
|
603
636
|
}
|
|
604
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
|
+
|
|
605
648
|
if (aiEnabled.value) {
|
|
606
649
|
extensions.push(
|
|
607
650
|
keymap.of([
|
|
@@ -662,19 +705,30 @@ defineExpose({
|
|
|
662
705
|
&.is-light {
|
|
663
706
|
background-color: #fff;
|
|
664
707
|
.cm-editor {
|
|
708
|
+
border: 1px solid var(--el-border-color-light);
|
|
709
|
+
border-radius: 8px;
|
|
710
|
+
cursor: text;
|
|
711
|
+
width: 100%;
|
|
712
|
+
overflow: auto;
|
|
713
|
+
.cm-gutters.cm-gutters-before {
|
|
714
|
+
background-color: #fafcff;
|
|
715
|
+
}
|
|
665
716
|
.cm-line.cm-activeLine::before {
|
|
666
|
-
background-color: #
|
|
717
|
+
background-color: #f1f7ff;
|
|
667
718
|
}
|
|
668
719
|
.cm-activeLineGutter {
|
|
669
|
-
background-color: #
|
|
720
|
+
background-color: #f1f7ff;
|
|
670
721
|
}
|
|
671
722
|
.cm-selectionBackground {
|
|
672
723
|
background-color: #d4e7f9 !important;
|
|
673
724
|
}
|
|
674
|
-
&.cm-focused
|
|
675
|
-
|
|
725
|
+
&.cm-focused {
|
|
726
|
+
outline: none;
|
|
727
|
+
box-shadow: 1px 1px 8px #b1c7db;
|
|
728
|
+
.cm-selectionBackground {
|
|
729
|
+
background-color: #b3d8ff !important;
|
|
730
|
+
}
|
|
676
731
|
}
|
|
677
|
-
// 双击选词后其它同名 occurrence(highlightSelectionMatches):用暖色,和蓝色选区、中性当前行都区分开
|
|
678
732
|
.cm-selectionMatch {
|
|
679
733
|
background-color: #ffe9c2;
|
|
680
734
|
}
|
|
@@ -745,6 +799,10 @@ defineExpose({
|
|
|
745
799
|
.ai-history-q {
|
|
746
800
|
color: #333;
|
|
747
801
|
}
|
|
802
|
+
.ai-history-loc {
|
|
803
|
+
background: rgba(64, 158, 255, 0.14);
|
|
804
|
+
color: #1a6fc4;
|
|
805
|
+
}
|
|
748
806
|
.ai-history-label .ai-history-scope {
|
|
749
807
|
color: #606266;
|
|
750
808
|
}
|
|
@@ -770,16 +828,6 @@ defineExpose({
|
|
|
770
828
|
line-height: 25px;
|
|
771
829
|
font-family: 'Fira Code', 'Roboto Mono', Consolas, 'Microsoft Yahei';
|
|
772
830
|
}
|
|
773
|
-
/* 当前行高亮 vs 选中区,必须一眼能区分 —— 这里真正的坑不是配色而是**绘制层级**:
|
|
774
|
-
CM6 的选区画在 .cm-selectionLayer 上(drawSelection 内部 above:false,z-index 取 -1-pos,是负数),
|
|
775
|
-
它和 .cm-content 同在 .cm-scroller 这个层叠上下文里;而 .cm-line 是普通 in-flow 块,
|
|
776
|
-
按 CSS 绘制顺序「负 z-index 后代 → in-flow 块 → 行内内容」,**in-flow 块画在选区层之上**。
|
|
777
|
-
所以给 .cm-line 设不透明背景会把选区整块盖掉 —— baseTheme 自带的
|
|
778
|
-
&light .cm-activeLine: #cceeff44 / &dark: #99eeff33 是半透明的,才不会盖住,
|
|
779
|
-
之前改成了不透明色,选区就被糊掉了。
|
|
780
|
-
解法:把当前行高亮挪到 ::before 上并压到 z-index:-100,绘制顺序变成
|
|
781
|
-
当前行底(-100) → 选区层(-2) → 文字
|
|
782
|
-
选中区永远压在当前行高亮之上,同时仍然在文字之下。 */
|
|
783
831
|
.cm-line.cm-activeLine::before {
|
|
784
832
|
content: '';
|
|
785
833
|
position: absolute;
|
|
@@ -791,19 +839,12 @@ defineExpose({
|
|
|
791
839
|
pointer-events: none;
|
|
792
840
|
background-color: #173541;
|
|
793
841
|
}
|
|
794
|
-
// 当前行高亮改由上面的 ::before 承载;这里必须把主题自带的**不透明**底色清掉,
|
|
795
|
-
// 否则它作为 in-flow 块的背景仍然会盖住选区层(solarizedDark 的 .cm-activeLine 就是 #173541,
|
|
796
|
-
// 和它的 selection 同色,这正是最初「选中区看不出来」的根源)。
|
|
797
842
|
.cm-activeLine {
|
|
798
843
|
background-color: transparent;
|
|
799
844
|
}
|
|
800
845
|
.cm-activeLineGutter {
|
|
801
846
|
background-color: #173541;
|
|
802
847
|
}
|
|
803
|
-
/* 选区色必须带 !important:CM 6.39 的 baseTheme 把默认色写成了
|
|
804
|
-
"&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground"
|
|
805
|
-
这种「5 个类 + 子代选择器」,权重高于本组件、也高于 solarizedDark 自带的主题,
|
|
806
|
-
光堆类名盖不住;用 !important 也不用跟它的 DOM 结构绑死。 */
|
|
807
848
|
.cm-selectionBackground {
|
|
808
849
|
/* 失焦:压暗一档 */
|
|
809
850
|
background-color: #204a5a !important;
|
|
@@ -1023,6 +1064,19 @@ defineExpose({
|
|
|
1023
1064
|
font-size: 11px;
|
|
1024
1065
|
opacity: 0.55;
|
|
1025
1066
|
}
|
|
1067
|
+
.ai-history-loc {
|
|
1068
|
+
flex: none;
|
|
1069
|
+
max-width: 45%;
|
|
1070
|
+
padding: 0 5px;
|
|
1071
|
+
border-radius: 2px;
|
|
1072
|
+
background: rgba(64, 158, 255, 0.22);
|
|
1073
|
+
color: #3070af;
|
|
1074
|
+
font-size: 11px;
|
|
1075
|
+
line-height: 16px;
|
|
1076
|
+
overflow: hidden;
|
|
1077
|
+
white-space: nowrap;
|
|
1078
|
+
text-overflow: ellipsis;
|
|
1079
|
+
}
|
|
1026
1080
|
.ai-history-text {
|
|
1027
1081
|
flex: 1;
|
|
1028
1082
|
overflow: hidden;
|
|
@@ -1068,9 +1122,6 @@ defineExpose({
|
|
|
1068
1122
|
word-break: break-all;
|
|
1069
1123
|
}
|
|
1070
1124
|
}
|
|
1071
|
-
/* 放大场景(window-form 的「放大」抽屉):编辑区撑满整个抽屉,AI 面板仍是 12px / 2 行会显得很局促,
|
|
1072
|
-
这里整体提一档字号、加两行输入高度、面板内边距与历史面板高度同步放大。
|
|
1073
|
-
选择器带 .is-ai-large 是为了权重高于上面 .is-light 里的同名规则,不依赖书写顺序。 */
|
|
1074
1125
|
&.is-ai-large {
|
|
1075
1126
|
.ai-prompt-panel,
|
|
1076
1127
|
.ai-history-panel {
|
package/README.md
CHANGED
|
@@ -1,102 +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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,39 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI 问答历史(模块级单例)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yh-codemirror",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "wraper codemirror with vue3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,15 +14,17 @@
|
|
|
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",
|
|
21
|
+
"@codemirror/lang-json": "^6.0.1",
|
|
20
22
|
"@codemirror/lang-sql": "^6.7.0",
|
|
21
23
|
"@codemirror/lang-html": "^6.4.9",
|
|
22
24
|
"@codemirror/language": "^6.10.2",
|
|
23
25
|
"@codemirror/view": "^6.28.2",
|
|
24
26
|
"@lezer/highlight": "^1.2.0",
|
|
25
|
-
"cm6-theme-solarized-dark": "^0.
|
|
27
|
+
"cm6-theme-solarized-dark": "^0.2.0",
|
|
26
28
|
"codemirror": "^6.0.1",
|
|
27
29
|
"js-beautify": "^1.15.1"
|
|
28
30
|
},
|
package/util.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { java } from "@codemirror/lang-java";
|
|
|
3
3
|
import { css } from "@codemirror/lang-css";
|
|
4
4
|
import { sql } from "@codemirror/lang-sql";
|
|
5
5
|
import { html } from "@codemirror/lang-html";
|
|
6
|
+
import { json } from "@codemirror/lang-json";
|
|
6
7
|
|
|
7
8
|
const plugins = {
|
|
8
9
|
javascript: () => javascript(),
|
|
@@ -10,6 +11,7 @@ const plugins = {
|
|
|
10
11
|
css: () => css(),
|
|
11
12
|
java: () => java(),
|
|
12
13
|
html: () => html(),
|
|
14
|
+
json: () => json(),
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -39,5 +41,6 @@ export type AiCodeEditRequest = (
|
|
|
39
41
|
) => Promise<string | void | {text?: string; code?: string; content?: string}>;
|
|
40
42
|
|
|
41
43
|
export const getCodeLanguagePlugin = function (language) {
|
|
42
|
-
|
|
44
|
+
// 未知语言(含 undefined)回落 javascript:直接取会拿到 undefined,CM6 会抛 extension 错误
|
|
45
|
+
return (plugins[language] || plugins.javascript)();
|
|
43
46
|
};
|
|
@@ -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,44 +0,0 @@
|
|
|
1
|
-
# CHANGELOG
|
|
2
|
-
|
|
3
|
-
## 1.0.7
|
|
4
|
-
|
|
5
|
-
- 2026-10-08 15:06 feature: 新增 `aiWindowId` 入参与 `ai-activate` 事件
|
|
6
|
-
- AI 面板打开时把 `aiWindowId` 通过 `ai-activate` 抛给宿主,宿主可趁用户还没打字时预取上下文
|
|
7
|
-
- 组件本身不认识任何业务接口(保持零依赖),窗口配置怎么取、怎么缓存完全由宿主决定
|
|
8
|
-
- `aiWindowId` 为空时不触发事件
|
|
9
|
-
- 2026-09-30 18:54 feature: 新增 `aiPanelSize` 尺寸档位(`'normal' | 'large'`)
|
|
10
|
-
- 放大场景(window-form 的「放大」抽屉)传 `large`:面板内边距 6/8 → 10/12、字号 12 → 14px、
|
|
11
|
-
输入框 2 行 → 4 行、历史面板 max-height 62% → 72%(代码区 170px → 300px)
|
|
12
|
-
- 抽屉里的编辑区撑满整个抽屉,AI 面板还是 12px / 2 行会显得很局促
|
|
13
|
-
- 2026-09-30 18:45 feature: 新增「历史问答」查看入口(AI 输入框右上角「历史」)
|
|
14
|
-
- 每次 AI 成功写入后记录 `{时间, 问题, 作用范围, 回答}`,列表可展开查看完整问答、一键复制回答、清空
|
|
15
|
-
- 历史存在新增的 `ai-history.js` **模块级单例**(上限 50 条,最新在前);
|
|
16
|
-
不放组件内是因为配置面板里的编辑器会随切换配置项反复卸载重建,组件 ref 一卸载历史就没了
|
|
17
|
-
- 2026-09-30 18:26 fix: 选中区被当前行整块盖住(真正的根因是**绘制层级**,不是配色)
|
|
18
|
-
- CM6 的选区画在 `.cm-selectionLayer` 上:`drawSelection` 内部 `above:false` → z-index 取 `-1-pos`(负数),
|
|
19
|
-
它和 `.cm-content` 同在 `.cm-scroller`(`position:relative; z-index:0`)这个层叠上下文里
|
|
20
|
-
- 按 CSS 绘制顺序「负 z-index 后代 → in-flow 块 → 行内内容」,`.cm-line` 作为普通 in-flow 块
|
|
21
|
-
**画在选区层之上**;只要给 `.cm-line` 设不透明背景,选区就被完全遮住
|
|
22
|
-
- baseTheme 自带的 `&light .cm-activeLine:#cceeff44` / `&dark:#99eeff33` 是半透明的才不会遮,
|
|
23
|
-
solarizedDark 的 `.cm-activeLine:#173541` 和它的 `selection:#173541` 一样是不透明的同色 —— 这才是最初「看不出来」的根源
|
|
24
|
-
- 解法:当前行高亮挪到 `.cm-line.cm-activeLine::before`(`z-index:-100`),
|
|
25
|
-
同时把 `.cm-activeLine` 自身清成 `transparent`;绘制顺序变成「当前行底(-100) → 选区(-2) → 文字」
|
|
26
|
-
- 2026-09-28 10:04 feature: 新增 AI 提示代码编辑能力(回调注入 `aiRequest`,不引入任何 AI 服务依赖)
|
|
27
|
-
- 支持选区 / 整篇 / 光标插入三种作用范围(`aiScope`)
|
|
28
|
-
- 支持流式回写(`onChunk` 实时写入编辑器)
|
|
29
|
-
- 支持中断(`AbortSignal` + 停止按钮 / Esc)
|
|
30
|
-
- 新增 `ai-apply`、`ai-error` 事件与 `openAi / closeAi / stopAi / getView` 暴露方法
|
|
31
|
-
- 新增快捷键 `Mod-i` 唤起 AI 输入框(`aiHotkey` 可配)
|
|
32
|
-
- 回写走 dispatch,保留撤销栈;自动剥离 ```lang 代码围栏
|
|
33
|
-
- 2026-09-28 10:04 docs: 新增 README.md 说明 AI 接入方式(非流式 / SSE 流式示例)
|
|
34
|
-
- 2026-09-30 18:08 fix: 拖选/双击选词仍然和当前行同色(上一版权重不够)
|
|
35
|
-
- CM 6.39 的 baseTheme 用 `&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground`
|
|
36
|
-
(5 个类 + 子代选择器)给了默认色 `#d7d4f0` / `#233`,权重高于组件规则,堆类名盖不住
|
|
37
|
-
- 选区色改带 `!important`(不跟它的 DOM 结构绑死),当前行仍用选择器权重覆盖
|
|
38
|
-
- 补回浅色 `.cm-selectionMatch`(双击选词后其它同名 occurrence),改用暖色 `#ffe9c2` 与蓝色选区区分
|
|
39
|
-
- solarizedDark 的 `selection` 与 `activeLine` 都是 `#173541`,浅色主题又是 `#b3d8ff` / `#e8f4ff` 同一蓝色系
|
|
40
|
-
- 改为「当前行 = 中性色,选中 = 蓝色」,并区分聚焦/失焦两档选中色
|
|
41
|
-
- 配色统一挪到组件 `<style>`(带 `.cm-editor` / `.cm-editor.cm-focused` 前缀,权重高于外部主题),
|
|
42
|
-
`lightTheme` 里不再重复维护
|
|
43
|
-
- 2026-09-30 17:54 feature: 编辑器未聚焦时隐藏右下角功能按钮栏(`.editor-operation-btn.is-hidden`)
|
|
44
|
-
- 按钮栏加 `@mousedown.prevent`,避免点按钮瞬间编辑器失焦导致按钮隐藏、click 落空
|