@ptengine/inline-editor 0.1.10 → 0.1.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.12
4
+
5
+ 依赖不变,可以直接发。
6
+
7
+ **变更**
8
+
9
+ - 代码框 JS 补全补齐到老编辑器的口径:还没敲到 `ptKit.`、敲标识符(如 `pt`)时就列出 `ptKit.query(...)` 等整句片段,
10
+ 选中插入完整模板(此前只有敲完 `ptKit.` 才有成员补全,用户敲 `pt` 看不到任何提示)。按常用程度排序,`query` 在前。
11
+ - 新增常用浏览器全局对象的补全(`document` / `window` / `console` / `setTimeout` / `localStorage` / `MutationObserver` …)
12
+ 及其成员(`document.querySelector` 等);用白名单作用域,不把编辑器所在页面的全局与 Object.prototype 成员补出来。
13
+ - 字符串、注释里与别的对象的成员访问(`foo.pt`)不出 ptKit 整句补全。
14
+
15
+ ## 0.1.11
16
+
17
+ 依赖不变,可以直接发。
18
+
19
+ **修复**
20
+
21
+ - 代码框输入被吞、补全不出:宿主回传值是异步的,回传落后于编辑器时(用户已经又敲了字)旧值会整篇覆盖编辑器 ——
22
+ 刚敲的字丢失、光标重置、补全弹层被关掉,每次覆盖还要整篇重新解析(真机:敲 `ptKit.` 只剩 `p`)。
23
+ 现在记住自己交出去过的值,落后的回传不再覆盖;格式化 / AI 追加 / 宿主回填等真正的外部写入照常生效。
24
+ - 全选看不到选区:当前行高亮改成半透明(不透明时会盖住光标所在行的选区,单行代码全选什么都看不见),选区颜色加深。
25
+
3
26
  ## 0.1.10
4
27
 
5
28
  新增依赖:`@codemirror/*`(state / view / commands / language / autocomplete / lint / lang-css / lang-javascript)与 `js-beautify`。
@@ -21,8 +21,13 @@ export interface CodeMirrorFieldProps {
21
21
  ariaLabel?: string;
22
22
  'data-testid'?: string;
23
23
  }
24
- /** ptKit 工具函数(与后端 `engagev5/ptkit.ts`、SDK 同一套)—— JS 里敲 `ptKit.` 时补全。 */
24
+ /** 敲 `ptKit.` 之后:成员补全。 */
25
25
  export declare const PT_KIT_COMPLETIONS: import("@codemirror/autocomplete").Completion[];
26
+ /**
27
+ * 敲标识符时(如 `pt`):整句补全 `ptKit.query(...)` 等 —— 老编辑器(`ptKitConfig.js`)就是这样提示的,
28
+ * 用户不知道有 `ptKit` 时也能从这里发现它。`boost` 让它们排在关键字片段前面。
29
+ */
30
+ export declare const PT_KIT_IDENT_COMPLETIONS: import("@codemirror/autocomplete").Completion[];
26
31
  export declare function ptKitCompletion(context: CompletionContext): CompletionResult | null;
