yh-codemirror 1.0.6 → 1.0.8
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/.workbuddy/memory/2026-09-28.md +19 -0
- package/CHANGELOG.md +44 -0
- package/CodeMirror.vue +1175 -283
- package/README.md +102 -0
- package/ai-history.js +39 -0
- package/index.ts +3 -0
- package/package.json +34 -26
- package/util.ts +43 -17
package/README.md
ADDED
|
@@ -0,0 +1,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
|
+
## AI 提示编辑
|
|
17
|
+
|
|
18
|
+
组件本身**不内置任何 AI 服务**(保持零业务依赖),由宿主通过 `aiRequest` 注入请求实现。传入 `aiRequest` 即自动启用:
|
|
19
|
+
|
|
20
|
+
- 编辑器右下角出现 `AI` 入口按钮
|
|
21
|
+
- 快捷键 `Ctrl+I`(Mac `Cmd+I`,可通过 `aiHotkey` 改)唤起输入框
|
|
22
|
+
- 有选区 → 只改写选中代码;无选区 → 按 `aiScope` 作用于整个文档或光标处插入
|
|
23
|
+
- `Enter` 发送、`Esc` 取消;请求中按钮变「停止」,可中断
|
|
24
|
+
- 支持流式:调用 `onChunk` 逐段返回,编辑器实时写入;非流式则直接返回完整替换文本
|
|
25
|
+
- 自动剥离模型常见的 ` ```lang ... ``` ` 包裹
|
|
26
|
+
- 通过 dispatch 写入,保留 CodeMirror 撤销栈,可 `Ctrl+Z` 回退
|
|
27
|
+
- **历史问答**:输入框右上角「历史」可查看本轮会话问过的问题与 AI 的完整回答(可展开、可复制、可清空)
|
|
28
|
+
|
|
29
|
+
历史记录存在 `ai-history.js` 的模块级单例里(上限 50 条,最新在前)。之所以不放组件内:
|
|
30
|
+
配置面板里的编辑器会随切换配置项反复卸载重建,组件内的 ref 一卸载就没了。
|
|
31
|
+
代价是同一页面所有编辑器共用一份(未做分组),所以每条都带时间与问题原文;需要按字段分组时再给 `pushAiHistory` 加 key 维度。
|
|
32
|
+
|
|
33
|
+
### Props
|
|
34
|
+
|
|
35
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
36
|
+
| --------------- | --------------------------------- | ---------- | ------------------------------------------------------ |
|
|
37
|
+
| `aiRequest` | `AiCodeEditRequest` | - | AI 请求函数,传入即启用 AI 编辑 |
|
|
38
|
+
| `aiHotkey` | `string` | `'Mod-i'` | 唤起输入框的快捷键(CodeMirror 键位语法) |
|
|
39
|
+
| `aiPlaceholder` | `string` | 见源码 | 输入框占位提示 |
|
|
40
|
+
| `aiScope` | `'document' \| 'cursor'` | `'document'` | 无选区时的作用范围 |
|
|
41
|
+
| `aiPanelSize` | `'normal' \| 'large'` | `'normal'` | AI 输入框 / 历史面板的尺寸档位。编辑区被放大时(如 window-form 的「放大」抽屉)传 `large` |
|
|
42
|
+
| `aiWindowId` | `string \| number` | `''` | AI 上下文用的「所属窗口 ID」。组件不认识任何业务接口,只在 AI 被唤起时通过 `ai-activate` 把它抛给宿主,由宿主去取窗口配置 |
|
|
43
|
+
|
|
44
|
+
### Events
|
|
45
|
+
|
|
46
|
+
| 事件 | 参数 | 说明 |
|
|
47
|
+
| ---------------- | ----------------------------- | ------------------------ |
|
|
48
|
+
| `update:modelValue` | `string` | 代码变更(原有) |
|
|
49
|
+
| `ai-activate` | `string \| number` | AI 面板刚打开(携带 `aiWindowId`),宿主可趁此时预取上下文;`aiWindowId` 为空时不触发 |
|
|
50
|
+
| `ai-apply` | `{prompt, text}` | AI 结果已写入编辑器 |
|
|
51
|
+
| `ai-error` | `Error` | AI 请求失败 |
|
|
52
|
+
|
|
53
|
+
### Expose
|
|
54
|
+
|
|
55
|
+
`openAi(prompt?)`、`closeAi()`、`stopAi()`、`getView()`。
|
|
56
|
+
|
|
57
|
+
### 接入示例(非流式)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import {YhCodemirror} from 'yh-codemirror';
|
|
61
|
+
import type {AiCodeEditRequest} from 'yh-codemirror';
|
|
62
|
+
|
|
63
|
+
const aiRequest: AiCodeEditRequest = async ({prompt, code, selectedCode, language, signal}) => {
|
|
64
|
+
const res = await fetch('/api/ai/code-edit', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
signal,
|
|
67
|
+
headers: {'Content-Type': 'application/json'},
|
|
68
|
+
body: JSON.stringify({prompt, code, selectedCode, language}),
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
throw new Error('AI 请求失败');
|
|
72
|
+
}
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
// 返回「作用范围的完整替换文本」,组件负责写入
|
|
75
|
+
return data.code;
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 接入示例(流式 SSE)
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const aiRequest: AiCodeEditRequest = async ({prompt, selectedCode, signal, onChunk}) => {
|
|
83
|
+
const res = await fetch('/api/ai/code-edit/stream', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
signal,
|
|
86
|
+
headers: {'Content-Type': 'application/json'},
|
|
87
|
+
body: JSON.stringify({prompt, code: selectedCode}),
|
|
88
|
+
});
|
|
89
|
+
const reader = res.body!.getReader();
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
while (true) {
|
|
92
|
+
const {done, value} = await reader.read();
|
|
93
|
+
if (done) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
onChunk?.(decoder.decode(value, {stream: true}));
|
|
97
|
+
}
|
|
98
|
+
// 走 onChunk 时返回值可为空,组件已实时写入编辑器
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> 提示:流式场景下 `onChunk` 传入的是增量片段,组件按累积文本整体覆盖作用区间,因此服务端无需自行做 diff。
|
package/ai-history.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 问答历史(模块级单例)。
|
|
3
|
+
*
|
|
4
|
+
* 代价是同一页面里所有编辑器共用一份历史(未做分组),所以每条都带上时间与问题原文,
|
|
5
|
+
* 靠问题内容本身区分是哪个字段的提问。需要按字段分组时再给 pushAiHistory 加 key 维度。
|
|
6
|
+
*/
|
|
7
|
+
import {ref} from 'vue';
|
|
8
|
+
|
|
9
|
+
/** 最多保留多少条(超出丢最旧的) */
|
|
10
|
+
const MAX_HISTORY = 50;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @type {import('vue').Ref<Array<{id:number, time:string, prompt:string, scope:string, answer:string}>>}
|
|
14
|
+
* 最新的排在最前面
|
|
15
|
+
*/
|
|
16
|
+
export const aiHistory = ref([]);
|
|
17
|
+
|
|
18
|
+
let seed = 0;
|
|
19
|
+
|
|
20
|
+
const nowText = () => {
|
|
21
|
+
const d = new Date();
|
|
22
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
23
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(
|
|
24
|
+
d.getSeconds()
|
|
25
|
+
)}`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** 追加一条:{prompt, scope, answer} */
|
|
29
|
+
export const pushAiHistory = ({prompt, scope = '', answer = ''}) => {
|
|
30
|
+
if (!prompt && !answer) return;
|
|
31
|
+
aiHistory.value = [{id: ++seed, time: nowText(), prompt, scope, answer}, ...aiHistory.value].slice(
|
|
32
|
+
0,
|
|
33
|
+
MAX_HISTORY
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const clearAiHistory = () => {
|
|
38
|
+
aiHistory.value = [];
|
|
39
|
+
};
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "yh-codemirror",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "wraper codemirror with vue3",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"@codemirror/
|
|
17
|
-
"@codemirror/
|
|
18
|
-
"codemirror": "^6.0.1",
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "yh-codemirror",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "wraper codemirror with vue3",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./index.ts",
|
|
9
|
+
"types": "./index.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"pub": "npm publish --registry https://registry.npmjs.org/ && npm publish --registry https://packages.aliyun.com/60765e0161a945067837bb5f/npm/npm-registry/"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@codemirror/autocomplete": "^6.16.3",
|
|
17
|
+
"@codemirror/lang-css": "^6.2.1",
|
|
18
|
+
"@codemirror/lang-java": "^6.0.1",
|
|
19
|
+
"@codemirror/lang-javascript": "^6.2.2",
|
|
20
|
+
"@codemirror/lang-sql": "^6.7.0",
|
|
21
|
+
"@codemirror/lang-html": "^6.4.9",
|
|
22
|
+
"@codemirror/language": "^6.10.2",
|
|
23
|
+
"@codemirror/view": "^6.28.2",
|
|
24
|
+
"@lezer/highlight": "^1.2.0",
|
|
25
|
+
"cm6-theme-solarized-dark": "^0.1.0",
|
|
26
|
+
"codemirror": "^6.0.1",
|
|
27
|
+
"js-beautify": "^1.15.1"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"vue-i18n": "^9.8.0",
|
|
31
|
+
"vue": "^3.5.0",
|
|
32
|
+
"element-plus": "^2.9.0"
|
|
33
|
+
},
|
|
34
|
+
"author": "Liubin"
|
|
27
35
|
}
|
package/util.ts
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
|
-
import { javascript } from "@codemirror/lang-javascript";
|
|
2
|
-
import { java } from "@codemirror/lang-java";
|
|
3
|
-
import { css } from "@codemirror/lang-css";
|
|
4
|
-
import { sql } from "@codemirror/lang-sql";
|
|
5
|
-
import { html } from "@codemirror/lang-html";
|
|
6
|
-
|
|
7
|
-
const plugins = {
|
|
8
|
-
javascript: () => javascript(),
|
|
9
|
-
sql: () => sql(),
|
|
10
|
-
css: () => css(),
|
|
11
|
-
java: () => java(),
|
|
12
|
-
html: () => html(),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { javascript } from "@codemirror/lang-javascript";
|
|
2
|
+
import { java } from "@codemirror/lang-java";
|
|
3
|
+
import { css } from "@codemirror/lang-css";
|
|
4
|
+
import { sql } from "@codemirror/lang-sql";
|
|
5
|
+
import { html } from "@codemirror/lang-html";
|
|
6
|
+
|
|
7
|
+
const plugins = {
|
|
8
|
+
javascript: () => javascript(),
|
|
9
|
+
sql: () => sql(),
|
|
10
|
+
css: () => css(),
|
|
11
|
+
java: () => java(),
|
|
12
|
+
html: () => html(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* AI 提示编辑:宿主注入的请求参数
|
|
17
|
+
*/
|
|
18
|
+
export interface AiCodeEditParams {
|
|
19
|
+
/** 用户输入的提示词 */
|
|
20
|
+
prompt: string;
|
|
21
|
+
/** 编辑器全文代码 */
|
|
22
|
+
code: string;
|
|
23
|
+
/** 本次作用范围的代码(有选区=选中内容;无选区=全文或空串) */
|
|
24
|
+
selectedCode: string;
|
|
25
|
+
/** 当前编辑器语言 */
|
|
26
|
+
language?: string | string[];
|
|
27
|
+
/** 用于中断请求(用户点“停止”/关闭面板时触发) */
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
/** 流式输出回调:逐段返回内容,组件会实时写入编辑器 */
|
|
30
|
+
onChunk?: (chunk: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* AI 提示编辑:宿主注入的请求函数。
|
|
35
|
+
* 返回值需为「作用范围的完整替换文本」;若走流式(onChunk 逐段返回),返回值可为空。
|
|
36
|
+
*/
|
|
37
|
+
export type AiCodeEditRequest = (
|
|
38
|
+
params: AiCodeEditParams
|
|
39
|
+
) => Promise<string | void | {text?: string; code?: string; content?: string}>;
|
|
40
|
+
|
|
41
|
+
export const getCodeLanguagePlugin = function (language) {
|
|
42
|
+
return plugins[language]();
|
|
43
|
+
};
|