@veltra/desktop 1.2.24 → 1.2.25

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.
Files changed (87) hide show
  1. package/dist/components/action/action-group.vue.d.ts +2 -2
  2. package/dist/components/action/action.vue.d.ts +1 -1
  3. package/dist/components/auto-complete/auto-complete.vue.d.ts +2 -2
  4. package/dist/components/cascade/cascade.js +5 -3
  5. package/dist/components/cascade/cascade.js.map +1 -1
  6. package/dist/components/cascade/cascade.vue.d.ts +3 -3
  7. package/dist/components/cascade/use-data-map.js +2 -1
  8. package/dist/components/cascade/use-data-map.js.map +1 -1
  9. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  10. package/dist/components/checkbox-group/checkbox-group.js +10 -10
  11. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  12. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  13. package/dist/components/code-editor/basic-setup.js +2 -1
  14. package/dist/components/code-editor/basic-setup.js.map +1 -1
  15. package/dist/components/code-editor/code-editor.js +136 -79
  16. package/dist/components/code-editor/code-editor.js.map +1 -1
  17. package/dist/components/code-editor/code-editor.vue.d.ts +10 -2
  18. package/dist/components/code-editor/shell-extension.js +139 -0
  19. package/dist/components/code-editor/shell-extension.js.map +1 -0
  20. package/dist/components/code-editor/style.js +1 -0
  21. package/dist/components/code-editor/style2.css +15 -11
  22. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  23. package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
  24. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
  25. package/dist/components/empty/empty.vue.d.ts +1 -1
  26. package/dist/components/input/input.vue.d.ts +2 -2
  27. package/dist/components/multi-select/multi-select.js +31 -36
  28. package/dist/components/multi-select/multi-select.js.map +1 -1
  29. package/dist/components/multi-select/multi-select.vue.d.ts +2 -2
  30. package/dist/components/multi-tree-select/multi-tree-select.js +8 -6
  31. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  32. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +2 -2
  33. package/dist/components/number-input/number-input.vue.d.ts +2 -2
  34. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  35. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  36. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  37. package/dist/components/progress-nodes/progress-nodes.js +6 -4
  38. package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
  39. package/dist/components/radio-group/radio-group.js +8 -6
  40. package/dist/components/radio-group/radio-group.js.map +1 -1
  41. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  42. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  43. package/dist/components/select/select.js +13 -13
  44. package/dist/components/select/select.js.map +1 -1
  45. package/dist/components/select/select.vue.d.ts +2 -2
  46. package/dist/components/select/use-options.js +7 -4
  47. package/dist/components/select/use-options.js.map +1 -1
  48. package/dist/components/steps/steps.js +3 -2
  49. package/dist/components/steps/steps.js.map +1 -1
  50. package/dist/components/steps/steps.vue.d.ts +2 -2
  51. package/dist/components/textarea/textarea.vue.d.ts +2 -2
  52. package/dist/components/tip/tip.vue.d.ts +1 -1
  53. package/dist/components/tree/use-check.js +2 -1
  54. package/dist/components/tree/use-check.js.map +1 -1
  55. package/dist/components/tree/use-select.js +3 -1
  56. package/dist/components/tree/use-select.js.map +1 -1
  57. package/dist/components/tree/use-tree-nodes.js +4 -1
  58. package/dist/components/tree/use-tree-nodes.js.map +1 -1
  59. package/dist/components/tree-select/tree-select.js +6 -4
  60. package/dist/components/tree-select/tree-select.js.map +1 -1
  61. package/dist/components/tree-select/tree-select.vue.d.ts +2 -2
  62. package/dist/components/watermark/watermark.vue.d.ts +1 -1
  63. package/dist/node_modules/.bun/@codemirror_commands@6.10.4/node_modules/@codemirror/commands/dist/index.js +12 -1
  64. package/dist/node_modules/.bun/@codemirror_commands@6.10.4/node_modules/@codemirror/commands/dist/index.js.map +1 -1
  65. package/dist/style.js +1 -1
  66. package/dist/types/code-editor.d.ts +13 -2
  67. package/package.json +6 -6
  68. package/src/components/cascade/cascade.vue +6 -3
  69. package/src/components/cascade/use-data-map.ts +3 -2
  70. package/src/components/checkbox-group/checkbox-group.vue +10 -8
  71. package/src/components/code-editor/basic-setup.ts +3 -1
  72. package/src/components/code-editor/code-editor.vue +154 -96
  73. package/src/components/code-editor/shell-extension.ts +199 -0
  74. package/src/components/code-editor/style.scss +20 -13
  75. package/src/components/code-editor/style.ts +1 -0
  76. package/src/components/multi-select/multi-select.vue +31 -30
  77. package/src/components/multi-tree-select/multi-tree-select.vue +5 -2
  78. package/src/components/progress-nodes/progress-nodes.vue +7 -4
  79. package/src/components/radio-group/radio-group.vue +5 -1
  80. package/src/components/select/select.vue +13 -11
  81. package/src/components/select/use-options.ts +7 -4
  82. package/src/components/steps/steps.vue +3 -1
  83. package/src/components/tree/use-check.ts +2 -1
  84. package/src/components/tree/use-select.ts +3 -1
  85. package/src/components/tree/use-tree-nodes.ts +5 -9
  86. package/src/components/tree-select/tree-select.vue +7 -4
  87. package/src/types/code-editor.ts +13 -2
@@ -1,10 +1,12 @@
1
1
  import _sfc_main$1 from "../scroll/scroll.js";
2
+ import _sfc_main$2 from "../select/select.js";
2
3
  import { Compartment, EditorState } from "../../node_modules/.bun/@codemirror_state@6.7.0/node_modules/@codemirror/state/dist/index.js";
3
- import { EditorView, ViewPlugin, tooltips } from "../../node_modules/.bun/@codemirror_view@6.43.4/node_modules/@codemirror/view/dist/index.js";
4
+ import { EditorView, tooltips } from "../../node_modules/.bun/@codemirror_view@6.43.4/node_modules/@codemirror/view/dist/index.js";
4
5
  import { oneDark } from "../../node_modules/.bun/@codemirror_theme-one-dark@6.1.3/node_modules/@codemirror/theme-one-dark/dist/index.js";
5
6
  import { basicSetup } from "./basic-setup.js";
6
7
  import { loadLanguage } from "./lang-loaders.js";