27
32
  /**
28
33
  * 语法错误提示:解析树里的出错节点(Lezer 遇到写不下去的地方会插一个 error 节点)。
@@ -7,44 +7,131 @@ import { bracketMatching, defaultHighlightStyle, foldGutter, indentOnInput, ensu
7
7
  import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap, snippetCompletion } from '@codemirror/autocomplete';
8
8
  import { lintGutter, linter } from '@codemirror/lint';
9
9
  import { css } from '@codemirror/lang-css';
10
- import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
11
- export const PT_KIT_COMPLETIONS = [
12
- snippetCompletion("query('${selector}').then(function (el) {\n\t${}\n});", {
13
- label: 'query',
10
+ import { javascript, javascriptLanguage, scopeCompletionSource } from '@codemirror/lang-javascript';
11
+ const PT_KIT_DEFS = [
12
+ {
13
+ name: 'query',
14
+ template: "query('${selector}').then(function (el) {\n\t${}\n});",
14
15
  detail: '(selector, options?) → Promise<Element>',
15
16
  info: '轮询等待元素出现(默认最长 60 秒、每 500ms 查一次),找到后 resolve。'
16
- }),
17
- snippetCompletion("queryAll('${selector}').then(function (list) {\n\t${}\n});", {
18
- label: 'queryAll',
17
+ },
18
+ {
19
+ name: 'queryAll',
20
+ template: "queryAll('${selector}').then(function (list) {\n\t${}\n});",
19
21
  detail: '(selector, options?) → Promise<Element[]>',
20
22
  info: '轮询等待至少一个匹配元素出现,resolve 全部匹配元素。'
21
- }),
22
- snippetCompletion("querySelector('${selector}')", {
23
- label: 'querySelector',
23
+ },
24
+ {
25
+ name: 'querySelector',
26
+ template: "querySelector('${selector}')",
24
27
  detail: '(selector, context?) → Element | null',
25
28
  info: '立即查找一个元素(含 Ptengine 弹窗的 Shadow DOM)。'
26
- }),
27
- snippetCompletion("querySelectorAll('${selector}')", {
28
- label: 'querySelectorAll',
29
+ },
30
+ {
31
+ name: 'querySelectorAll',
32
+ template: "querySelectorAll('${selector}')",
29
33
  detail: '(selector, context?) → Element[]',
30
34
  info: '立即查找全部匹配元素(含 Ptengine 弹窗的 Shadow DOM)。'
31
- }),
32
- snippetCompletion("observe('${selector}', function (el) {\n\t${}\n});", {
33
- label: 'observe',
35
+ },
36
+ {
37
+ name: 'observe',
38
+ template: "observe('${selector}', function (el) {\n\t${}\n});",
34
39
  detail: '(selector, callback, options?)',
35
40
  info: '元素已在或之后被插入页面时执行回调(MutationObserver)。'
36
- }),
37
- snippetCompletion("moveElement('${target}', '${relative}', '${after}');", {
38
- label: 'moveElement',
41
+ },
42
+ {
43
+ name: 'moveElement',
44
+ template: "moveElement('${target}', '${relative}', '${after}');",
39
45
  detail: "(target, relative, 'before' | 'after', options?)",
40
46
  info: '等两个元素都出现后,把 target 移到 relative 的前面或后面。'
41
- })
47
+ }
42
48
  ];
49
+ export const PT_KIT_COMPLETIONS = PT_KIT_DEFS.map((d, i) => snippetCompletion(d.template, {
50
+ label: d.name,
51
+ detail: d.detail,
52
+ info: d.info,
53
+ boost: 6 - i,
54
+ type: 'function'
55
+ }));
56
+ export const PT_KIT_IDENT_COMPLETIONS = PT_KIT_DEFS.map((d, i) => snippetCompletion(`ptKit.${d.template}`, {
57
+ label: `ptKit.${d.name}`,
58
+ detail: d.detail,
59
+ info: d.info,
60
+ type: 'function',
61
+ boost: 20 - i
62
+ }));
63
+ function inStringOrComment(context) {
64
+ const tree = ensureSyntaxTree(context.state, context.pos, 20) ?? syntaxTree(context.state);
65
+ const node = tree.resolveInner(context.pos, -1);
66
+ return /String|Comment|TemplateString/.test(node.type.name);
67
+ }
43
68
  export function ptKitCompletion(context) {
44
- const before = context.matchBefore(/ptKit\.\w*/);
45
- if (!before)
69
+ if (inStringOrComment(context))
70
+ return null;
71
+ const member = context.matchBefore(/ptKit\.\w*/);
72
+ if (member)
73
+ return {
74
+ from: member.from + 'ptKit.'.length,
75
+ options: PT_KIT_COMPLETIONS,
76
+ validFor: /^\w*$/
77
+ };
78
+ const word = context.matchBefore(/[\w$]+/);
79
+ const from = word ? word.from : context.pos;
80
+ if (from > 0 && context.state.sliceDoc(from - 1, from) === '.')
46
81
  return null;
47
- return { from: before.from + 'ptKit.'.length, options: PT_KIT_COMPLETIONS, validFor: /^\w*$/ };
82
+ if (!word)
83
+ return context.explicit ? { from, options: PT_KIT_IDENT_COMPLETIONS } : null;
84
+ return { from, options: PT_KIT_IDENT_COMPLETIONS, validFor: /^[\w$]*$/ };
85
+ }
86
+ function browserScope() {
87
+ if (typeof window === 'undefined')
88
+ return {};
89
+ const names = [
90
+ 'window',
91
+ 'document',
92
+ 'console',
93
+ 'location',
94
+ 'navigator',
95
+ 'history',
96
+ 'localStorage',
97
+ 'sessionStorage',
98
+ 'JSON',
99
+ 'Math',
100
+ 'Date',
101
+ 'Promise',
102
+ 'Array',
103
+ 'Object',
104
+ 'String',
105
+ 'Number',
106
+ 'Boolean',
107
+ 'RegExp',
108
+ 'setTimeout',
109
+ 'clearTimeout',
110
+ 'setInterval',
111
+ 'clearInterval',
112
+ 'requestAnimationFrame',
113
+ 'fetch',
114
+ 'MutationObserver',
115
+ 'IntersectionObserver',
116
+ 'ResizeObserver',
117
+ 'Event',
118
+ 'CustomEvent',
119
+ 'URL',
120
+ 'URLSearchParams',
121
+ 'encodeURIComponent',
122
+ 'decodeURIComponent',
123
+ 'parseInt',
124
+ 'parseFloat'
125
+ ];
126
+ const scope = Object.create(null);
127
+ for (const name of names) {
128
+ try {
129
+ scope[name] = window[name];
130
+ }
131
+ catch {
132
+ }
133
+ }
134
+ return scope;
48
135
  }
