@coding-script/script-engine 0.0.7 → 0.0.9
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/README.md +123 -11
- package/dist/autocomplete/completion-source.d.ts +15 -1
- package/dist/autocomplete/completion-source.js +29 -203
- package/dist/autocomplete/index.d.ts +1 -1
- package/dist/autocomplete/index.js +1 -1
- package/dist/components/data-types-section.js +1 -0
- package/dist/components/drag-handle.js +1 -0
- package/dist/components/main-function-section.js +3 -1
- package/dist/components/panel-header.js +1 -0
- package/dist/components/toolbar.js +3 -0
- package/dist/components/variables-section.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/languages/groovy.d.ts +3 -0
- package/dist/languages/groovy.js +150 -0
- package/dist/languages/index.d.ts +17 -0
- package/dist/languages/index.js +16 -0
- package/dist/languages/javascript.d.ts +3 -0
- package/dist/languages/javascript.js +155 -0
- package/dist/script-code.d.ts +1 -1
- package/dist/script-code.js +46 -31
- package/dist/type-panel/type-panel.js +3 -1
- package/dist/types/index.d.ts +70 -2
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/@coding-script/script-engine)
|
|
2
2
|
# Script Engine
|
|
3
3
|
|
|
4
|
-
基于 React + CodeMirror 6
|
|
4
|
+
基于 React + CodeMirror 6 的多语言脚本编辑器组件库,内置支持 Groovy 和 JavaScript,提供语法高亮、动态类型自动补全、属性面板等功能。
|
|
5
5
|
|
|
6
6
|
## 功能特性
|
|
7
7
|
|
|
8
|
-
-
|
|
8
|
+
- **多语言支持**:内置 Groovy 和 JavaScript 语言支持,支持通过 `LanguageConfig` 扩展任意语言
|
|
9
|
+
- **语法高亮**:基于 CodeMirror 6,通过 `@codemirror/lang-*` 系列包提供各语言的语法着色
|
|
9
10
|
- **动态类型自动补全**:基于 `ScriptMetadata` 提供变量名补全和点号链式访问补全(如 `request.test.name`)
|
|
10
|
-
-
|
|
11
|
-
-
|
|
11
|
+
- **语言语法提示**:内置各语言的常用语法片段(`if`/`for`/`while`/`return` 等),支持 tab-stop 占位符
|
|
12
|
+
- **代码格式化**:Groovy 内置格式化器,也可通过 `onFormat` 自定义格式化逻辑
|
|
12
13
|
- **属性面板**:右侧侧边栏展示主函数签名、函数入参、绑定参数、数据类型(字段和方法),支持折叠/展开和拖拽调节宽度
|
|
13
14
|
- **脚本说明**:工具栏"脚本说明"按钮,点击展开/收起脚本描述弹框,支持多行文本和 `key: value` 格式高亮
|
|
14
15
|
- **主题切换**:暗色/亮色两套主题,编辑器、补全弹窗、属性面板同步切换,编辑器内部管理主题状态
|
|
15
16
|
- **全屏模式**:支持 CSS 全屏(`position: fixed` 覆盖视口),不影响编辑器内容
|
|
16
17
|
- **自定义工具栏**:通过 `toolbarExtra` 传入任意 React 节点
|
|
17
|
-
-
|
|
18
|
+
- **热更新**:主题、语言和 metadata 变化时通过 Compartment 热更新,不重建编辑器,不丢失用户输入
|
|
18
19
|
|
|
19
20
|
## 安装
|
|
20
21
|
|
|
@@ -77,6 +78,7 @@ function App() {
|
|
|
77
78
|
<ScriptCodeEditor
|
|
78
79
|
value="def run(request){\n return request.count;\n}\n"
|
|
79
80
|
title="Groovy 脚本编辑器"
|
|
81
|
+
language="groovy"
|
|
80
82
|
defaultTheme="dark"
|
|
81
83
|
metadata={metadata}
|
|
82
84
|
enableThemeToggle
|
|
@@ -92,6 +94,32 @@ function App() {
|
|
|
92
94
|
}
|
|
93
95
|
```
|
|
94
96
|
|
|
97
|
+
### JavaScript 用法
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { ScriptCodeEditor } from '@coding-script/script-engine';
|
|
101
|
+
|
|
102
|
+
function App() {
|
|
103
|
+
return (
|
|
104
|
+
<ScriptCodeEditor
|
|
105
|
+
value="function run(request) {\n return request.count;\n}\n"
|
|
106
|
+
title="JavaScript 脚本编辑器"
|
|
107
|
+
language="javascript"
|
|
108
|
+
defaultTheme="dark"
|
|
109
|
+
metadata={metadata}
|
|
110
|
+
enableThemeToggle
|
|
111
|
+
enableFormat
|
|
112
|
+
enableCompile
|
|
113
|
+
enableFullscreen
|
|
114
|
+
onFormat={() => {
|
|
115
|
+
// JavaScript 无内置格式化器,需自行提供
|
|
116
|
+
// 例如使用 prettier
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
95
123
|
## Props
|
|
96
124
|
|
|
97
125
|
| 属性 | 类型 | 默认值 | 说明 |
|
|
@@ -99,7 +127,8 @@ function App() {
|
|
|
99
127
|
| `value` | `string` | `undefined` | 代码内容 |
|
|
100
128
|
| `readonly` | `boolean` | `false` | 是否只读 |
|
|
101
129
|
| `onChange` | `(value: string) => void` | `undefined` | 代码变化回调 |
|
|
102
|
-
| `
|
|
130
|
+
| `language` | `string \| LanguageConfig` | `'groovy'` | 编程语言配置,支持内置语言名或自定义配置 |
|
|
131
|
+
| `placeholder` | `string` | 由语言配置决定 | 空内容占位符(覆盖语言默认值) |
|
|
103
132
|
| `defaultTheme` | `'dark' \| 'light'` | `'dark'` | 初始主题,编辑器内部管理状态 |
|
|
104
133
|
| `onThemeChange` | `(theme: 'dark' \| 'light') => void` | `undefined` | 主题切换通知回调 |
|
|
105
134
|
| `title` | `string` | `undefined` | 工具栏标题 |
|
|
@@ -113,7 +142,7 @@ function App() {
|
|
|
113
142
|
| 属性 | 类型 | 默认值 | 说明 |
|
|
114
143
|
|---|---|---|---|
|
|
115
144
|
| `enableThemeToggle` | `boolean` | `false` | 是否显示主题切换按钮 |
|
|
116
|
-
| `enableFormat` | `boolean` | `false` | 是否显示格式化按钮(需配合 `onFormat`) |
|
|
145
|
+
| `enableFormat` | `boolean` | `false` | 是否显示格式化按钮(需配合 `onFormat` 或 `language.formatter`) |
|
|
117
146
|
| `enableCompile` | `boolean` | `false` | 是否显示编译验证按钮(需配合 `onCompile`) |
|
|
118
147
|
| `enableFullscreen` | `boolean` | `false` | 是否显示全屏按钮 |
|
|
119
148
|
|
|
@@ -121,7 +150,7 @@ function App() {
|
|
|
121
150
|
|
|
122
151
|
| 属性 | 类型 | 默认值 | 说明 |
|
|
123
152
|
|---|---|---|---|
|
|
124
|
-
| `onFormat` | `() => void` | `undefined` |
|
|
153
|
+
| `onFormat` | `() => void` | `undefined` | 格式化代码回调(优先级高于 `language.formatter`) |
|
|
125
154
|
| `onCompile` | `(code: string) => void` | `undefined` | 编译/测试脚本回调 |
|
|
126
155
|
|
|
127
156
|
### 自定义工具栏
|
|
@@ -245,6 +274,87 @@ interface ScriptRequestInfo {
|
|
|
245
274
|
|
|
246
275
|
> **注意**:`metadata` 必须是解析后的 JavaScript 对象,不能是 JSON 字符串。如果从 API 获取的是 JSON 字符串,需要先 `JSON.parse()` 再传入。
|
|
247
276
|
|
|
277
|
+
## 多语言支持
|
|
278
|
+
|
|
279
|
+
### 内置语言
|
|
280
|
+
|
|
281
|
+
组件内置支持以下语言:
|
|
282
|
+
|
|
283
|
+
| 语言名 | 语法高亮 | 语法片段 | 内置格式化器 |
|
|
284
|
+
|---|---|---|---|
|
|
285
|
+
| `'groovy'` | `@codemirror/lang-java` | 21 个(`if`/`for`/`def`/`each`/`collect` 等) | ✅ `GroovyFormatter` |
|
|
286
|
+
| `'javascript'` | `@codemirror/lang-javascript` | 22 个(`if`/`for`/`function`/`=>`/`class` 等) | ❌(可通过 `onFormat` 提供) |
|
|
287
|
+
|
|
288
|
+
使用内置语言只需传入语言名:
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<ScriptCodeEditor language="groovy" />
|
|
292
|
+
<ScriptCodeEditor language="javascript" />
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 自定义语言扩展
|
|
296
|
+
|
|
297
|
+
通过传入 `LanguageConfig` 对象可以支持任意语言:
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { python } from '@codemirror/lang-python';
|
|
301
|
+
|
|
302
|
+
const PYTHON_SNIPPETS = [
|
|
303
|
+
{ label: 'def', type: 'keyword', apply: snippet('def ${name}(${params}):\n\t${}\n') },
|
|
304
|
+
{ label: 'class', type: 'keyword', apply: snippet('class ${ClassName}:\n\tdef __init__(self):\n\t\t${}\n') },
|
|
305
|
+
{ label: 'if', type: 'keyword', apply: snippet('if ${condition}:\n\t${}\n') },
|
|
306
|
+
{ label: 'for', type: 'keyword', apply: snippet('for ${item} in ${iterable}:\n\t${}\n') },
|
|
307
|
+
{ label: 'print', type: 'keyword', apply: snippet('print(${})') },
|
|
308
|
+
{ label: 'return', type: 'keyword', apply: snippet('return ${}') },
|
|
309
|
+
// ... 更多语法片段
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
<ScriptCodeEditor
|
|
313
|
+
language={{
|
|
314
|
+
name: 'python',
|
|
315
|
+
displayName: 'Python',
|
|
316
|
+
extension: () => python(),
|
|
317
|
+
keywordSnippets: PYTHON_SNIPPETS,
|
|
318
|
+
syntaxNodeNames: {
|
|
319
|
+
stringNodes: ['String'],
|
|
320
|
+
commentNodes: ['LineComment', 'BlockComment'],
|
|
321
|
+
},
|
|
322
|
+
placeholder: '请输入 Python 脚本...',
|
|
323
|
+
formatter: (code) => customPythonFormatter(code),
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### LanguageConfig 类型定义
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface LanguageConfig {
|
|
332
|
+
/** 语言标识名(小写) */
|
|
333
|
+
name: string;
|
|
334
|
+
/** 显示名称 */
|
|
335
|
+
displayName: string;
|
|
336
|
+
/** CodeMirror 语言扩展工厂 */
|
|
337
|
+
extension: () => Extension;
|
|
338
|
+
/** 关键字和语法片段列表 */
|
|
339
|
+
keywordSnippets: readonly Completion[];
|
|
340
|
+
/** 语法树节点名(用于判断字符串/注释位置,抑制自动补全) */
|
|
341
|
+
syntaxNodeNames: {
|
|
342
|
+
/** 字符串类节点名 */
|
|
343
|
+
stringNodes: string[];
|
|
344
|
+
/** 注释类节点名 */
|
|
345
|
+
commentNodes: string[];
|
|
346
|
+
};
|
|
347
|
+
/** 默认占位符文本(可选) */
|
|
348
|
+
placeholder?: string;
|
|
349
|
+
/** 内置格式化函数(可选,优先级低于 onFormat prop) */
|
|
350
|
+
formatter?: (code: string) => string;
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 语言切换热更新
|
|
355
|
+
|
|
356
|
+
语言切换时通过 Compartment 热更新,不重建编辑器,不丢失用户已输入的内容。
|
|
357
|
+
|
|
248
358
|
## 自动补全
|
|
249
359
|
|
|
250
360
|
提供 metadata 后,编辑器支持以下补全能力:
|
|
@@ -254,9 +364,11 @@ interface ScriptRequestInfo {
|
|
|
254
364
|
| `re` | 弹出 `request`、`$request` 等变量 |
|
|
255
365
|
| `request.` | 弹出 `count`、`test`、`isSupport` 等字段和方法 |
|
|
256
366
|
| `request.test.` | 弹出 `id`、`name` 等链式访问成员 |
|
|
257
|
-
| `if` / `for` / `while` |
|
|
367
|
+
| `if` / `for` / `while` | 弹出当前语言的语法片段(含 tab-stop 占位符) |
|
|
368
|
+
|
|
369
|
+
不提供 metadata 时,仅启用当前语言的关键字和语法片段补全。
|
|
258
370
|
|
|
259
|
-
|
|
371
|
+
> 补全不会在字符串和注释内触发(通过各语言的语法树节点名判断)。
|
|
260
372
|
|
|
261
373
|
## 本地开发
|
|
262
374
|
|
|
@@ -275,7 +387,7 @@ pnpm run dev:app-pc
|
|
|
275
387
|
|
|
276
388
|
## 技术栈
|
|
277
389
|
|
|
278
|
-
- **编辑器**:[CodeMirror 6](https://codemirror.net/)(`@codemirror/view`、`state`、`autocomplete`、`lang-java`、`theme-one-dark`)
|
|
390
|
+
- **编辑器**:[CodeMirror 6](https://codemirror.net/)(`@codemirror/view`、`state`、`autocomplete`、`lang-java`、`lang-javascript`、`theme-one-dark`)
|
|
279
391
|
- **构建工具**:[Rslib](https://rslib.rs/)(库)+ [Rsbuild](https://rsbuild.dev/)(演示应用)
|
|
280
392
|
- **包管理**:pnpm monorepo(workspaces)
|
|
281
393
|
- **UI**:纯 CSS-in-JS(React `style` 对象),库本身不依赖 Ant Design
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { type CompletionSource } from '@codemirror/autocomplete';
|
|
2
|
-
import type { ScriptMetadata } from '../types';
|
|
2
|
+
import type { LanguageConfig, ScriptMetadata } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* 创建语言无关的自动补全源
|
|
5
|
+
*
|
|
6
|
+
* 当 metadata 存在时,同时提供元数据驱动的类型补全和语言关键字片段补全。
|
|
7
|
+
* 当 metadata 为空时,仅提供关键字片段补全。
|
|
8
|
+
*
|
|
9
|
+
* @param languageConfig 语言配置(提供 keywordSnippets 和 syntaxNodeNames)
|
|
10
|
+
* @param metadata 脚本元数据(可选,提供变量/字段/方法补全)
|
|
11
|
+
*/
|
|
12
|
+
export declare function createCompletionSource(languageConfig: LanguageConfig, metadata?: ScriptMetadata): CompletionSource;
|
|
3
13
|
/**
|
|
4
14
|
* 创建绑定到指定元数据的 Groovy 自动补全源
|
|
5
15
|
* 同时提供元数据驱动的类型补全和 Groovy 常用语法补全
|
|
16
|
+
*
|
|
17
|
+
* @deprecated 请使用 `createCompletionSource(languageConfig, metadata)` 代替
|
|
6
18
|
*/
|
|
7
19
|
export declare function createGroovyCompletionSource(metadata: ScriptMetadata): CompletionSource;
|
|
8
20
|
/**
|
|
9
21
|
* 创建仅包含 Groovy 语法提示的补全源(无元数据时使用)
|
|
22
|
+
*
|
|
23
|
+
* @deprecated 请使用 `createCompletionSource(languageConfig)` 代替
|
|
10
24
|
*/
|
|
11
25
|
export declare function createGroovyKeywordSource(): CompletionSource;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { snippet } from "@codemirror/autocomplete";
|
|
2
1
|
import { syntaxTree } from "@codemirror/language";
|
|
3
2
|
import { resolveChainType } from "./resolve.js";
|
|
3
|
+
import { groovyConfig } from "../languages/groovy.js";
|
|
4
4
|
function formatFunctionSignature(fn) {
|
|
5
5
|
const params = fn.parameters.map((p)=>`${p.name}: ${p.dataType}`).join(', ');
|
|
6
6
|
return `(${params})`;
|
|
@@ -28,170 +28,21 @@ function buildMemberCompletions(typeInfo) {
|
|
|
28
28
|
}
|
|
29
29
|
return options;
|
|
30
30
|
}
|
|
31
|
-
function isInStringOrComment(context) {
|
|
31
|
+
function isInStringOrComment(context, syntaxNodeNames) {
|
|
32
32
|
const node = syntaxTree(context.state).resolveInner(context.pos, -1);
|
|
33
33
|
const name = node.name;
|
|
34
|
-
return
|
|
34
|
+
return syntaxNodeNames.stringNodes.includes(name) || syntaxNodeNames.commentNodes.includes(name);
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
label: 'if',
|
|
39
|
-
detail: '条件语句',
|
|
40
|
-
info: '如果条件为真则执行代码块',
|
|
41
|
-
snippet: 'if (${condition}) {\n\t${}\n}',
|
|
42
|
-
boost: -1
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
label: 'else',
|
|
46
|
-
detail: '否则分支',
|
|
47
|
-
info: '条件为假时执行的代码块',
|
|
48
|
-
snippet: 'else {\n\t${}\n}',
|
|
49
|
-
boost: -2
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
label: 'for',
|
|
53
|
-
detail: '循环语句',
|
|
54
|
-
info: '遍历集合或范围',
|
|
55
|
-
snippet: 'for (${item} in ${collection}) {\n\t${}\n}',
|
|
56
|
-
boost: -1
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
label: 'while',
|
|
60
|
-
detail: '循环语句',
|
|
61
|
-
info: '当条件为真时循环执行',
|
|
62
|
-
snippet: 'while (${condition}) {\n\t${}\n}',
|
|
63
|
-
boost: -1
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
label: 'switch',
|
|
67
|
-
detail: '分支语句',
|
|
68
|
-
info: '多条件分支选择',
|
|
69
|
-
snippet: 'switch (${expression}) {\n\tcase ${value}:\n\t\t${}\n\t\tbreak\n\tdefault:\n\t\tbreak\n}',
|
|
70
|
-
boost: -1
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
label: 'try',
|
|
74
|
-
detail: '异常捕获',
|
|
75
|
-
info: '尝试执行代码并捕获异常',
|
|
76
|
-
snippet: 'try {\n\t${}\n} catch (${Exception} e) {\n\t\n}',
|
|
77
|
-
boost: -1
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
label: 'def',
|
|
81
|
-
detail: '变量/函数定义',
|
|
82
|
-
info: '定义变量或函数',
|
|
83
|
-
snippet: 'def ${name}',
|
|
84
|
-
boost: -1
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
label: 'println',
|
|
88
|
-
detail: '打印输出',
|
|
89
|
-
info: '输出到控制台并换行',
|
|
90
|
-
snippet: 'println(${})',
|
|
91
|
-
boost: 0
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
label: 'print',
|
|
95
|
-
detail: '打印输出',
|
|
96
|
-
info: '输出到控制台不换行',
|
|
97
|
-
snippet: 'print(${})',
|
|
98
|
-
boost: -1
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
label: 'return',
|
|
102
|
-
detail: '返回值',
|
|
103
|
-
info: '从函数中返回结果',
|
|
104
|
-
snippet: 'return ${}',
|
|
105
|
-
boost: 0
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
label: 'each',
|
|
109
|
-
detail: '遍历闭包',
|
|
110
|
-
info: '遍历集合中的每个元素',
|
|
111
|
-
snippet: 'each { ${item} ->\n\t${}\n}',
|
|
112
|
-
boost: -1
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
label: 'collect',
|
|
116
|
-
detail: '转换闭包',
|
|
117
|
-
info: '将集合元素转换为新集合',
|
|
118
|
-
snippet: 'collect { ${item} ->\n\t${}\n}',
|
|
119
|
-
boost: -1
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
label: 'find',
|
|
123
|
-
detail: '查找闭包',
|
|
124
|
-
info: '查找集合中第一个匹配的元素',
|
|
125
|
-
snippet: 'find { ${item} ->\n\t${}\n}',
|
|
126
|
-
boost: -1
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
label: 'findAll',
|
|
130
|
-
detail: '查找全部',
|
|
131
|
-
info: '查找集合中所有匹配的元素',
|
|
132
|
-
snippet: 'findAll { ${item} ->\n\t${}\n}',
|
|
133
|
-
boost: -1
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
label: 'inject',
|
|
137
|
-
detail: '累加闭包',
|
|
138
|
-
info: '对集合进行累加操作',
|
|
139
|
-
snippet: 'inject(${initial}) { ${acc}, ${item} ->\n\t${}\n}',
|
|
140
|
-
boost: -1
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
label: 'assert',
|
|
144
|
-
detail: '断言',
|
|
145
|
-
info: '验证条件是否为真',
|
|
146
|
-
snippet: 'assert ${condition}',
|
|
147
|
-
boost: -1
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
label: 'new',
|
|
151
|
-
detail: '创建实例',
|
|
152
|
-
info: '创建新的对象实例',
|
|
153
|
-
snippet: 'new ${ClassName}(${})',
|
|
154
|
-
boost: -1
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
label: 'class',
|
|
158
|
-
detail: '类定义',
|
|
159
|
-
info: '定义一个新的类',
|
|
160
|
-
snippet: 'class ${ClassName} {\n\t${}\n}',
|
|
161
|
-
boost: -1
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
label: 'interface',
|
|
165
|
-
detail: '接口定义',
|
|
166
|
-
info: '定义一个新的接口',
|
|
167
|
-
snippet: 'interface ${InterfaceName} {\n\t${}\n}',
|
|
168
|
-
boost: -2
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
label: 'throw',
|
|
172
|
-
detail: '抛出异常',
|
|
173
|
-
info: '抛出一个异常对象',
|
|
174
|
-
snippet: 'throw new ${Exception}("${}")',
|
|
175
|
-
boost: -1
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
label: 'import',
|
|
179
|
-
detail: '导入',
|
|
180
|
-
info: '导入外部类或包',
|
|
181
|
-
snippet: 'import ${}',
|
|
182
|
-
boost: -2
|
|
183
|
-
}
|
|
184
|
-
];
|
|
185
|
-
function createGroovyCompletionSource(metadata) {
|
|
36
|
+
function createCompletionSource(languageConfig, metadata) {
|
|
186
37
|
return function(context) {
|
|
187
38
|
try {
|
|
188
|
-
if (isInStringOrComment(context)) return null;
|
|
39
|
+
if (isInStringOrComment(context, languageConfig.syntaxNodeNames)) return null;
|
|
189
40
|
const chainMatch = context.matchBefore(/[\w$]+(?:\.[\w$]+)*\.|[\w$]+/);
|
|
190
41
|
if (!chainMatch) return null;
|
|
191
42
|
const hasDot = chainMatch.text.includes('.');
|
|
192
43
|
const parts = chainMatch.text.split('.');
|
|
193
44
|
const isDotAccess = hasDot && parts.length >= 2;
|
|
194
|
-
if (isDotAccess) {
|
|
45
|
+
if (isDotAccess && metadata) {
|
|
195
46
|
const baseParts = parts.slice(0, -1);
|
|
196
47
|
const currentPrefix = parts[parts.length - 1];
|
|
197
48
|
const resolvedType = resolveChainType(baseParts, metadata);
|
|
@@ -205,28 +56,24 @@ function createGroovyCompletionSource(metadata) {
|
|
|
205
56
|
};
|
|
206
57
|
}
|
|
207
58
|
const options = [];
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
detail: s.detail,
|
|
227
|
-
info: s.info,
|
|
228
|
-
apply: snippet(s.snippet),
|
|
229
|
-
boost: s.boost
|
|
59
|
+
if (metadata) {
|
|
60
|
+
for (const bind of metadata.binds)options.push({
|
|
61
|
+
label: bind.name,
|
|
62
|
+
type: 'variable',
|
|
63
|
+
detail: bind.description ? `${bind.dataType} — ${bind.description}` : bind.dataType,
|
|
64
|
+
info: bind.description || '(绑定变量)',
|
|
65
|
+
boost: 3
|
|
66
|
+
});
|
|
67
|
+
for (const req of metadata.requests)options.push({
|
|
68
|
+
label: req.name,
|
|
69
|
+
type: 'variable',
|
|
70
|
+
detail: req.description ? `${req.dataType} — ${req.description}` : req.dataType,
|
|
71
|
+
info: req.description || '(请求参数)',
|
|
72
|
+
boost: 3
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
for (const s of languageConfig.keywordSnippets)options.push({
|
|
76
|
+
...s
|
|
230
77
|
});
|
|
231
78
|
if (0 === options.length) return null;
|
|
232
79
|
return {
|
|
@@ -239,31 +86,10 @@ function createGroovyCompletionSource(metadata) {
|
|
|
239
86
|
}
|
|
240
87
|
};
|
|
241
88
|
}
|
|
89
|
+
function createGroovyCompletionSource(metadata) {
|
|
90
|
+
return createCompletionSource(groovyConfig, metadata);
|
|
91
|
+
}
|
|
242
92
|
function createGroovyKeywordSource() {
|
|
243
|
-
return
|
|
244
|
-
try {
|
|
245
|
-
if (isInStringOrComment(context)) return null;
|
|
246
|
-
const match = context.matchBefore(/[\w$]+/);
|
|
247
|
-
if (!match) return null;
|
|
248
|
-
const prefix = match.text.toLowerCase();
|
|
249
|
-
const options = [];
|
|
250
|
-
for (const s of GROOVY_SNIPPETS)if (s.label.toLowerCase().startsWith(prefix) || '' === prefix) options.push({
|
|
251
|
-
label: s.label,
|
|
252
|
-
type: 'keyword',
|
|
253
|
-
detail: s.detail,
|
|
254
|
-
info: s.info,
|
|
255
|
-
apply: snippet(s.snippet),
|
|
256
|
-
boost: s.boost
|
|
257
|
-
});
|
|
258
|
-
if (0 === options.length) return null;
|
|
259
|
-
return {
|
|
260
|
-
from: match.from,
|
|
261
|
-
options,
|
|
262
|
-
validFor: /^[\w$]*$/
|
|
263
|
-
};
|
|
264
|
-
} catch {
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
};
|
|
93
|
+
return createCompletionSource(groovyConfig);
|
|
268
94
|
}
|
|
269
|
-
export { createGroovyCompletionSource, createGroovyKeywordSource };
|
|
95
|
+
export { createCompletionSource, createGroovyCompletionSource, createGroovyKeywordSource };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createGroovyCompletionSource, createGroovyKeywordSource } from './completion-source';
|
|
1
|
+
export { createCompletionSource, createGroovyCompletionSource, createGroovyKeywordSource } from './completion-source';
|
|
2
2
|
export { resolveChainType, resolveVariableType, resolveMemberType } from './resolve';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createGroovyCompletionSource, createGroovyKeywordSource } from "./completion-source.js";
|
|
1
|
+
export { createCompletionSource, createGroovyCompletionSource, createGroovyKeywordSource } from "./completion-source.js";
|
|
2
2
|
export { resolveChainType, resolveMemberType, resolveVariableType } from "./resolve.js";
|
|
@@ -2,6 +2,7 @@ import react from "react";
|
|
|
2
2
|
import { SectionHeader } from "./section-header.js";
|
|
3
3
|
import { TypeSection } from "./type-section.js";
|
|
4
4
|
const DataTypesSection = ({ allTypes, expanded, onToggle, colors })=>/*#__PURE__*/ react.createElement("div", {
|
|
5
|
+
className: "data-types-section",
|
|
5
6
|
style: {
|
|
6
7
|
marginTop: 4
|
|
7
8
|
}
|
|
@@ -2,6 +2,7 @@ import react from "react";
|
|
|
2
2
|
const DragHandle = ({ colors, dragging, handleHovered, onMouseDown, onHoverChange })=>{
|
|
3
3
|
const handleActiveColor = dragging || handleHovered ? colors.accent : colors.border;
|
|
4
4
|
return /*#__PURE__*/ react.createElement("div", {
|
|
5
|
+
className: "se-drag-handle",
|
|
5
6
|
onMouseDown: onMouseDown,
|
|
6
7
|
onMouseEnter: ()=>onHoverChange(true),
|
|
7
8
|
onMouseLeave: ()=>onHoverChange(false),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import react from "react";
|
|
2
2
|
import { SectionHeader } from "./section-header.js";
|
|
3
|
-
const MainFunctionSection = ({ metadata, colors })=>/*#__PURE__*/ react.createElement("div",
|
|
3
|
+
const MainFunctionSection = ({ metadata, colors })=>/*#__PURE__*/ react.createElement("div", {
|
|
4
|
+
className: "main-function-section"
|
|
5
|
+
}, /*#__PURE__*/ react.createElement(SectionHeader, {
|
|
4
6
|
colors: colors,
|
|
5
7
|
label: "主函数"
|
|
6
8
|
}), /*#__PURE__*/ react.createElement("div", {
|
|
@@ -2,6 +2,7 @@ import react, { useState } from "react";
|
|
|
2
2
|
const PanelHeader = ({ colors, onCollapse })=>{
|
|
3
3
|
const [collapseHovered, setCollapseHovered] = useState(false);
|
|
4
4
|
return /*#__PURE__*/ react.createElement("div", {
|
|
5
|
+
className: "panel-header",
|
|
5
6
|
style: {
|
|
6
7
|
padding: '8px 12px',
|
|
7
8
|
display: 'flex',
|
|
@@ -121,6 +121,7 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
|
|
|
121
121
|
onThemeChange?.(next);
|
|
122
122
|
};
|
|
123
123
|
return /*#__PURE__*/ react.createElement("div", {
|
|
124
|
+
className: "script-editor-toolbar",
|
|
124
125
|
style: {
|
|
125
126
|
display: 'flex',
|
|
126
127
|
alignItems: 'center',
|
|
@@ -133,6 +134,7 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
|
|
|
133
134
|
fontSize: 13
|
|
134
135
|
}
|
|
135
136
|
}, title && /*#__PURE__*/ react.createElement("span", {
|
|
137
|
+
className: "script-editor-toolbar-title",
|
|
136
138
|
style: {
|
|
137
139
|
color: toolbarText,
|
|
138
140
|
fontWeight: 600,
|
|
@@ -142,6 +144,7 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
|
|
|
142
144
|
textOverflow: 'ellipsis'
|
|
143
145
|
}
|
|
144
146
|
}, title), !title && /*#__PURE__*/ react.createElement("span", {
|
|
147
|
+
className: "script-editor-toolbar-title",
|
|
145
148
|
style: {
|
|
146
149
|
marginRight: 'auto'
|
|
147
150
|
}
|
|
@@ -3,7 +3,9 @@ import { SectionHeader } from "./section-header.js";
|
|
|
3
3
|
import { VariableRow } from "./variable-row.js";
|
|
4
4
|
const VariablesSection = ({ sortedBinds, sortedRequests, colors })=>{
|
|
5
5
|
if (0 === sortedBinds.length && 0 === sortedRequests.length) return null;
|
|
6
|
-
return /*#__PURE__*/ react.createElement("div",
|
|
6
|
+
return /*#__PURE__*/ react.createElement("div", {
|
|
7
|
+
className: "variables-section"
|
|
8
|
+
}, sortedRequests.length > 0 && /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(SectionHeader, {
|
|
7
9
|
colors: colors,
|
|
8
10
|
label: "函数入参"
|
|
9
11
|
}), sortedRequests.map((req)=>/*#__PURE__*/ react.createElement(VariableRow, {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export * from "./script-code";
|
|
|
2
2
|
export * from "./types";
|
|
3
3
|
export { GroovyFormatter, GroovyScriptConvertorUtil } from "./utils/groovy-formatter";
|
|
4
4
|
export type { FormatOptions } from "./utils/groovy-formatter";
|
|
5
|
+
export { BUILTIN_LANGUAGES, resolveLanguageConfig, groovyConfig, javascriptConfig, } from "./languages";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export * from "./script-code.js";
|
|
2
2
|
export * from "./types/index.js";
|
|
3
3
|
export { GroovyFormatter, GroovyScriptConvertorUtil } from "./utils/groovy-formatter.js";
|
|
4
|
+
export { BUILTIN_LANGUAGES, groovyConfig, javascriptConfig, resolveLanguageConfig } from "./languages/index.js";
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { snippet } from "@codemirror/autocomplete";
|
|
2
|
+
import { java } from "@codemirror/lang-java";
|
|
3
|
+
import { GroovyFormatter } from "../utils/groovy-formatter.js";
|
|
4
|
+
const GROOVY_SNIPPETS = [
|
|
5
|
+
{
|
|
6
|
+
label: 'if',
|
|
7
|
+
type: 'keyword',
|
|
8
|
+
boost: -1,
|
|
9
|
+
apply: snippet('if (${condition}) {\n\t${}\n}')
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: 'else',
|
|
13
|
+
type: 'keyword',
|
|
14
|
+
boost: -2,
|
|
15
|
+
apply: snippet('else {\n\t${}\n}')
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'for',
|
|
19
|
+
type: 'keyword',
|
|
20
|
+
boost: -1,
|
|
21
|
+
apply: snippet('for (${item} in ${collection}) {\n\t${}\n}')
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'while',
|
|
25
|
+
type: 'keyword',
|
|
26
|
+
boost: -1,
|
|
27
|
+
apply: snippet('while (${condition}) {\n\t${}\n}')
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'switch',
|
|
31
|
+
type: 'keyword',
|
|
32
|
+
boost: -1,
|
|
33
|
+
apply: snippet('switch (${expression}) {\n\tcase ${value}:\n\t\t${}\n\t\tbreak\n\tdefault:\n\t\tbreak\n}')
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: 'try',
|
|
37
|
+
type: 'keyword',
|
|
38
|
+
boost: -1,
|
|
39
|
+
apply: snippet('try {\n\t${}\n} catch (${Exception} e) {\n\t\n}')
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'def',
|
|
43
|
+
type: 'keyword',
|
|
44
|
+
boost: -1,
|
|
45
|
+
apply: snippet('def ${name}')
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'println',
|
|
49
|
+
type: 'keyword',
|
|
50
|
+
boost: 0,
|
|
51
|
+
apply: snippet('println(${})')
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'print',
|
|
55
|
+
type: 'keyword',
|
|
56
|
+
boost: -1,
|
|
57
|
+
apply: snippet('print(${})')
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: 'return',
|
|
61
|
+
type: 'keyword',
|
|
62
|
+
boost: 0,
|
|
63
|
+
apply: snippet('return ${}')
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'each',
|
|
67
|
+
type: 'keyword',
|
|
68
|
+
boost: -1,
|
|
69
|
+
apply: snippet('each { ${item} ->\n\t${}\n}')
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: 'collect',
|
|
73
|
+
type: 'keyword',
|
|
74
|
+
boost: -1,
|
|
75
|
+
apply: snippet('collect { ${item} ->\n\t${}\n}')
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: 'find',
|
|
79
|
+
type: 'keyword',
|
|
80
|
+
boost: -1,
|
|
81
|
+
apply: snippet('find { ${item} ->\n\t${}\n}')
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: 'findAll',
|
|
85
|
+
type: 'keyword',
|
|
86
|
+
boost: -1,
|
|
87
|
+
apply: snippet('findAll { ${item} ->\n\t${}\n}')
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: 'inject',
|
|
91
|
+
type: 'keyword',
|
|
92
|
+
boost: -1,
|
|
93
|
+
apply: snippet('inject(${initial}) { ${acc}, ${item} ->\n\t${}\n}')
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: 'assert',
|
|
97
|
+
type: 'keyword',
|
|
98
|
+
boost: -1,
|
|
99
|
+
apply: snippet('assert ${condition}')
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: 'new',
|
|
103
|
+
type: 'keyword',
|
|
104
|
+
boost: -1,
|
|
105
|
+
apply: snippet('new ${ClassName}(${})')
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: 'class',
|
|
109
|
+
type: 'keyword',
|
|
110
|
+
boost: -1,
|
|
111
|
+
apply: snippet('class ${ClassName} {\n\t${}\n}')
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
label: 'interface',
|
|
115
|
+
type: 'keyword',
|
|
116
|
+
boost: -2,
|
|
117
|
+
apply: snippet('interface ${InterfaceName} {\n\t${}\n}')
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
label: 'throw',
|
|
121
|
+
type: 'keyword',
|
|
122
|
+
boost: -1,
|
|
123
|
+
apply: snippet('throw new ${Exception}("${}")')
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
label: 'import',
|
|
127
|
+
type: 'keyword',
|
|
128
|
+
boost: -2,
|
|
129
|
+
apply: snippet('import ${}')
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
const groovyConfig = {
|
|
133
|
+
name: 'groovy',
|
|
134
|
+
displayName: 'Groovy',
|
|
135
|
+
extension: ()=>java(),
|
|
136
|
+
keywordSnippets: GROOVY_SNIPPETS,
|
|
137
|
+
syntaxNodeNames: {
|
|
138
|
+
stringNodes: [
|
|
139
|
+
'StringLiteral',
|
|
140
|
+
'CharLiteral'
|
|
141
|
+
],
|
|
142
|
+
commentNodes: [
|
|
143
|
+
'LineComment',
|
|
144
|
+
'BlockComment'
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
placeholder: '请输入 Groovy 脚本...',
|
|
148
|
+
formatter: (code)=>GroovyFormatter.formatScript(code)
|
|
149
|
+
};
|
|
150
|
+
export { groovyConfig };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LanguageConfig } from '../types';
|
|
2
|
+
/** 内置语言配置注册表 */
|
|
3
|
+
export declare const BUILTIN_LANGUAGES: Record<string, LanguageConfig>;
|
|
4
|
+
/**
|
|
5
|
+
* 解析语言配置
|
|
6
|
+
*
|
|
7
|
+
* 接受语言名(字符串)或自定义 LanguageConfig 对象,返回完整的 LanguageConfig。
|
|
8
|
+
* 当传入字符串时,从内置注册表中查找;未找到则抛出错误。
|
|
9
|
+
* 不传参时默认使用 Groovy 配置。
|
|
10
|
+
*
|
|
11
|
+
* @param language 语言名或自定义配置
|
|
12
|
+
* @returns 解析后的完整语言配置
|
|
13
|
+
* @throws 当传入的语言名不在内置注册表中时
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveLanguageConfig(language?: string | LanguageConfig): LanguageConfig;
|
|
16
|
+
export { groovyConfig } from './groovy';
|
|
17
|
+
export { javascriptConfig } from './javascript';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { groovyConfig } from "./groovy.js";
|
|
2
|
+
import { javascriptConfig } from "./javascript.js";
|
|
3
|
+
const BUILTIN_LANGUAGES = {
|
|
4
|
+
groovy: groovyConfig,
|
|
5
|
+
javascript: javascriptConfig
|
|
6
|
+
};
|
|
7
|
+
function resolveLanguageConfig(language) {
|
|
8
|
+
if (null == language) return BUILTIN_LANGUAGES.groovy;
|
|
9
|
+
if ('string' == typeof language) {
|
|
10
|
+
const config = BUILTIN_LANGUAGES[language];
|
|
11
|
+
if (!config) throw new Error(`Unknown language "${language}". Available built-in languages: ${Object.keys(BUILTIN_LANGUAGES).join(', ')}. You can also pass a custom LanguageConfig object.`);
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
return language;
|
|
15
|
+
}
|
|
16
|
+
export { BUILTIN_LANGUAGES, groovyConfig, javascriptConfig, resolveLanguageConfig };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { snippet } from "@codemirror/autocomplete";
|
|
2
|
+
import { javascript } from "@codemirror/lang-javascript";
|
|
3
|
+
const JAVASCRIPT_SNIPPETS = [
|
|
4
|
+
{
|
|
5
|
+
label: 'if',
|
|
6
|
+
type: 'keyword',
|
|
7
|
+
boost: -1,
|
|
8
|
+
apply: snippet('if (${condition}) {\n\t${}\n}')
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
label: 'else',
|
|
12
|
+
type: 'keyword',
|
|
13
|
+
boost: -2,
|
|
14
|
+
apply: snippet('else {\n\t${}\n}')
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
label: 'for',
|
|
18
|
+
type: 'keyword',
|
|
19
|
+
boost: -1,
|
|
20
|
+
apply: snippet('for (let ${i} = 0; ${i} < ${length}; ${i}++) {\n\t${}\n}')
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: 'for...of',
|
|
24
|
+
type: 'keyword',
|
|
25
|
+
boost: -1,
|
|
26
|
+
apply: snippet('for (const ${item} of ${array}) {\n\t${}\n}')
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'for...in',
|
|
30
|
+
type: 'keyword',
|
|
31
|
+
boost: -1,
|
|
32
|
+
apply: snippet('for (const ${key} in ${object}) {\n\t${}\n}')
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'while',
|
|
36
|
+
type: 'keyword',
|
|
37
|
+
boost: -1,
|
|
38
|
+
apply: snippet('while (${condition}) {\n\t${}\n}')
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: 'function',
|
|
42
|
+
type: 'keyword',
|
|
43
|
+
boost: -1,
|
|
44
|
+
apply: snippet('function ${name}(${params}) {\n\t${}\n}')
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: 'const',
|
|
48
|
+
type: 'keyword',
|
|
49
|
+
boost: -1,
|
|
50
|
+
apply: snippet('const ${name} = ${}')
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'let',
|
|
54
|
+
type: 'keyword',
|
|
55
|
+
boost: -1,
|
|
56
|
+
apply: snippet('let ${name} = ${}')
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: '=>',
|
|
60
|
+
type: 'keyword',
|
|
61
|
+
boost: -1,
|
|
62
|
+
apply: snippet('(${params}) => {\n\t${}\n}')
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: 'class',
|
|
66
|
+
type: 'keyword',
|
|
67
|
+
boost: -1,
|
|
68
|
+
apply: snippet('class ${ClassName} {\n\tconstructor(${params}) {\n\t\t${}\n\t}\n}')
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: 'try',
|
|
72
|
+
type: 'keyword',
|
|
73
|
+
boost: -1,
|
|
74
|
+
apply: snippet('try {\n\t${}\n} catch (${error}) {\n\t\n}')
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'switch',
|
|
78
|
+
type: 'keyword',
|
|
79
|
+
boost: -1,
|
|
80
|
+
apply: snippet('switch (${expression}) {\n\tcase ${value}:\n\t\t${}\n\t\tbreak\n\tdefault:\n\t\tbreak\n}')
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: 'return',
|
|
84
|
+
type: 'keyword',
|
|
85
|
+
boost: 0,
|
|
86
|
+
apply: snippet('return ${}')
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'import',
|
|
90
|
+
type: 'keyword',
|
|
91
|
+
boost: -2,
|
|
92
|
+
apply: snippet("import { ${} } from '${module}'")
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'export',
|
|
96
|
+
type: 'keyword',
|
|
97
|
+
boost: -2,
|
|
98
|
+
apply: snippet('export ${}')
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: 'async',
|
|
102
|
+
type: 'keyword',
|
|
103
|
+
boost: -1,
|
|
104
|
+
apply: snippet('async function ${name}(${params}) {\n\t${}\n}')
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: 'await',
|
|
108
|
+
type: 'keyword',
|
|
109
|
+
boost: -1,
|
|
110
|
+
apply: snippet('await ${}')
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
label: 'new',
|
|
114
|
+
type: 'keyword',
|
|
115
|
+
boost: -1,
|
|
116
|
+
apply: snippet('new ${ClassName}(${})')
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
label: 'throw',
|
|
120
|
+
type: 'keyword',
|
|
121
|
+
boost: -1,
|
|
122
|
+
apply: snippet('throw new ${Error}("${}")')
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: 'console.log',
|
|
126
|
+
type: 'keyword',
|
|
127
|
+
boost: 0,
|
|
128
|
+
apply: snippet('console.log(${})')
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: 'Promise',
|
|
132
|
+
type: 'keyword',
|
|
133
|
+
boost: -1,
|
|
134
|
+
apply: snippet('new Promise((${resolve}, ${reject}) => {\n\t${}\n})')
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
const javascriptConfig = {
|
|
138
|
+
name: "javascript",
|
|
139
|
+
displayName: 'JavaScript',
|
|
140
|
+
extension: ()=>javascript(),
|
|
141
|
+
keywordSnippets: JAVASCRIPT_SNIPPETS,
|
|
142
|
+
syntaxNodeNames: {
|
|
143
|
+
stringNodes: [
|
|
144
|
+
'String',
|
|
145
|
+
'TemplateString',
|
|
146
|
+
'RegExp'
|
|
147
|
+
],
|
|
148
|
+
commentNodes: [
|
|
149
|
+
'LineComment',
|
|
150
|
+
'BlockComment'
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
placeholder: '请输入 JavaScript 脚本...'
|
|
154
|
+
};
|
|
155
|
+
export { javascriptConfig };
|
package/dist/script-code.d.ts
CHANGED
package/dist/script-code.js
CHANGED
|
@@ -3,15 +3,14 @@ import { Compartment, EditorState } from "@codemirror/state";
|
|
|
3
3
|
import { EditorView, crosshairCursor, drawSelection, dropCursor, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, placeholder as view_placeholder, rectangularSelection } from "@codemirror/view";
|
|
4
4
|
import { defaultKeymap, history as commands_history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
|
5
5
|
import { HighlightStyle, bracketMatching, defaultHighlightStyle, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting } from "@codemirror/language";
|
|
6
|
-
import { java } from "@codemirror/lang-java";
|
|
7
6
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
8
7
|
import { tags } from "@lezer/highlight";
|
|
9
8
|
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
|
10
|
-
import {
|
|
9
|
+
import { createCompletionSource } from "./autocomplete/index.js";
|
|
10
|
+
import { resolveLanguageConfig } from "./languages/index.js";
|
|
11
11
|
import { TypePanel } from "./type-panel/index.js";
|
|
12
12
|
import { Toolbar } from "./components/toolbar.js";
|
|
13
13
|
import { ExpandSidebarButton } from "./components/expand-sidebar-button.js";
|
|
14
|
-
import { GroovyFormatter } from "./utils/groovy-formatter.js";
|
|
15
14
|
const darkHighlightStyle = HighlightStyle.define([
|
|
16
15
|
{
|
|
17
16
|
tag: tags.keyword,
|
|
@@ -132,16 +131,10 @@ function buildThemeExtensions(theme) {
|
|
|
132
131
|
}));
|
|
133
132
|
return exts;
|
|
134
133
|
}
|
|
135
|
-
function buildAutocompleteExt(metadata) {
|
|
136
|
-
return
|
|
134
|
+
function buildAutocompleteExt(languageConfig, metadata) {
|
|
135
|
+
return autocompletion({
|
|
137
136
|
override: [
|
|
138
|
-
|
|
139
|
-
],
|
|
140
|
-
activateOnTyping: true,
|
|
141
|
-
icons: true
|
|
142
|
-
}) : autocompletion({
|
|
143
|
-
override: [
|
|
144
|
-
createGroovyKeywordSource()
|
|
137
|
+
createCompletionSource(languageConfig, metadata)
|
|
145
138
|
],
|
|
146
139
|
activateOnTyping: true,
|
|
147
140
|
icons: true
|
|
@@ -181,8 +174,11 @@ function buildLayoutExtensions(fontSize, minHeight, maxHeight, isFullscreen) {
|
|
|
181
174
|
});
|
|
182
175
|
}
|
|
183
176
|
const ScriptCodeEditor = (props)=>{
|
|
184
|
-
const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange,
|
|
177
|
+
const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange, language = 'groovy', placeholder, defaultTheme, title, metadata, defaultSidebarOpen, enableThemeToggle, enableFormat, enableCompile, enableFullscreen, toolbar, toolbarExtra, options = {} } = props;
|
|
185
178
|
const { fontSize = 14, minHeight = 300, maxHeight = 300 } = options;
|
|
179
|
+
const languageConfig = resolveLanguageConfig(language);
|
|
180
|
+
const effectivePlaceholder = placeholder ?? languageConfig.placeholder ?? '';
|
|
181
|
+
const effectiveFormatter = onFormat ?? (languageConfig.formatter ? (code)=>languageConfig.formatter(code) : void 0);
|
|
186
182
|
const [internalTheme, setInternalTheme] = useState(defaultTheme ?? 'dark');
|
|
187
183
|
useEffect(()=>{
|
|
188
184
|
if (void 0 !== defaultTheme) setInternalTheme(defaultTheme);
|
|
@@ -194,6 +190,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
194
190
|
const themeCompartmentRef = useRef(new Compartment());
|
|
195
191
|
const autocompleteCompartmentRef = useRef(new Compartment());
|
|
196
192
|
const layoutCompartmentRef = useRef(new Compartment());
|
|
193
|
+
const languageCompartmentRef = useRef(new Compartment());
|
|
197
194
|
const onChangeRef = useRef(onChange);
|
|
198
195
|
onChangeRef.current = onChange;
|
|
199
196
|
const metadataRef = useRef(metadata);
|
|
@@ -227,6 +224,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
227
224
|
const themeCompartment = themeCompartmentRef.current;
|
|
228
225
|
const acCompartment = autocompleteCompartmentRef.current;
|
|
229
226
|
const layoutCompartment = layoutCompartmentRef.current;
|
|
227
|
+
const langCompartment = languageCompartmentRef.current;
|
|
230
228
|
const extensions = [
|
|
231
229
|
lineNumbers(),
|
|
232
230
|
highlightActiveLineGutter(),
|
|
@@ -248,14 +246,14 @@ const ScriptCodeEditor = (props)=>{
|
|
|
248
246
|
...completionKeymap,
|
|
249
247
|
indentWithTab
|
|
250
248
|
]),
|
|
251
|
-
|
|
252
|
-
view_placeholder(
|
|
249
|
+
langCompartment.of(languageConfig.extension()),
|
|
250
|
+
view_placeholder(effectivePlaceholder),
|
|
253
251
|
EditorView.updateListener.of((update)=>{
|
|
254
252
|
if (update.docChanged && onChangeRef.current) onChangeRef.current(update.state.doc.toString());
|
|
255
253
|
}),
|
|
256
254
|
layoutCompartment.of(buildLayoutExtensions(fontSize, minHeight, maxHeight, false)),
|
|
257
255
|
themeCompartment.of(buildThemeExtensions(internalTheme)),
|
|
258
|
-
acCompartment.of(buildAutocompleteExt(metadataRef.current))
|
|
256
|
+
acCompartment.of(buildAutocompleteExt(languageConfig, metadataRef.current))
|
|
259
257
|
];
|
|
260
258
|
if (readonly) extensions.push(EditorState.readOnly.of(true));
|
|
261
259
|
const state = EditorState.create({
|
|
@@ -274,7 +272,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
274
272
|
fontSize,
|
|
275
273
|
minHeight,
|
|
276
274
|
maxHeight,
|
|
277
|
-
|
|
275
|
+
effectivePlaceholder,
|
|
278
276
|
readonly
|
|
279
277
|
]);
|
|
280
278
|
useEffect(()=>{
|
|
@@ -299,10 +297,22 @@ const ScriptCodeEditor = (props)=>{
|
|
|
299
297
|
useEffect(()=>{
|
|
300
298
|
if (!viewRef.current) return;
|
|
301
299
|
viewRef.current.dispatch({
|
|
302
|
-
effects: autocompleteCompartmentRef.current.reconfigure(buildAutocompleteExt(metadata))
|
|
300
|
+
effects: autocompleteCompartmentRef.current.reconfigure(buildAutocompleteExt(languageConfig, metadata))
|
|
303
301
|
});
|
|
304
302
|
}, [
|
|
305
|
-
metadata
|
|
303
|
+
metadata,
|
|
304
|
+
languageConfig
|
|
305
|
+
]);
|
|
306
|
+
useEffect(()=>{
|
|
307
|
+
if (!viewRef.current) return;
|
|
308
|
+
viewRef.current.dispatch({
|
|
309
|
+
effects: [
|
|
310
|
+
languageCompartmentRef.current.reconfigure(languageConfig.extension()),
|
|
311
|
+
autocompleteCompartmentRef.current.reconfigure(buildAutocompleteExt(languageConfig, metadata))
|
|
312
|
+
]
|
|
313
|
+
});
|
|
314
|
+
}, [
|
|
315
|
+
languageConfig
|
|
306
316
|
]);
|
|
307
317
|
useEffect(()=>{
|
|
308
318
|
if (!viewRef.current) return;
|
|
@@ -326,19 +336,21 @@ const ScriptCodeEditor = (props)=>{
|
|
|
326
336
|
if (onCompile && viewRef.current) onCompile(viewRef.current.state.doc.toString());
|
|
327
337
|
};
|
|
328
338
|
const handleFormat = ()=>{
|
|
329
|
-
if (viewRef.current)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
339
|
+
if (!viewRef.current) return;
|
|
340
|
+
const code = viewRef.current.state.doc.toString();
|
|
341
|
+
const formatter = effectiveFormatter;
|
|
342
|
+
if (!formatter) return;
|
|
343
|
+
const formatted = formatter(code);
|
|
344
|
+
if ('string' == typeof formatted && formatted !== code) viewRef.current.dispatch({
|
|
345
|
+
changes: {
|
|
346
|
+
from: 0,
|
|
347
|
+
to: viewRef.current.state.doc.length,
|
|
348
|
+
insert: formatted
|
|
349
|
+
}
|
|
350
|
+
});
|
|
340
351
|
};
|
|
341
352
|
return /*#__PURE__*/ react.createElement("div", {
|
|
353
|
+
className: "script-editor",
|
|
342
354
|
style: isFullscreen ? {
|
|
343
355
|
position: 'fixed',
|
|
344
356
|
top: 0,
|
|
@@ -362,7 +374,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
362
374
|
},
|
|
363
375
|
enableThemeToggle: enableThemeToggle,
|
|
364
376
|
enableFormat: enableFormat,
|
|
365
|
-
onFormat: readonly ? void 0 : onFormat ?? handleFormat,
|
|
377
|
+
onFormat: readonly || !effectiveFormatter ? void 0 : onFormat ?? handleFormat,
|
|
366
378
|
enableCompile: enableCompile,
|
|
367
379
|
onCompile: onCompile ? handleCompile : void 0,
|
|
368
380
|
enableFullscreen: enableFullscreen,
|
|
@@ -372,6 +384,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
372
384
|
toolbarExtra: toolbarExtra,
|
|
373
385
|
description: metadata?.description
|
|
374
386
|
}), /*#__PURE__*/ react.createElement("div", {
|
|
387
|
+
className: "script-editor-body",
|
|
375
388
|
style: {
|
|
376
389
|
display: 'flex',
|
|
377
390
|
border: `1px solid ${borderColor}`,
|
|
@@ -382,6 +395,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
382
395
|
minHeight: isFullscreen ? 0 : void 0
|
|
383
396
|
}
|
|
384
397
|
}, /*#__PURE__*/ react.createElement("div", {
|
|
398
|
+
className: "script-editor-code",
|
|
385
399
|
style: {
|
|
386
400
|
flex: 1,
|
|
387
401
|
minWidth: 0,
|
|
@@ -394,6 +408,7 @@ const ScriptCodeEditor = (props)=>{
|
|
|
394
408
|
onClick: ()=>setSidebarOpen(true)
|
|
395
409
|
}), /*#__PURE__*/ react.createElement("div", {
|
|
396
410
|
ref: editorContainerRef,
|
|
411
|
+
className: "script-editor-codemirror",
|
|
397
412
|
style: isFullscreen ? {
|
|
398
413
|
height: '100%'
|
|
399
414
|
} : void 0
|
|
@@ -72,6 +72,7 @@ const TypePanel = ({ metadata, theme, minHeight, maxHeight, width, onWidthChange
|
|
|
72
72
|
]);
|
|
73
73
|
return /*#__PURE__*/ react.createElement("div", {
|
|
74
74
|
ref: panelRef,
|
|
75
|
+
className: "type-panel",
|
|
75
76
|
style: {
|
|
76
77
|
width,
|
|
77
78
|
flexShrink: 0,
|
|
@@ -89,6 +90,7 @@ const TypePanel = ({ metadata, theme, minHeight, maxHeight, width, onWidthChange
|
|
|
89
90
|
onMouseDown: onHandleMouseDown,
|
|
90
91
|
onHoverChange: setHandleHovered
|
|
91
92
|
}), /*#__PURE__*/ react.createElement("div", {
|
|
93
|
+
className: "type-panel-body",
|
|
92
94
|
style: {
|
|
93
95
|
flex: 1,
|
|
94
96
|
minWidth: 0,
|
|
@@ -105,7 +107,7 @@ const TypePanel = ({ metadata, theme, minHeight, maxHeight, width, onWidthChange
|
|
|
105
107
|
colors: colors,
|
|
106
108
|
onCollapse: onCollapse
|
|
107
109
|
}), /*#__PURE__*/ react.createElement("div", {
|
|
108
|
-
className: SCROLL_CLASS
|
|
110
|
+
className: `type-panel-content ${SCROLL_CLASS}`,
|
|
109
111
|
style: {
|
|
110
112
|
flex: 1,
|
|
111
113
|
overflowY: 'auto',
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
+
import type { Completion } from '@codemirror/autocomplete';
|
|
3
|
+
import type { Extension } from '@codemirror/state';
|
|
2
4
|
/** 工具栏按钮属性 */
|
|
3
5
|
export interface ToolbarButtonProps {
|
|
4
6
|
/** 按钮内容(支持 ReactNode,可包含图标 + 文字) */
|
|
@@ -79,6 +81,26 @@ export interface ScriptCodeEditorProps {
|
|
|
79
81
|
onChange?: (value: string) => void;
|
|
80
82
|
/** 占位符 */
|
|
81
83
|
placeholder?: string;
|
|
84
|
+
/**
|
|
85
|
+
* 编程语言配置(默认 'groovy')。
|
|
86
|
+
*
|
|
87
|
+
* - 传入内置语言名:`'groovy'` | `'javascript'`
|
|
88
|
+
* - 传入自定义 `LanguageConfig` 对象以支持其他语言
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // 内置语言
|
|
92
|
+
* <ScriptCodeEditor language="javascript" />
|
|
93
|
+
*
|
|
94
|
+
* // 自定义语言
|
|
95
|
+
* <ScriptCodeEditor language={{
|
|
96
|
+
* name: 'python',
|
|
97
|
+
* displayName: 'Python',
|
|
98
|
+
* extension: () => python(),
|
|
99
|
+
* keywordSnippets: PYTHON_SNIPPETS,
|
|
100
|
+
* syntaxNodeNames: { stringNodes: ['String'], commentNodes: ['LineComment', 'BlockComment'] },
|
|
101
|
+
* }} />
|
|
102
|
+
*/
|
|
103
|
+
language?: string | LanguageConfig;
|
|
82
104
|
/** 初始主题(默认 'dark'),编辑器内部管理状态 */
|
|
83
105
|
defaultTheme?: 'dark' | 'light';
|
|
84
106
|
/** 主题切换通知回调(可选) */
|
|
@@ -91,13 +113,13 @@ export interface ScriptCodeEditorProps {
|
|
|
91
113
|
defaultSidebarOpen?: boolean;
|
|
92
114
|
/** 是否显示主题切换按钮(默认 false) */
|
|
93
115
|
enableThemeToggle?: boolean;
|
|
94
|
-
/** 是否显示格式化按钮(默认 false,需配合 onFormat 使用) */
|
|
116
|
+
/** 是否显示格式化按钮(默认 false,需配合 onFormat 或 language.formatter 使用) */
|
|
95
117
|
enableFormat?: boolean;
|
|
96
118
|
/** 是否显示编译验证按钮(默认 false,需配合 onCompile 使用) */
|
|
97
119
|
enableCompile?: boolean;
|
|
98
120
|
/** 是否显示全屏按钮(默认 false) */
|
|
99
121
|
enableFullscreen?: boolean;
|
|
100
|
-
/** 格式化代码回调(enableFormat
|
|
122
|
+
/** 格式化代码回调(enableFormat 时提供,优先级高于 language.formatter) */
|
|
101
123
|
onFormat?: () => void;
|
|
102
124
|
/** 编译/测试脚本回调(enableCompile 时需提供) */
|
|
103
125
|
onCompile?: (code: string) => void;
|
|
@@ -115,3 +137,49 @@ export interface ScriptCodeEditorProps {
|
|
|
115
137
|
maxHeight?: number;
|
|
116
138
|
};
|
|
117
139
|
}
|
|
140
|
+
/** 格式化选项 */
|
|
141
|
+
export interface FormatOptions {
|
|
142
|
+
/** 缩进大小(空格数) */
|
|
143
|
+
indentSize?: number;
|
|
144
|
+
/** 操作符周围添加空格 */
|
|
145
|
+
addSpacesAroundOperators?: boolean;
|
|
146
|
+
/** 格式化注释 */
|
|
147
|
+
formatComments?: boolean;
|
|
148
|
+
/** 最大行长度 */
|
|
149
|
+
maxLineLength?: number;
|
|
150
|
+
/** 保留空行 */
|
|
151
|
+
preserveEmptyLines?: boolean;
|
|
152
|
+
}
|
|
153
|
+
/** 格式化函数签名 */
|
|
154
|
+
export type FormatFn = (code: string, options?: FormatOptions) => string;
|
|
155
|
+
/**
|
|
156
|
+
* 语言配置接口
|
|
157
|
+
*
|
|
158
|
+
* 封装一个编程语言在编辑器中所需的全部差异点:
|
|
159
|
+
* - CodeMirror 语法高亮扩展
|
|
160
|
+
* - 关键字/语法片段
|
|
161
|
+
* - AST 节点名(用于判断字符串/注释位置,抑制自动补全)
|
|
162
|
+
* - 默认占位符文本
|
|
163
|
+
* - 可选的内置格式化器
|
|
164
|
+
*/
|
|
165
|
+
export interface LanguageConfig {
|
|
166
|
+
/** 语言标识名(小写),如 'groovy'、'javascript' */
|
|
167
|
+
name: string;
|
|
168
|
+
/** 显示名称,如 'Groovy'、'JavaScript' */
|
|
169
|
+
displayName: string;
|
|
170
|
+
/** CodeMirror 语言扩展工厂 */
|
|
171
|
+
extension: () => Extension;
|
|
172
|
+
/** 关键字和语法片段列表 */
|
|
173
|
+
keywordSnippets: readonly Completion[];
|
|
174
|
+
/** 各语言的语法树节点名(用于 isInStringOrComment 判断) */
|
|
175
|
+
syntaxNodeNames: {
|
|
176
|
+
/** 字符串类节点名,如 Java: ['StringLiteral'],JS: ['String', 'TemplateString'] */
|
|
177
|
+
stringNodes: string[];
|
|
178
|
+
/** 注释类节点名,如 ['LineComment', 'BlockComment'] */
|
|
179
|
+
commentNodes: string[];
|
|
180
|
+
};
|
|
181
|
+
/** 默认占位符文本 */
|
|
182
|
+
placeholder?: string;
|
|
183
|
+
/** 可选的内置格式化函数(优先级低于 onFormat prop) */
|
|
184
|
+
formatter?: FormatFn;
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coding-script/script-engine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "script-engine components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coding-script",
|
|
@@ -31,10 +31,12 @@
|
|
|
31
31
|
"@codemirror/autocomplete": "^6.18.0",
|
|
32
32
|
"@codemirror/commands": "^6.10.2",
|
|
33
33
|
"@codemirror/lang-java": "^6.0.2",
|
|
34
|
+
"@codemirror/lang-javascript": "^6.2.0",
|
|
34
35
|
"@codemirror/language": "^6.12.2",
|
|
35
36
|
"@codemirror/state": "^6.5.4",
|
|
36
37
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
37
|
-
"@codemirror/view": "^6.39.16"
|
|
38
|
+
"@codemirror/view": "^6.39.16",
|
|
39
|
+
"@lezer/highlight": "^1.2.3"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
42
|
"react": ">=18",
|