7
- import { computed, createBlock, createElementVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, shallowRef, unref, useModel, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
8
+ import { buildFullDoc, extractBodyFromText, getShellRanges, mapSelectionToBody, shellExtension, textEqualsShellDoc } from "./shell-extension.js";
9
+ import { computed, createBlock, createCommentVNode, createElementVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, shallowRef, unref, useModel, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
8
10
  import { useFormFallbackProps } from "@veltra/compositions";
9
11
  import { bem, injectFormContext, zIndex } from "@veltra/utils";
10
12
  //#region src/components/code-editor/code-editor.vue
@@ -14,7 +16,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
14
16
  __name: "code-editor",
15
17
  props: /*@__PURE__*/ mergeModels({
16
18
  modelValue: {},
17
- language: {},
19
+ langs: {},
20
+ lang: {},
21
+ prefix: {},
22
+ suffix: {},
18
23
  dark: {
19
24
  type: Boolean,
20
25
  default: false
@@ -35,12 +40,20 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
35
40
  rules: {}
36
41
  }, {
37
42
  "modelValue": {},
38
- "modelModifiers": {}
43
+ "modelModifiers": {},
44
+ "lang": {},
45
+ "langModifiers": {}
39
46
  }),
40
- emits: ["update:modelValue"],
41
- setup(__props) {
47
+ emits: /*@__PURE__*/ mergeModels([
48
+ "update:modelValue",
49
+ "update:lang",
50
+ "change"
51
+ ], ["update:modelValue", "update:lang"]),
52
+ setup(__props, { emit: __emit }) {
42
53
  const props = __props;
54
+ const emit = __emit;
43
55
  const model = useModel(__props, "modelValue");
56
+ const lang = useModel(__props, "lang");
44
57
  const cls = bem("code-editor");
45
58
  const { formProps } = injectFormContext();
46
59
  const { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
@@ -54,58 +67,41 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
54
67
  bem.is("dark", props.dark)
55
68
  ]);
56
69
  const rootStyle = computed(() => ({ "--u-code-editor-default-lines": Math.max(1, props.defaultLines) }));
70
+ const langs = computed(() => props.langs ?? []);
71
+ const showLangSelect = computed(() => langs.value.length > 1);
72
+ const langOptions = computed(() => langs.value.map((value) => ({
73
+ label: value.toUpperCase(),
74
+ value
75
+ })));
76
+ /** 实际生效的语言:优先 lang model,否则回落 langs[0] */
77
+ const activeLang = computed(() => lang.value ?? langs.value[0]);
78
+ const shellConfig = computed(() => ({
79
+ prefix: props.prefix ?? "",
80
+ suffix: props.suffix ?? ""
81
+ }));
57
82
  const containerRef = useTemplateRef("container");
58
83
  const editor = shallowRef(null);
59
84
  const themeCompartment = new Compartment();
60
85
  const editableCompartment = new Compartment();
61
86
  const readOnlyCompartment = new Compartment();
62
87
  const langCompartment = new Compartment();
63
- let phantomGutter = null;
64
- let phantomContainer = null;
65
- let lastPhantomKey = "";
66
- function resetPhantomCache() {
67
- phantomGutter = null;
68
- phantomContainer = null;
69
- lastPhantomKey = "";
70
- }
88
+ const shellCompartment = new Compartment();
89
+ /** 聚焦时的正文快照,用于失焦时判断是否触发 change */
90
+ let focusSnapshot = "";
71
91
  /**
72
- * 在 .cm-lineNumbers 末尾追加占位行号 DOM,使可视行号数量补齐到 defaultLines。
73
- * CodeMirror 的 lineNumbers() 仅为真实文档行渲染行号,超出部分由本逻辑补全。
92
+ * 编辑器最近一次写出的正文引用。
93
+ * 用于短路 model → doc 同步,避免每次按键对整篇文档做 O(n) 相等比较。
74
94
  */
75
- function syncPhantomLineNumbers() {
76
- const view = editor.value;
77
- if (!view) return;
78
- if (!phantomGutter || !phantomGutter.isConnected) phantomGutter = view.dom.querySelector(".cm-gutter.cm-lineNumbers");
79
- if (!phantomGutter) return;
80
- if (!phantomContainer) {
81
- phantomContainer = document.createElement("div");
82
- phantomContainer.className = "cm-phantom-lines";
83
- }
84
- if (phantomContainer.parentElement !== phantomGutter || phantomContainer !== phantomGutter.lastElementChild) phantomGutter.appendChild(phantomContainer);
85
- const realLines = view.state.doc.lines;
86
- const target = Math.max(0, Math.max(1, props.defaultLines) - realLines);
87
- const lineHeight = view.defaultLineHeight;
88
- const key = `${realLines}|${target}|${lineHeight}`;
89
- if (key === lastPhantomKey) return;
90
- lastPhantomKey = key;
91
- while (phantomContainer.children.length > target) phantomContainer.lastElementChild.remove();
92
- while (phantomContainer.children.length < target) {
93
- const el = document.createElement("div");
94
- el.className = "cm-gutterElement cm-gutterElement-phantom";
95
- phantomContainer.appendChild(el);
96
- }
97
- for (let i = 0; i < target; i++) {
98
- const el = phantomContainer.children[i];
99
- const num = String(realLines + i + 1);
100
- if (el.textContent !== num) el.textContent = num;
101
- if (lineHeight && el.style.height !== `${lineHeight}px`) el.style.height = `${lineHeight}px`;
102
- }
95
+ let lastEditorBody;
96
+ /** 上一帧是否处于 IME composition,用于在 composition 结束时补一次 model 同步 */
97
+ let wasComposing = false;
98
+ /** 将编辑器正文写回 v-model(跳过与上次相同的值,避免多余更新) */
99
+ function writeModelFromDoc(doc) {
100
+ const nextBody = extractBodyFromText(doc, shellConfig.value);
101
+ if (nextBody === lastEditorBody && nextBody === model.value) return;
102
+ lastEditorBody = nextBody;
103
+ model.value = nextBody;
103
104
  }
104
- const phantomLineNumbersPlugin = ViewPlugin.fromClass(class {
105
- update(update) {
106
- if (update.docChanged || update.geometryChanged) syncPhantomLineNumbers();
107
- }
108
- });
109
105
  function buildExtensions() {
110
106
  return [
111
107
  basicSetup,
@@ -115,7 +111,20 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
115
111
  editableCompartment.of(EditorView.editable.of(!disabled.value)),
116
112
  readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),
117
113
  langCompartment.of([]),
118
- phantomLineNumbersPlugin
114
+ shellCompartment.of(shellExtension(shellConfig.value)),
115
+ EditorView.updateListener.of((update) => {
116
+ const composing = update.view.composing;
117
+ if (update.docChanged && !composing) writeModelFromDoc(update.state.doc);
118
+ else if (wasComposing && !composing) writeModelFromDoc(update.state.doc);
119
+ wasComposing = composing;
120
+ if (!update.focusChanged) return;
121
+ if (update.view.hasFocus) {
122
+ focusSnapshot = extractBodyFromText(update.state.doc, shellConfig.value);
123
+ return;
124
+ }
125
+ const current = extractBodyFromText(update.state.doc, shellConfig.value);
126
+ if (current !== focusSnapshot) emit("change", current);
127
+ })
119
128
  ];
120
129
  }
121
130
  let editorToken = 0;
@@ -124,7 +133,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
124
133
  const view = editor.value;
125
134
  if (!view) return;
126
135
  const myToken = ++langToken;
127
- const { language } = props;
136
+ const language = activeLang.value;
128
137
  if (!language) {
129
138
  if (myToken !== langToken || editor.value !== view) return;
130
139
  view.dispatch({ effects: langCompartment.reconfigure([]) });
@@ -139,30 +148,63 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
139
148
  ++langToken;
140
149
  editor.value?.destroy();
141
150
  editor.value = null;
142
- resetPhantomCache();
151
+ lastEditorBody = void 0;
152
+ wasComposing = false;
143
153
  }
144
154
  function createEditor() {
145
155
  destroyEditor();
146
156
  const myToken = ++editorToken;
147
157
  const parent = containerRef.value;
148
158
  if (!parent) return;
159
+ const config = shellConfig.value;
160
+ const body = model.value ?? "";
161
+ const doc = buildFullDoc(body, config);
162
+ const { bodyStart } = getShellRanges(doc.length, config);
163
+ lastEditorBody = body;
149
164
  const view = new EditorView({
150
- doc: model.value,
165
+ doc,
166
+ selection: {
167
+ anchor: bodyStart,
168
+ head: bodyStart
169
+ },
151
170
  extensions: buildExtensions(),
152
- parent,
153
- dispatch(tr) {
154
- view.update([tr]);
155
- if (tr.docChanged) model.value = tr.state.doc.toString();
156
- }
171
+ parent
157
172
  });
158
173
  if (myToken !== editorToken) {
159
174
  view.destroy();
160
175
  return;
161
176
  }
162
177
  editor.value = view;
163
- syncPhantomLineNumbers();
164
178
  applyLanguage();
165
179
  }
180
+ /**
181
+ * 将 model 正文同步为完整文档;可选同时 reconfigure shell,
182
+ * 避免 prefix/suffix 变更时「先改扩展、后改文档」导致选区越界。
183
+ */
184
+ function syncDocFromModel(view, body, config, options) {
185
+ const current = view.state.doc;
186
+ const docChanged = !textEqualsShellDoc(current, body, config);
187
+ const reconfigureShell = options?.reconfigureShell ?? false;
188
+ if (!docChanged && !reconfigureShell) return;
189
+ const nextDoc = buildFullDoc(body, config);
190
+ const prevShell = options?.prevShell ?? config;
191
+ const prevRanges = getShellRanges(current.length, prevShell);
192
+ const nextRanges = getShellRanges(nextDoc.length, config);
193
+ const selection = mapSelectionToBody(view.state.selection, prevRanges, nextRanges, nextDoc.length);
194
+ view.dispatch({
195
+ ...docChanged ? {
196
+ changes: {
197
+ from: 0,
198
+ to: current.length,
199
+ insert: nextDoc
200
+ },
201
+ selection,
202
+ ...reconfigureShell ? { scrollIntoView: true } : {}
203
+ } : { selection },
204
+ ...reconfigureShell ? { effects: shellCompartment.reconfigure(shellExtension(config)) } : {},
205
+ filter: false
206
+ });
207
+ }
166
208
  watch(containerRef, (el) => {
167
209
  if (el) createEditor();
168
210
  else destroyEditor();
@@ -170,7 +212,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
170
212
  immediate: true,
171
213
  flush: "post"
172
214
  });
173
- watch(() => props.language, applyLanguage);
215
+ watch(activeLang, applyLanguage);
216
+ /** langs 变化时,若当前 lang 未设或不在列表中则纠正为 langs[0] */
217
+ watch(langs, (list) => {
218
+ if (!list.length) return;
219
+ if (!lang.value || !list.includes(lang.value)) lang.value = list[0];
220
+ }, { immediate: true });
174
221
  watch(() => props.dark, (dark) => {
175
222
  editor.value?.dispatch({ effects: themeCompartment.reconfigure(dark ? oneDark : []) });
176
223
  });
@@ -180,35 +227,45 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
180
227
  watch(readonly, (v) => {
181
228
  editor.value?.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v)) });
182
229
  });