49
136
  export function findSyntaxErrors(state, message) {
50
137
  const diagnostics = [];
@@ -77,8 +164,11 @@ const theme = EditorView.theme({
77
164
  borderRight: '1px solid #EBECF0',
78
165
  color: '#97A0AF'
79
166
  },
80
- '.cm-activeLine': { backgroundColor: '#f4f5f7' },
81
- '.cm-activeLineGutter': { backgroundColor: '#eef0f3' },
167
+ '.cm-activeLine': { backgroundColor: 'rgba(9, 30, 66, 0.04)' },
168
+ '.cm-activeLineGutter': { backgroundColor: 'rgba(9, 30, 66, 0.06)' },
169
+ '.cm-selectionBackground': { backgroundColor: '#c7dbf7 !important' },
170
+ '&.cm-focused .cm-selectionBackground': { backgroundColor: '#a9c8f5 !important' },
171
+ '.cm-content ::selection': { backgroundColor: '#a9c8f5' },
82
172
  '.cm-tooltip': { fontSize: '12px' }
83
173
  });
84
174
  function languageExtensions(language, errorMessage) {
@@ -87,6 +177,7 @@ function languageExtensions(language, errorMessage) {
87
177
  return [
88
178
  javascript(),
89
179
  javascriptLanguage.data.of({ autocomplete: ptKitCompletion }),
180
+ javascriptLanguage.data.of({ autocomplete: scopeCompletionSource(browserScope()) }),
90
181
  syntaxLinter(errorMessage)
91
182
  ];
92
183
  }
@@ -94,6 +185,7 @@ export default function CodeMirrorField({ language, value, onChange, onBlur, pla
94
185
  const hostRef = useRef(null);
95
186
  const viewRef = useRef(null);
96
187
  const onChangeRef = useRef(onChange);
188
+ const emittedRef = useRef(new Set());
97
189
  onChangeRef.current = onChange;
98
190
  const onBlurRef = useRef(onBlur);
99
191
  onBlurRef.current = onBlur;
@@ -132,8 +224,14 @@ export default function CodeMirrorField({ language, value, onChange, onBlur, pla
132
224
  spellcheck: 'false'
133
225
  }),
134
226
  EditorView.updateListener.of(update => {
135
- if (update.docChanged)
136
- onChangeRef.current(update.state.doc.toString());
227
+ if (update.docChanged) {
228
+ const next = update.state.doc.toString();
229
+ const emitted = emittedRef.current;
230
+ emitted.add(next);
231
+ if (emitted.size > 64)
232
+ emitted.delete(emitted.values().next().value);
233
+ onChangeRef.current(next);
234
+ }
137
235
  if (update.focusChanged && !update.view.hasFocus)
138
236
  onBlurRef.current?.();
139
237
  }),
@@ -152,8 +250,13 @@ export default function CodeMirrorField({ language, value, onChange, onBlur, pla
152
250
  if (!view)
153
251
  return;
154
252
  const current = view.state.doc.toString();
155
- if (current === value)
253
+ if (current === value) {
254
+ emittedRef.current.clear();
255
+ return;
256
+ }
257
+ if (emittedRef.current.has(value))
156
258
  return;
259
+ emittedRef.current.clear();
157
260
  view.dispatch({ changes: { from: 0, to: current.length, insert: value } });
158
261
  }, [value]);
159
262
  return _jsx("div", { ref: hostRef, "data-testid": testId, className: "h-full min-h-0 overflow-hidden" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptengine/inline-editor",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Ptengine Inline 编辑器:画布会话编排、图层树、属性面板与组件库(含内部 canvas-ui)",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,