183
- watch(() => props.defaultLines, syncPhantomLineNumbers);
184
- watch([() => props.modelValue, editor], ([v, e]) => {
230
+ watch([
231
+ model,
232
+ editor,
233
+ shellConfig
234
+ ], ([v, e], [, , prevShell]) => {
185
235
  if (!e) return;
186
236
  if (e.composing) return;
187
- const next = v ?? "";
188
- if (e.state.doc.toString() === next) return;
189
- const valueLength = next.length;
190
- const inRange = e.state.selection.ranges.every((range) => range.anchor <= valueLength && range.head <= valueLength);
191
- e.dispatch({
192
- changes: {
193
- from: 0,
194
- to: e.state.doc.length,
195
- insert: next
196
- },
197
- selection: inRange ? e.state.selection : {
198
- anchor: 0,
199
- head: 0
200
- },
201
- scrollIntoView: true
237
+ const config = shellConfig.value;
238
+ const shellChanged = !prevShell || prevShell.prefix !== config.prefix || prevShell.suffix !== config.suffix;
239
+ if (!shellChanged && v === lastEditorBody) return;
240
+ const body = v ?? "";
241
+ syncDocFromModel(e, body, config, {
242
+ reconfigureShell: shellChanged,
243
+ prevShell: prevShell ?? config
202
244
  });
245
+ lastEditorBody = body;
203
246
  }, { immediate: true });
204
247
  onBeforeUnmount(destroyEditor);
205
248
  return (_ctx, _cache) => {
206
249
  return openBlock(), createBlock(unref(_sfc_main$1), {
207
250
  class: normalizeClass(className.value),
208
251
  style: normalizeStyle(rootStyle.value),
209
- onKeyup: _cache[0] || (_cache[0] = withKeys(withModifiers(() => {}, ["stop"]), ["enter"]))
252
+ onKeyup: _cache[1] || (_cache[1] = withKeys(withModifiers(() => {}, ["stop"]), ["enter"]))
210
253
  }, {
211
- default: withCtx(() => [createElementVNode("div", _hoisted_1, null, 512)]),
254
+ default: withCtx(() => [createElementVNode("div", _hoisted_1, null, 512), showLangSelect.value ? (openBlock(), createBlock(unref(_sfc_main$2), {
255
+ key: 0,
256
+ class: normalizeClass(unref(cls).e("lang-select")),
257
+ modelValue: lang.value,
258
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => lang.value = $event),
259
+ size: "small",
260
+ options: langOptions.value,
261
+ clearable: false,
262
+ disabled: unref(disabled)
263
+ }, null, 8, [
264
+ "class",
265
+ "modelValue",
266
+ "options",
267
+ "disabled"
268
+ ])) : createCommentVNode("v-if", true)]),
212
269
  _: 1
213
270
  }, 8, ["class", "style"]);
214
271
  };
@@ -1 +1 @@
1
- {"version":3,"file":"code-editor.js","names":[],"sources":["../../../src/components/code-editor/code-editor.vue"],"sourcesContent":["<template>\n <u-scroll :class=\"className\" :style=\"rootStyle\" @keyup.enter.stop>\n <div ref=\"container\"></div>\n </u-scroll>\n</template>\n\n<script lang=\"ts\" setup>\nimport { Compartment, EditorState, type Extension } from '@codemirror/state'\nimport { oneDark } from '@codemirror/theme-one-dark'\nimport { EditorView, ViewPlugin, tooltips, type ViewUpdate } from '@codemirror/view'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem, zIndex } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport {\n computed,\n onBeforeUnmount,\n shallowRef,\n useTemplateRef,\n watch,\n type CSSProperties\n} from 'vue'\n\nimport type { CodeEditorProps } from '../../types'\nimport { UScroll } from '../scroll'\nimport { basicSetup } from './basic-setup'\nimport { loadLanguage } from './lang-loaders'\n\ndefineOptions({ name: 'UCodeEditor' })\n\nconst props = withDefaults(defineProps<CodeEditorProps>(), {\n disabled: undefined,\n readonly: undefined,\n dark: false,\n defaultLines: 8\n})\n\nconst model = defineModel<string>()\n\nconst cls = bem('code-editor')\n\nconst { formProps } = injectFormContext()\n\nconst { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n disabled: false,\n readonly: false\n})\n\nconst className = computed<string[]>(() => [\n cls.b,\n bem.is('disabled', disabled.value),\n bem.is('readonly', readonly.value),\n bem.is('dark', props.dark)\n])\n\nconst rootStyle = computed<CSSProperties>(() => ({\n '--u-code-editor-default-lines': Math.max(1, props.defaultLines)\n}))\n\nconst containerRef = useTemplateRef('container')\nconst editor = shallowRef<EditorView | null>(null)\n\nconst themeCompartment = new Compartment()\nconst editableCompartment = new Compartment()\nconst readOnlyCompartment = new Compartment()\nconst langCompartment = new Compartment()\n\nlet phantomGutter: HTMLElement | null = null\nlet phantomContainer: HTMLElement | null = null\nlet lastPhantomKey = ''\n\nfunction resetPhantomCache() {\n phantomGutter = null\n phantomContainer = null\n lastPhantomKey = ''\n}\n\n/**\n * 在 .cm-lineNumbers 末尾追加占位行号 DOM,使可视行号数量补齐到 defaultLines。\n * CodeMirror 的 lineNumbers() 仅为真实文档行渲染行号,超出部分由本逻辑补全。\n */\nfunction syncPhantomLineNumbers() {\n const view = editor.value\n if (!view) return\n\n if (!phantomGutter || !phantomGutter.isConnected) {\n phantomGutter = view.dom.querySelector<HTMLElement>('.cm-gutter.cm-lineNumbers')\n }\n if (!phantomGutter) return\n\n if (!phantomContainer) {\n phantomContainer = document.createElement('div')\n phantomContainer.className = 'cm-phantom-lines'\n }\n if (\n phantomContainer.parentElement !== phantomGutter ||\n phantomContainer !== phantomGutter.lastElementChild\n ) {\n phantomGutter.appendChild(phantomContainer)\n }\n\n const realLines = view.state.doc.lines\n const target = Math.max(0, Math.max(1, props.defaultLines) - realLines)\n const lineHeight = view.defaultLineHeight\n\n const key = `${realLines}|${target}|${lineHeight}`\n if (key === lastPhantomKey) return\n lastPhantomKey = key\n\n while (phantomContainer.children.length > target) {\n phantomContainer.lastElementChild!.remove()\n }\n while (phantomContainer.children.length < target) {\n const el = document.createElement('div')\n el.className = 'cm-gutterElement cm-gutterElement-phantom'\n phantomContainer.appendChild(el)\n }\n for (let i = 0; i < target; i++) {\n const el = phantomContainer.children[i] as HTMLElement\n const num = String(realLines + i + 1)\n if (el.textContent !== num) el.textContent = num\n if (lineHeight && el.style.height !== `${lineHeight}px`) {\n el.style.height = `${lineHeight}px`\n }\n }\n}\n\nconst phantomLineNumbersPlugin = ViewPlugin.fromClass(\n class {\n update(update: ViewUpdate) {\n if (update.docChanged || update.geometryChanged) {\n syncPhantomLineNumbers()\n }\n }\n }\n)\n\nfunction buildExtensions(): Extension[] {\n return [\n basicSetup,\n tooltips({ parent: document.body }),\n EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } }),\n themeCompartment.of(props.dark ? oneDark : []),\n editableCompartment.of(EditorView.editable.of(!disabled.value)),\n readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),\n langCompartment.of([]),\n phantomLineNumbersPlugin\n ]\n}\n\nlet editorToken = 0\nlet langToken = 0\n\nasync function applyLanguage() {\n const view = editor.value\n if (!view) return\n const myToken = ++langToken\n const { language } = props\n\n if (!language) {\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure([]) })\n return\n }\n\n const ext = await loadLanguage(language)\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure(ext) })\n}\n\nfunction destroyEditor() {\n ++editorToken\n ++langToken\n editor.value?.destroy()\n editor.value = null\n resetPhantomCache()\n}\n\nfunction createEditor() {\n destroyEditor()\n const myToken = ++editorToken\n const parent = containerRef.value\n if (!parent) return\n\n const view = new EditorView({\n doc: model.value,\n extensions: buildExtensions(),\n parent,\n dispatch(tr) {\n view.update([tr])\n if (tr.docChanged) {\n model.value = tr.state.doc.toString()\n }\n }\n })\n\n if (myToken !== editorToken) {\n view.destroy()\n return\n }\n\n editor.value = view\n syncPhantomLineNumbers()\n applyLanguage()\n}\n\nwatch(\n containerRef,\n (el) => {\n if (el) createEditor()\n else destroyEditor()\n },\n { immediate: true, flush: 'post' }\n)\n\nwatch(() => props.language, applyLanguage)\n\nwatch(\n () => props.dark,\n (dark) => {\n editor.value?.dispatch({ effects: themeCompartment.reconfigure(dark ? oneDark : []) })\n }\n)\n\nwatch(disabled, (v) => {\n editor.value?.dispatch({ effects: editableCompartment.reconfigure(EditorView.editable.of(!v)) })\n})\n\nwatch(readonly, (v) => {\n editor.value?.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v)) })\n})\n\nwatch(() => props.defaultLines, syncPhantomLineNumbers)\n\nwatch(\n [() => props.modelValue, editor],\n ([v, e]) => {\n if (!e) return\n if (e.composing) return\n const next = v ?? ''\n if (e.state.doc.toString() === next) return\n\n const valueLength = next.length\n const inRange = e.state.selection.ranges.every(\n (range) => range.anchor <= valueLength && range.head <= valueLength\n )\n e.dispatch({\n changes: { from: 0, to: e.state.doc.length, insert: next },\n selection: inRange ? e.state.selection : { anchor: 0, head: 0 },\n scrollIntoView: true\n })\n },\n { immediate: true }\n)\n\nonBeforeUnmount(destroyEditor)\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"code-editor.js","names":[],"sources":["../../../src/components/code-editor/code-editor.vue"],"sourcesContent":["<template>\n <u-scroll :class=\"className\" :style=\"rootStyle\" @keyup.enter.stop>\n <div ref=\"container\"></div>\n\n <u-select\n v-if=\"showLangSelect\"\n :class=\"cls.e('lang-select')\"\n v-model=\"lang\"\n size=\"small\"\n :options=\"langOptions\"\n :clearable=\"false\"\n :disabled=\"disabled\"\n />\n </u-scroll>\n</template>\n\n<script lang=\"ts\" setup>\nimport { Compartment, EditorState, type Extension, type Text } from '@codemirror/state'\nimport { oneDark } from '@codemirror/theme-one-dark'\nimport { EditorView, tooltips } from '@codemirror/view'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem, injectFormContext, zIndex } from '@veltra/utils'\nimport {\n computed,\n onBeforeUnmount,\n shallowRef,\n useTemplateRef,\n watch,\n type CSSProperties\n} from 'vue'\n\nimport type { CodeEditorEmits, CodeEditorLang, CodeEditorProps } from '../../types'\nimport { UScroll } from '../scroll'\nimport { USelect } from '../select'\nimport { basicSetup } from './basic-setup'\nimport { loadLanguage } from './lang-loaders'\nimport {\n buildFullDoc,\n extractBodyFromText,\n getShellRanges,\n mapSelectionToBody,\n shellExtension,\n textEqualsShellDoc,\n type ShellConfig\n} from './shell-extension'\n\ndefineOptions({ name: 'UCodeEditor' })\n\nconst props = withDefaults(defineProps<CodeEditorProps>(), {\n disabled: undefined,\n readonly: undefined,\n dark: false,\n defaultLines: 8\n})\n\nconst emit = defineEmits<CodeEditorEmits>()\n\nconst model = defineModel<string>()\nconst lang = defineModel<CodeEditorLang>('lang')\n\nconst cls = bem('code-editor')\n\nconst { formProps } = injectFormContext()\n\nconst { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n disabled: false,\n readonly: false\n})\n\nconst className = computed<string[]>(() => [\n cls.b,\n bem.is('disabled', disabled.value),\n bem.is('readonly', readonly.value),\n bem.is('dark', props.dark)\n])\n\nconst rootStyle = computed<CSSProperties>(() => ({\n '--u-code-editor-default-lines': Math.max(1, props.defaultLines)\n}))\n\nconst langs = computed(() => props.langs ?? [])\n\nconst showLangSelect = computed(() => langs.value.length > 1)\n\nconst langOptions = computed(() =>\n langs.value.map((value) => ({ label: value.toUpperCase(), value }))\n)\n\n/** 实际生效的语言:优先 lang model,否则回落 langs[0] */\nconst activeLang = computed<CodeEditorLang | undefined>(() => lang.value ?? langs.value[0])\n\nconst shellConfig = computed<ShellConfig>(() => ({\n prefix: props.prefix ?? '',\n suffix: props.suffix ?? ''\n}))\n\nconst containerRef = useTemplateRef('container')\nconst editor = shallowRef<EditorView | null>(null)\n\nconst themeCompartment = new Compartment()\nconst editableCompartment = new Compartment()\nconst readOnlyCompartment = new Compartment()\nconst langCompartment = new Compartment()\nconst shellCompartment = new Compartment()\n\n/** 聚焦时的正文快照,用于失焦时判断是否触发 change */\nlet focusSnapshot = ''\n\n/**\n * 编辑器最近一次写出的正文引用。\n * 用于短路 model → doc 同步,避免每次按键对整篇文档做 O(n) 相等比较。\n */\nlet lastEditorBody: string | undefined\n\n/** 上一帧是否处于 IME composition,用于在 composition 结束时补一次 model 同步 */\nlet wasComposing = false\n\n/** 将编辑器正文写回 v-model(跳过与上次相同的值,避免多余更新) */\nfunction writeModelFromDoc(doc: Text) {\n const nextBody = extractBodyFromText(doc, shellConfig.value)\n if (nextBody === lastEditorBody && nextBody === model.value) return\n lastEditorBody = nextBody\n model.value = nextBody\n}\n\nfunction buildExtensions(): Extension[] {\n return [\n basicSetup,\n tooltips({ parent: document.body }),\n EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } }),\n themeCompartment.of(props.dark ? oneDark : []),\n editableCompartment.of(EditorView.editable.of(!disabled.value)),\n readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),\n langCompartment.of([]),\n shellCompartment.of(shellExtension(shellConfig.value)),\n EditorView.updateListener.of((update) => {\n const composing = update.view.composing\n\n // IME 进行中不写回,避免中间态触发父级重渲染;结束后再统一同步\n if (update.docChanged && !composing) {\n writeModelFromDoc(update.state.doc)\n } else if (wasComposing && !composing) {\n writeModelFromDoc(update.state.doc)\n }\n wasComposing = composing\n\n if (!update.focusChanged) return\n if (update.view.hasFocus) {\n focusSnapshot = extractBodyFromText(update.state.doc, shellConfig.value)\n return\n }\n const current = extractBodyFromText(update.state.doc, shellConfig.value)\n if (current !== focusSnapshot) {\n emit('change', current)\n }\n })\n ]\n}\n\nlet editorToken = 0\nlet langToken = 0\n\nasync function applyLanguage() {\n const view = editor.value\n if (!view) return\n const myToken = ++langToken\n const language = activeLang.value\n\n if (!language) {\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure([]) })\n return\n }\n\n const ext = await loadLanguage(language)\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure(ext) })\n}\n\nfunction destroyEditor() {\n ++editorToken\n ++langToken\n editor.value?.destroy()\n editor.value = null\n lastEditorBody = undefined\n wasComposing = false\n}\n\nfunction createEditor() {\n destroyEditor()\n const myToken = ++editorToken\n const parent = containerRef.value\n if (!parent) return\n\n const config = shellConfig.value\n const body = model.value ?? ''\n const doc = buildFullDoc(body, config)\n const { bodyStart } = getShellRanges(doc.length, config)\n lastEditorBody = body\n\n const view = new EditorView({\n doc,\n selection: { anchor: bodyStart, head: bodyStart },\n extensions: buildExtensions(),\n parent\n })\n\n if (myToken !== editorToken) {\n view.destroy()\n return\n }\n\n editor.value = view\n applyLanguage()\n}\n\n/**\n * 将 model 正文同步为完整文档;可选同时 reconfigure shell,\n * 避免 prefix/suffix 变更时「先改扩展、后改文档」导致选区越界。\n */\nfunction syncDocFromModel(\n view: EditorView,\n body: string,\n config: ShellConfig,\n options?: { reconfigureShell?: boolean; prevShell?: ShellConfig }\n) {\n const current = view.state.doc\n const docChanged = !textEqualsShellDoc(current, body, config)\n const reconfigureShell = options?.reconfigureShell ?? false\n\n if (!docChanged && !reconfigureShell) return\n\n const nextDoc = buildFullDoc(body, config)\n const prevShell = options?.prevShell ?? config\n const prevRanges = getShellRanges(current.length, prevShell)\n const nextRanges = getShellRanges(nextDoc.length, config)\n const selection = mapSelectionToBody(view.state.selection, prevRanges, nextRanges, nextDoc.length)\n\n view.dispatch({\n ...(docChanged\n ? {\n changes: { from: 0, to: current.length, insert: nextDoc },\n selection,\n // 仅外壳变更时滚入视口;外部 model 回写不应打断用户滚动位置\n ...(reconfigureShell ? { scrollIntoView: true } : {})\n }\n : { selection }),\n ...(reconfigureShell ? { effects: shellCompartment.reconfigure(shellExtension(config)) } : {}),\n // 绕过 shell 的 change/transactionFilter,避免全量替换被拦截或选区被错误钳制\n filter: false\n })\n}\n\nwatch(\n containerRef,\n (el) => {\n if (el) createEditor()\n else destroyEditor()\n },\n { immediate: true, flush: 'post' }\n)\n\nwatch(activeLang, applyLanguage)\n\n/** langs 变化时,若当前 lang 未设或不在列表中则纠正为 langs[0] */\nwatch(\n langs,\n (list) => {\n if (!list.length) return\n if (!lang.value || !list.includes(lang.value)) {\n lang.value = list[0]\n }\n },\n { immediate: true }\n)\n\nwatch(\n () => props.dark,\n (dark) => {\n editor.value?.dispatch({ effects: themeCompartment.reconfigure(dark ? oneDark : []) })\n }\n)\n\nwatch(disabled, (v) => {\n editor.value?.dispatch({ effects: editableCompartment.reconfigure(EditorView.editable.of(!v)) })\n})\n\nwatch(readonly, (v) => {\n editor.value?.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v)) })\n})\n\nwatch(\n [model, editor, shellConfig],\n ([v, e], [, , prevShell]) => {\n if (!e) return\n // IME 组字中不接受外部回写,避免打断输入\n if (e.composing) return\n const config = shellConfig.value\n const shellChanged =\n !prevShell || prevShell.prefix !== config.prefix || prevShell.suffix !== config.suffix\n // 编辑器自身写出的 model 无需回写文档\n if (!shellChanged && v === lastEditorBody) return\n const body = v ?? ''\n syncDocFromModel(e, body, config, {\n reconfigureShell: shellChanged,\n prevShell: prevShell ?? config\n })\n lastEditorBody = body\n },\n { immediate: true }\n)\n\nonBeforeUnmount(destroyEditor)\n</script>\n"],"mappings":"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"}
@@ -1,14 +1,22 @@
1
- import { CodeEditorProps } from "../../types/code-editor.js";
1
+ import { CodeEditorLang, CodeEditorProps } from "../../types/code-editor.js";
2
2
  //#region src/components/code-editor/code-editor.vue.d.ts
3
3
  type __VLS_Props = CodeEditorProps;
4
4
  type __VLS_ModelProps = {
5
5
  modelValue?: string;
6
+ 'lang'?: CodeEditorLang;
6
7
  };
7
8
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
10
  "update:modelValue": (value: string | undefined) => any;
11
+ "update:lang": (value: CodeEditorLang | undefined) => any;
12
+ } & {
13
+ "update:modelValue": (value: string) => any;
14
+ change: (value: string) => any;
15
+ "update:lang": (value: CodeEditorLang | undefined) => any;
10
16
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
- "onUpdate:modelValue"?: ((value: string | undefined) => any) | undefined;
17
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
18
+ onChange?: ((value: string) => any) | undefined;
19
+ "onUpdate:lang"?: ((value: CodeEditorLang | undefined) => any) | undefined;
12
20
  }>, {
13
21
  disabled: boolean;
14
22
  readonly: boolean;
@@ -0,0 +1,139 @@
1
+ import { EditorSelection, EditorState, RangeSetBuilder, StateField } from "../../node_modules/.bun/@codemirror_state@6.7.0/node_modules/@codemirror/state/dist/index.js";
2
+ import { Decoration, EditorView } from "../../node_modules/.bun/@codemirror_view@6.43.4/node_modules/@codemirror/view/dist/index.js";
3
+ //#region src/components/code-editor/shell-extension.ts
4
+ /**
5
+ * 判断 Text 文档是否与外壳对齐。
6
+ * 仅 slice 前后缀边界,避免整篇 toString。
7
+ */
8
+ function textMatchesShell(doc, config) {
9
+ const { prefix, suffix } = config;
10
+ const len = doc.length;
11
+ if (prefix.length + suffix.length > len) return false;
12
+ if (prefix && doc.sliceString(0, prefix.length) !== prefix) return false;
13
+ if (suffix && doc.sliceString(len - suffix.length) !== suffix) return false;
14
+ return true;
15
+ }
16
+ /** 计算正文区间在完整文档中的起止位置(始终落在 [0, docLength]) */
17
+ function getShellRanges(docLength, config) {
18
+ const prefixLen = config.prefix.length;
19
+ const suffixLen = config.suffix.length;
20
+ const bodyStart = Math.min(prefixLen, docLength);
21
+ return {
22
+ prefixLen,
23
+ suffixLen,
24
+ bodyStart,
25
+ bodyEnd: Math.max(bodyStart, docLength - Math.min(suffixLen, docLength - bodyStart))
26
+ };
27
+ }
28
+ /** 从 CodeMirror Text 提取正文(热路径,避免整篇 toString) */
29
+ function extractBodyFromText(doc, config) {
30
+ if (!config.prefix && !config.suffix) return doc.toString();
31
+ if (!textMatchesShell(doc, config)) return doc.toString();
32
+ const { bodyStart, bodyEnd } = getShellRanges(doc.length, config);
33
+ return doc.sliceString(bodyStart, bodyEnd);
34
+ }
35
+ /**
36
+ * 判断 Text 是否已等于 prefix + body + suffix(用于 model → doc 同步短路)。
37
+ * 长度不等时 O(1);相等时只比较正文与边界,不分配完整文档字符串。
38
+ */
39
+ function textEqualsShellDoc(doc, body, config) {
40
+ const { prefix, suffix } = config;
41
+ if (doc.length !== prefix.length + body.length + suffix.length) return false;
42
+ if (!prefix && !suffix) return doc.toString() === body;
43
+ if (!textMatchesShell(doc, config)) return false;
44
+ return doc.sliceString(prefix.length, doc.length - suffix.length) === body;
45
+ }
46
+ /** 将正文与前后缀拼成完整 CodeMirror 文档 */
47
+ function buildFullDoc(body, config) {
48
+ return config.prefix + body + config.suffix;
49
+ }
50
+ /** 选区是否已完全落在 [lo, hi] 内 */
51
+ function selectionWithin(selection, lo, hi) {
52
+ for (const range of selection.ranges) if (range.from < lo || range.to > hi) return false;
53
+ return true;
54
+ }
55
+ /** 将选区限制在正文区间内,并保证不超出文档长度 */
56
+ function clampSelection(selection, bodyStart, bodyEnd, docLength = bodyEnd) {
57
+ const lo = Math.max(0, Math.min(bodyStart, docLength));
58
+ const hi = Math.max(lo, Math.min(bodyEnd, docLength));
59
+ const clamp = (pos) => Math.max(lo, Math.min(hi, pos));
60
+ let changed = false;
61
+ const ranges = selection.ranges.map((range) => {
62
+ const anchor = clamp(range.anchor);
63
+ const head = clamp(range.head);
64
+ if (anchor !== range.anchor || head !== range.head) changed = true;
65
+ return EditorSelection.range(anchor, head);
66
+ });
67
+ if (!changed) return selection;
68
+ return EditorSelection.create(ranges, selection.mainIndex);
69
+ }
70
+ /**
71
+ * 将旧文档选区映射到新文档的正文区间(按正文内相对偏移)。
72
+ * 用于 prefix/suffix 切换时保持光标大致位置且始终合法。
73
+ */
74
+ function mapSelectionToBody(selection, from, to, docLength) {
75
+ const fromLen = Math.max(0, from.bodyEnd - from.bodyStart);
76
+ const toLen = Math.max(0, to.bodyEnd - to.bodyStart);
77
+ const mapPos = (pos) => {
78
+ const offset = Math.max(0, Math.min(fromLen, pos - from.bodyStart));
79
+ return to.bodyStart + Math.min(offset, toLen);
80
+ };
81
+ const ranges = selection.ranges.map((range) => EditorSelection.range(mapPos(range.anchor), mapPos(range.head)));
82
+ return clampSelection(EditorSelection.create(ranges, selection.mainIndex), to.bodyStart, to.bodyEnd, docLength);
83
+ }
84
+ const shellMark = Decoration.mark({ class: "cm-shell" });
85
+ function buildShellDecorations(doc, config) {
86
+ if (!textMatchesShell(doc, config)) return Decoration.none;
87
+ const { prefixLen, suffixLen, bodyEnd } = getShellRanges(doc.length, config);
88
+ const builder = new RangeSetBuilder();
89
+ if (prefixLen > 0) builder.add(0, prefixLen, shellMark);
90
+ if (suffixLen > 0 && bodyEnd < doc.length) builder.add(bodyEnd, doc.length, shellMark);
91
+ return builder.finish();
92
+ }
93
+ /** 变更是否触及外壳区间(相对 startState 文档坐标) */
94
+ function changesTouchShell(changes, bodyStart, bodyEnd) {
95
+ let touches = false;
96
+ changes.iterChangedRanges((fromA, toA) => {
97
+ if (fromA < bodyStart || toA > bodyEnd) touches = true;
98
+ });
99
+ return touches;
100
+ }
101
+ /**
102
+ * CodeMirror 扩展:将 prefix/suffix 作为文档内的只读区域,
103
+ * 通过 changeFilter 阻止编辑、transactionFilter 限制选区,并用装饰着色。
104
+ */
105
+ function shellExtension(config) {
106
+ if (!(config.prefix.length > 0 || config.suffix.length > 0)) return [];
107
+ return [
108
+ StateField.define({
109
+ create(state) {
110
+ return buildShellDecorations(state.doc, config);
111
+ },
112
+ update(decorations, tr) {
113
+ if (!tr.docChanged) return decorations;
114
+ const { bodyStart, bodyEnd } = getShellRanges(tr.startState.doc.length, config);
115
+ if (!changesTouchShell(tr.changes, bodyStart, bodyEnd) && decorations.size > 0) return decorations.map(tr.changes);
116
+ return buildShellDecorations(tr.newDoc, config);
117
+ },
118
+ provide: (f) => EditorView.decorations.from(f)
119
+ }),
120
+ EditorState.changeFilter.of((tr) => {
121
+ if (!tr.docChanged) return true;
122
+ const { bodyStart, bodyEnd } = getShellRanges(tr.startState.doc.length, config);
123
+ if (!changesTouchShell(tr.changes, bodyStart, bodyEnd)) return true;
124
+ return !textMatchesShell(tr.startState.doc, config);
125
+ }),
126
+ EditorState.transactionFilter.of((tr) => {
127
+ const { bodyStart, bodyEnd } = getShellRanges(tr.newDoc.length, config);
128
+ if (selectionWithin(tr.newSelection, bodyStart, bodyEnd)) return tr;
129
+ if (!textMatchesShell(tr.newDoc, config)) return tr;
130
+ const selection = clampSelection(tr.newSelection, bodyStart, bodyEnd, tr.newDoc.length);
131
+ if (selection.eq(tr.newSelection)) return tr;
132
+ return [tr, { selection }];
133
+ })
134
+ ];
135
+ }
136
+ //#endregion
137
+ export { buildFullDoc, extractBodyFromText, getShellRanges, mapSelectionToBody, shellExtension, textEqualsShellDoc };
138
+
139
+ //# sourceMappingURL=shell-extension.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-extension.js","names":[],"sources":["../../../src/components/code-editor/shell-extension.ts"],"sourcesContent":["import {\n EditorSelection,\n EditorState,\n RangeSetBuilder,\n StateField,\n type Extension,\n type Text\n} from '@codemirror/state'\nimport { Decoration, type DecorationSet, EditorView } from '@codemirror/view'\n\n/** 前后缀外壳配置 */\nexport interface ShellConfig {\n prefix: string\n suffix: string\n}\n\n/**\n * 判断 Text 文档是否与外壳对齐。\n * 仅 slice 前后缀边界,避免整篇 toString。\n */\nfunction textMatchesShell(doc: Text, config: ShellConfig): boolean {\n const { prefix, suffix } = config\n const len = doc.length\n if (prefix.length + suffix.length > len) return false\n if (prefix && doc.sliceString(0, prefix.length) !== prefix) return false\n if (suffix && doc.sliceString(len - suffix.length) !== suffix) return false\n return true\n}\n\n/** 计算正文区间在完整文档中的起止位置(始终落在 [0, docLength]) */\nexport function getShellRanges(docLength: number, config: ShellConfig) {\n const prefixLen = config.prefix.length\n const suffixLen = config.suffix.length\n // 外壳与文档可能短暂不一致,区间必须可安全用于选区\n const bodyStart = Math.min(prefixLen, docLength)\n const bodyEnd = Math.max(bodyStart, docLength - Math.min(suffixLen, docLength - bodyStart))\n return { prefixLen, suffixLen, bodyStart, bodyEnd }\n}\n\n/** 从 CodeMirror Text 提取正文(热路径,避免整篇 toString) */\nexport function extractBodyFromText(doc: Text, config: ShellConfig): string {\n // 无外壳时直接 toString,跳过边界校验与二次 slice\n if (!config.prefix && !config.suffix) return doc.toString()\n if (!textMatchesShell(doc, config)) return doc.toString()\n const { bodyStart, bodyEnd } = getShellRanges(doc.length, config)\n return doc.sliceString(bodyStart, bodyEnd)\n}\n\n/**\n * 判断 Text 是否已等于 prefix + body + suffix(用于 model → doc 同步短路)。\n * 长度不等时 O(1);相等时只比较正文与边界,不分配完整文档字符串。\n */\nexport function textEqualsShellDoc(doc: Text, body: string, config: ShellConfig): boolean {\n const { prefix, suffix } = config\n if (doc.length !== prefix.length + body.length + suffix.length) return false\n if (!prefix && !suffix) return doc.toString() === body\n if (!textMatchesShell(doc, config)) return false\n return doc.sliceString(prefix.length, doc.length - suffix.length) === body\n}\n\n/** 将正文与前后缀拼成完整 CodeMirror 文档 */\nexport function buildFullDoc(body: string, config: ShellConfig): string {\n return config.prefix + body + config.suffix\n}\n\n/** 选区是否已完全落在 [lo, hi] 内 */\nfunction selectionWithin(selection: EditorSelection, lo: number, hi: number): boolean {\n for (const range of selection.ranges) {\n if (range.from < lo || range.to > hi) return false\n }\n return true\n}\n\n/** 将选区限制在正文区间内,并保证不超出文档长度 */\nfunction clampSelection(\n selection: EditorSelection,\n bodyStart: number,\n bodyEnd: number,\n docLength = bodyEnd\n): EditorSelection {\n const lo = Math.max(0, Math.min(bodyStart, docLength))\n const hi = Math.max(lo, Math.min(bodyEnd, docLength))\n const clamp = (pos: number) => Math.max(lo, Math.min(hi, pos))\n let changed = false\n const ranges = selection.ranges.map((range) => {\n const anchor = clamp(range.anchor)\n const head = clamp(range.head)\n if (anchor !== range.anchor || head !== range.head) changed = true\n return EditorSelection.range(anchor, head)\n })\n if (!changed) return selection\n return EditorSelection.create(ranges, selection.mainIndex)\n}\n\n/**\n * 将旧文档选区映射到新文档的正文区间(按正文内相对偏移)。\n * 用于 prefix/suffix 切换时保持光标大致位置且始终合法。\n */\nexport function mapSelectionToBody(\n selection: EditorSelection,\n from: { bodyStart: number; bodyEnd: number },\n to: { bodyStart: number; bodyEnd: number },\n docLength: number\n): EditorSelection {\n const fromLen = Math.max(0, from.bodyEnd - from.bodyStart)\n const toLen = Math.max(0, to.bodyEnd - to.bodyStart)\n const mapPos = (pos: number) => {\n const offset = Math.max(0, Math.min(fromLen, pos - from.bodyStart))\n return to.bodyStart + Math.min(offset, toLen)\n }\n const ranges = selection.ranges.map((range) =>\n EditorSelection.range(mapPos(range.anchor), mapPos(range.head))\n )\n return clampSelection(\n EditorSelection.create(ranges, selection.mainIndex),\n to.bodyStart,\n to.bodyEnd,\n docLength\n )\n}\n\nconst shellMark = Decoration.mark({ class: 'cm-shell' })\n\nfunction buildShellDecorations(doc: Text, config: ShellConfig): DecorationSet {\n if (!textMatchesShell(doc, config)) return Decoration.none\n\n const { prefixLen, suffixLen, bodyEnd } = getShellRanges(doc.length, config)\n const builder = new RangeSetBuilder<Decoration>()\n\n if (prefixLen > 0) {\n builder.add(0, prefixLen, shellMark)\n }\n if (suffixLen > 0 && bodyEnd < doc.length) {\n builder.add(bodyEnd, doc.length, shellMark)\n }\n\n return builder.finish()\n}\n\n/** 变更是否触及外壳区间(相对 startState 文档坐标) */\nfunction changesTouchShell(\n changes: { iterChangedRanges: (f: (fromA: number, toA: number) => void) => void },\n bodyStart: number,\n bodyEnd: number\n): boolean {\n let touches = false\n changes.iterChangedRanges((fromA, toA) => {\n if (fromA < bodyStart || toA > bodyEnd) touches = true\n })\n return touches\n}\n\n/**\n * CodeMirror 扩展:将 prefix/suffix 作为文档内的只读区域,\n * 通过 changeFilter 阻止编辑、transactionFilter 限制选区,并用装饰着色。\n */\nexport function shellExtension(config: ShellConfig): Extension {\n const hasShell = config.prefix.length > 0 || config.suffix.length > 0\n if (!hasShell) return []\n\n const shellField = StateField.define<DecorationSet>({\n create(state) {\n return buildShellDecorations(state.doc, config)\n },\n update(decorations, tr) {\n if (!tr.docChanged) return decorations\n\n const { bodyStart, bodyEnd } = getShellRanges(tr.startState.doc.length, config)\n // 正文内编辑:外壳 mark 只需随 ChangeSet 映射,避免边界 slice / 重建\n if (!changesTouchShell(tr.changes, bodyStart, bodyEnd) && decorations.size > 0) {\n return decorations.map(tr.changes)\n }\n return buildShellDecorations(tr.newDoc, config)\n },\n provide: (f) => EditorView.decorations.from(f)\n })\n\n return [\n shellField,\n EditorState.changeFilter.of((tr) => {\n if (!tr.docChanged) return true\n const { bodyStart, bodyEnd } = getShellRanges(tr.startState.doc.length, config)\n // 未触及外壳区间时直接放行,避免 sliceString\n if (!changesTouchShell(tr.changes, bodyStart, bodyEnd)) return true\n // 触及外壳:文档尚未对齐时放行(由组件侧全量替换同步)\n return !textMatchesShell(tr.startState.doc, config)\n }),\n EditorState.transactionFilter.of((tr) => {\n const { bodyStart, bodyEnd } = getShellRanges(tr.newDoc.length, config)\n // 选区已在正文内:无需钳制,也无需校验外壳\n if (selectionWithin(tr.newSelection, bodyStart, bodyEnd)) return tr\n // 外壳与文档不一致时不钳选区,避免 Selection points outside of document\n if (!textMatchesShell(tr.newDoc, config)) return tr\n const selection = clampSelection(tr.newSelection, bodyStart, bodyEnd, tr.newDoc.length)\n if (selection.eq(tr.newSelection)) return tr\n return [tr, { selection }]\n })\n ]\n}\n"],"mappings":"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"}
@@ -1 +1,2 @@
1
+ import "../select/style.js";
1
2
  import "./style2.css";
@@ -39,6 +39,17 @@
39
39
  .u-code-editor:hover, .u-code-editor:focus-within {
40
40
  border-color: var(--u-color-primary);
41
41
  }
42
+ .u-code-editor__lang-select {
43
+ position: absolute;
44
+ top: 2px;
45
+ right: 2px;
46
+ z-index: 2;
47
+ width: 88px;
48
+ border: none;
49
+ }
50
+ .u-code-editor__lang-select input {
51
+ text-align: right;
52
+ }
42
53
  .u-code-editor .cm-editor {
43
54
  background: transparent;
44
55
  color: var(--u-code-editor-fg);
@@ -66,19 +77,9 @@
66
77
  .u-code-editor .cm-editor .cm-gutters {
67
78
  background-color: transparent;
68
79
  color: var(--u-code-editor-gutter-fg);
69
- border-right: 1px solid var(--u-code-editor-border);
80
+ border: none;
70
81
  user-select: none;
71
82
  }
72
- .u-code-editor .cm-editor .cm-gutterElement-phantom {
73
- color: var(--u-code-editor-gutter-fg);
74
- opacity: 0.45;
75
- padding: 0 3px 0 5px;
76
- text-align: right;
77
- white-space: nowrap;
78
- min-width: 1ch;
79
- box-sizing: border-box;
80
- pointer-events: none;
81
- }
82
83
  .u-code-editor .cm-editor .cm-activeLineGutter,
83
84
  .u-code-editor .cm-editor .cm-activeLine {
84
85
  background-color: var(--u-code-editor-active-bg);
@@ -90,6 +91,9 @@
90
91
  .u-code-editor .cm-editor .cm-cursor {
91
92
  border-left-color: var(--u-color-primary);
92
93
  }
94
+ .u-code-editor .cm-editor .cm-shell {
95
+ color: var(--u-code-editor-gutter-fg);
96
+ }
93
97
  .u-code-editor.is-dark {
94
98
  --u-code-editor-bg: #282c34;
95
99
  --u-code-editor-fg: #abb2bf;
@@ -7,8 +7,8 @@ declare const __VLS_export: import("vue").DefineComponent<DatePanelProps, {}, {}
7
7
  "onSelect:range-date"?: ((rangeDate?: [import("@cat-kit/core").Dater, import("@cat-kit/core").Dater] | undefined) => any) | undefined;
8
8
  "onSelect:date"?: ((date: import("@cat-kit/core").Dater) => any) | undefined;
9
9
  }>, {
10
- size: import("@veltra/utils").ComponentSize;
11
10
  type: "date" | "month" | "year";
11
+ size: import("@veltra/utils").ComponentSize;
12
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
13
  declare const _default: typeof __VLS_export;
14
14
  //#endregion
@@ -5,11 +5,11 @@ declare const __VLS_export: import("vue").DefineComponent<DatePickerProps, {}, {
5
5
  }, string, import("vue").PublicProps, Readonly<DatePickerProps> & Readonly<{
6
6
  "onUpdate:modelValue"?: ((value?: string | number | Date | undefined) => any) | undefined;
7
7
  }>, {
8
- disabled: boolean;
9
- readonly: boolean;
10
8
  type: "date" | "month" | "year";
11
9
  placeholder: string;
12
10
  clearable: boolean;
11
+ disabled: boolean;
12
+ readonly: boolean;
13
13
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
14
  declare const _default: typeof __VLS_export;
15
15
  //#endregion