@fc-components/monaco-editor 0.1.17 → 0.1.18
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/dist/index.d.ts +2 -0
- package/dist/monaco-editor.cjs.development.js +457 -0
- package/dist/monaco-editor.cjs.development.js.map +1 -1
- package/dist/monaco-editor.cjs.production.min.js +1 -1
- package/dist/monaco-editor.cjs.production.min.js.map +1 -1
- package/dist/monaco-editor.esm.js +457 -1
- package/dist/monaco-editor.esm.js.map +1 -1
- package/dist/sql/completion/getCompletionProvider.d.ts +4 -0
- package/dist/sql/index.d.ts +17 -0
- package/dist/sql/sql.d.ts +85 -0
- package/dist/sql/types.d.ts +8 -0
- package/dist/sql/validation.d.ts +2 -0
- package/package.json +1 -1
- package/src/index.tsx +2 -0
- package/src/sql/README.md +140 -0
- package/src/sql/completion/getCompletionProvider.ts +125 -0
- package/src/sql/index.tsx +263 -0
- package/src/sql/sql.ts +250 -0
- package/src/sql/types.ts +8 -0
- package/src/sql/validation.ts +92 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as monaco from 'monaco-editor';
|
|
2
|
+
export declare const getSqlCompletionProvider: () => {
|
|
3
|
+
provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position, _context: monaco.languages.CompletionContext, _token: monaco.CancellationToken): monaco.languages.ProviderResult<monaco.languages.CompletionList>;
|
|
4
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api';
|
|
3
|
+
interface SqlEditorProps {
|
|
4
|
+
size?: 'small' | 'middle' | 'large';
|
|
5
|
+
theme?: 'light' | 'dark';
|
|
6
|
+
value?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
enableAutocomplete?: boolean;
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onChange?: (value: string) => void;
|
|
12
|
+
onEnter?: (value: string) => void;
|
|
13
|
+
onBlur?: (value: string) => void;
|
|
14
|
+
editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void;
|
|
15
|
+
}
|
|
16
|
+
export default function SqlEditor(props: SqlEditorProps): React.JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export declare const languageConfiguration: {
|
|
2
|
+
wordPattern: RegExp;
|
|
3
|
+
comments: {
|
|
4
|
+
lineComment: string;
|
|
5
|
+
blockComment: [string, string];
|
|
6
|
+
};
|
|
7
|
+
brackets: any;
|
|
8
|
+
autoClosingPairs: any;
|
|
9
|
+
surroundingPairs: any;
|
|
10
|
+
folding: {
|
|
11
|
+
offSide: boolean;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare const language: {
|
|
15
|
+
defaultToken: string;
|
|
16
|
+
tokenPostfix: string;
|
|
17
|
+
ignoreCase: boolean;
|
|
18
|
+
brackets: {
|
|
19
|
+
open: string;
|
|
20
|
+
close: string;
|
|
21
|
+
token: string;
|
|
22
|
+
}[];
|
|
23
|
+
keywords: string[];
|
|
24
|
+
operators: string[];
|
|
25
|
+
builtinFunctions: string[];
|
|
26
|
+
escapes: RegExp;
|
|
27
|
+
digits: RegExp;
|
|
28
|
+
octaldigits: RegExp;
|
|
29
|
+
hexdigits: RegExp;
|
|
30
|
+
regexpctl: RegExp;
|
|
31
|
+
regexpattern: RegExp;
|
|
32
|
+
tokenizer: {
|
|
33
|
+
root: ((string | RegExp)[] | {
|
|
34
|
+
include: string;
|
|
35
|
+
} | (RegExp | {
|
|
36
|
+
cases: {
|
|
37
|
+
'@builtinFunctions': string;
|
|
38
|
+
'@default': string;
|
|
39
|
+
};
|
|
40
|
+
})[] | (RegExp | {
|
|
41
|
+
cases: {
|
|
42
|
+
'@keywords': string;
|
|
43
|
+
'@default': string;
|
|
44
|
+
};
|
|
45
|
+
})[])[];
|
|
46
|
+
whitespace: (string | RegExp)[][];
|
|
47
|
+
comments: ((string | RegExp)[] | (RegExp | {
|
|
48
|
+
token: string;
|
|
49
|
+
next: string;
|
|
50
|
+
})[])[];
|
|
51
|
+
comment: ((string | RegExp)[] | (RegExp | {
|
|
52
|
+
token: string;
|
|
53
|
+
next: string;
|
|
54
|
+
})[])[];
|
|
55
|
+
'pseudo-columns': (RegExp | {
|
|
56
|
+
cases: {
|
|
57
|
+
'@keywords': string;
|
|
58
|
+
'@default': string;
|
|
59
|
+
};
|
|
60
|
+
})[][];
|
|
61
|
+
builtinVariables: (RegExp | {
|
|
62
|
+
cases: {
|
|
63
|
+
'@keywords': string;
|
|
64
|
+
'@default': string;
|
|
65
|
+
};
|
|
66
|
+
})[][];
|
|
67
|
+
numbers: (string | RegExp)[][];
|
|
68
|
+
strings: (RegExp | {
|
|
69
|
+
token: string;
|
|
70
|
+
next: string;
|
|
71
|
+
})[][];
|
|
72
|
+
string: ((string | RegExp)[] | (RegExp | {
|
|
73
|
+
token: string;
|
|
74
|
+
next: string;
|
|
75
|
+
})[])[];
|
|
76
|
+
string_double: ((string | RegExp)[] | (RegExp | {
|
|
77
|
+
token: string;
|
|
78
|
+
next: string;
|
|
79
|
+
})[])[];
|
|
80
|
+
string_backtick: ((string | RegExp)[] | (RegExp | {
|
|
81
|
+
token: string;
|
|
82
|
+
next: string;
|
|
83
|
+
})[])[];
|
|
84
|
+
};
|
|
85
|
+
};
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SQL Editor
|
|
2
|
+
|
|
3
|
+
这是一个基于 Monaco Editor 和 `monaco-sql-languages` 的 SQL 编辑器组件。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- ✨ 完整的 SQL 语法高亮
|
|
8
|
+
- 🎯 智能代码补全(关键字、函数等)
|
|
9
|
+
- ✅ SQL 语法验证(括号、引号匹配等)
|
|
10
|
+
- 🎨 支持浅色和深色主题
|
|
11
|
+
- 📏 支持不同尺寸(small, middle, large)
|
|
12
|
+
- ♿ 只读和禁用模式
|
|
13
|
+
- 🎹 快捷键支持(Ctrl+Enter 执行)
|
|
14
|
+
|
|
15
|
+
## 使用方法
|
|
16
|
+
|
|
17
|
+
### 基本用法
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { SqlMonacoEditor } from '@fc-components/monaco-editor';
|
|
21
|
+
|
|
22
|
+
function MyComponent() {
|
|
23
|
+
const [sql, setSql] = React.useState('SELECT * FROM users;');
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div style={{ height: '400px' }}>
|
|
27
|
+
<SqlMonacoEditor value={sql} onChange={setSql} placeholder='Enter SQL query...' />
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 完整示例
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import React, { useState } from 'react';
|
|
37
|
+
import { SqlMonacoEditor } from '@fc-components/monaco-editor';
|
|
38
|
+
|
|
39
|
+
function SqlQueryBuilder() {
|
|
40
|
+
const [sql, setSql] = useState('');
|
|
41
|
+
const [result, setResult] = useState<any>(null);
|
|
42
|
+
|
|
43
|
+
const handleChange = (value: string) => {
|
|
44
|
+
setSql(value);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleExecute = (value: string) => {
|
|
48
|
+
console.log('执行 SQL:', value);
|
|
49
|
+
// 发送到后端执行
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleBlur = (value: string) => {
|
|
53
|
+
console.log('SQL 失焦:', value);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<div style={{ height: '300px', border: '1px solid #ccc', marginBottom: '20px' }}>
|
|
59
|
+
<SqlMonacoEditor
|
|
60
|
+
size='large'
|
|
61
|
+
theme='light'
|
|
62
|
+
value={sql}
|
|
63
|
+
placeholder='输入 SQL 查询...'
|
|
64
|
+
enableAutocomplete={true}
|
|
65
|
+
onChange={handleChange}
|
|
66
|
+
onEnter={handleExecute}
|
|
67
|
+
onBlur={handleBlur}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
{result && (
|
|
71
|
+
<div>
|
|
72
|
+
<h3>查询结果:</h3>
|
|
73
|
+
<pre>{JSON.stringify(result, null, 2)}</pre>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default SqlQueryBuilder;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Props
|
|
84
|
+
|
|
85
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
86
|
+
| -------------------- | ------------------------------ | -------- | ---------------------- |
|
|
87
|
+
| `size` | 'small' \| 'middle' \| 'large' | 'middle' | 编辑器尺寸 |
|
|
88
|
+
| `theme` | 'light' \| 'dark' | 'light' | 主题 |
|
|
89
|
+
| `value` | string | '' | 编辑器内容 |
|
|
90
|
+
| `placeholder` | string | - | 占位符文本 |
|
|
91
|
+
| `enableAutocomplete` | boolean | true | 启用代码补全 |
|
|
92
|
+
| `readOnly` | boolean | false | 只读模式 |
|
|
93
|
+
| `disabled` | boolean | false | 禁用编辑 |
|
|
94
|
+
| `onChange` | (value: string) => void | - | 内容变化回调 |
|
|
95
|
+
| `onEnter` | (value: string) => void | - | Ctrl+Enter 时触发 |
|
|
96
|
+
| `onBlur` | (value: string) => void | - | 编辑器失焦时触发 |
|
|
97
|
+
| `editorDidMount` | (editor) => void | - | 编辑器挂载完成后的回调 |
|
|
98
|
+
|
|
99
|
+
## 快捷键
|
|
100
|
+
|
|
101
|
+
- **Ctrl+Enter** 或 **Cmd+Enter**: 触发 `onEnter` 事件(通常用于执行查询)
|
|
102
|
+
- **Ctrl+Z** 或 **Cmd+Z**: 撤销
|
|
103
|
+
- **Ctrl+Y** 或 **Cmd+Y**: 重做
|
|
104
|
+
- **Ctrl+F** 或 **Cmd+F**: 查找
|
|
105
|
+
- **Ctrl+H** 或 **Cmd+H**: 查找替换
|
|
106
|
+
- **Ctrl+L**: 选中当前行
|
|
107
|
+
|
|
108
|
+
## 支持的 SQL 关键字
|
|
109
|
+
|
|
110
|
+
包括 SELECT, FROM, WHERE, JOIN, GROUP BY, ORDER BY, INSERT, UPDATE, DELETE, CREATE, DROP 等。
|
|
111
|
+
|
|
112
|
+
## 支持的 SQL 函数
|
|
113
|
+
|
|
114
|
+
- 聚合函数: COUNT, SUM, AVG, MIN, MAX
|
|
115
|
+
- 字符串函数: UPPER, LOWER, LENGTH, SUBSTRING, TRIM, CONCAT
|
|
116
|
+
- 数值函数: ROUND, ABS
|
|
117
|
+
- 日期函数: DATE, NOW, YEAR, MONTH, DAY
|
|
118
|
+
- 其他函数: COALESCE, NULLIF, IFNULL
|
|
119
|
+
|
|
120
|
+
## 验证
|
|
121
|
+
|
|
122
|
+
编辑器会自动检测以下 SQL 语法问题:
|
|
123
|
+
|
|
124
|
+
- 未闭合的单引号、双引号和反引号
|
|
125
|
+
- 未匹配的括号
|
|
126
|
+
- 其他基本的语法问题
|
|
127
|
+
|
|
128
|
+
## 主题定制
|
|
129
|
+
|
|
130
|
+
如需自定义主题,可以添加 CSS 规则覆盖默认样式:
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
.sql-light {
|
|
134
|
+
/* 浅色主题样式 */
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.sql-dark {
|
|
138
|
+
/* 深色主题样式 */
|
|
139
|
+
}
|
|
140
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as monaco from 'monaco-editor';
|
|
2
|
+
|
|
3
|
+
const SQL_KEYWORDS = [
|
|
4
|
+
'SELECT',
|
|
5
|
+
'FROM',
|
|
6
|
+
'WHERE',
|
|
7
|
+
'AND',
|
|
8
|
+
'OR',
|
|
9
|
+
'NOT',
|
|
10
|
+
'JOIN',
|
|
11
|
+
'INNER',
|
|
12
|
+
'LEFT',
|
|
13
|
+
'RIGHT',
|
|
14
|
+
'FULL',
|
|
15
|
+
'OUTER',
|
|
16
|
+
'ON',
|
|
17
|
+
'ORDER',
|
|
18
|
+
'BY',
|
|
19
|
+
'GROUP',
|
|
20
|
+
'HAVING',
|
|
21
|
+
'LIMIT',
|
|
22
|
+
'OFFSET',
|
|
23
|
+
'INSERT',
|
|
24
|
+
'INTO',
|
|
25
|
+
'VALUES',
|
|
26
|
+
'UPDATE',
|
|
27
|
+
'SET',
|
|
28
|
+
'DELETE',
|
|
29
|
+
'CREATE',
|
|
30
|
+
'TABLE',
|
|
31
|
+
'ALTER',
|
|
32
|
+
'DROP',
|
|
33
|
+
'PRIMARY',
|
|
34
|
+
'KEY',
|
|
35
|
+
'FOREIGN',
|
|
36
|
+
'CONSTRAINT',
|
|
37
|
+
'UNIQUE',
|
|
38
|
+
'INDEX',
|
|
39
|
+
'VIEW',
|
|
40
|
+
'DATABASE',
|
|
41
|
+
'SCHEMA',
|
|
42
|
+
'AS',
|
|
43
|
+
'DISTINCT',
|
|
44
|
+
'CASE',
|
|
45
|
+
'WHEN',
|
|
46
|
+
'THEN',
|
|
47
|
+
'ELSE',
|
|
48
|
+
'END',
|
|
49
|
+
'CAST',
|
|
50
|
+
'BETWEEN',
|
|
51
|
+
'IN',
|
|
52
|
+
'LIKE',
|
|
53
|
+
'IS',
|
|
54
|
+
'NULL',
|
|
55
|
+
'TRUE',
|
|
56
|
+
'FALSE',
|
|
57
|
+
'WITH',
|
|
58
|
+
'UNION',
|
|
59
|
+
'EXCEPT',
|
|
60
|
+
'INTERSECT',
|
|
61
|
+
'ASC',
|
|
62
|
+
'DESC',
|
|
63
|
+
'ALL',
|
|
64
|
+
'ANY',
|
|
65
|
+
'EXISTS',
|
|
66
|
+
'CROSS',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const SQL_FUNCTIONS = [
|
|
70
|
+
{ name: 'COUNT', signature: 'COUNT(expression)', description: 'Returns the number of rows' },
|
|
71
|
+
{ name: 'SUM', signature: 'SUM(expression)', description: 'Returns the sum of values' },
|
|
72
|
+
{ name: 'AVG', signature: 'AVG(expression)', description: 'Returns the average value' },
|
|
73
|
+
{ name: 'MIN', signature: 'MIN(expression)', description: 'Returns the minimum value' },
|
|
74
|
+
{ name: 'MAX', signature: 'MAX(expression)', description: 'Returns the maximum value' },
|
|
75
|
+
{ name: 'UPPER', signature: 'UPPER(string)', description: 'Converts string to uppercase' },
|
|
76
|
+
{ name: 'LOWER', signature: 'LOWER(string)', description: 'Converts string to lowercase' },
|
|
77
|
+
{ name: 'LENGTH', signature: 'LENGTH(string)', description: 'Returns the length of string' },
|
|
78
|
+
{ name: 'SUBSTRING', signature: 'SUBSTRING(string, start, length)', description: 'Extracts substring' },
|
|
79
|
+
{ name: 'TRIM', signature: 'TRIM(string)', description: 'Removes leading and trailing spaces' },
|
|
80
|
+
{ name: 'ROUND', signature: 'ROUND(number, decimals)', description: 'Rounds a number' },
|
|
81
|
+
{ name: 'ABS', signature: 'ABS(number)', description: 'Returns absolute value' },
|
|
82
|
+
{ name: 'COALESCE', signature: 'COALESCE(value1, value2, ...)', description: 'Returns first non-null value' },
|
|
83
|
+
{ name: 'NULLIF', signature: 'NULLIF(value1, value2)', description: 'Returns null if two values are equal' },
|
|
84
|
+
{ name: 'IFNULL', signature: 'IFNULL(value, default)', description: 'Returns alternative if null' },
|
|
85
|
+
{ name: 'CONCAT', signature: 'CONCAT(string1, string2, ...)', description: 'Concatenates strings' },
|
|
86
|
+
{ name: 'DATE', signature: 'DATE(date)', description: 'Extracts date part' },
|
|
87
|
+
{ name: 'NOW', signature: 'NOW()', description: 'Returns current date and time' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export const getSqlCompletionProvider = () => {
|
|
91
|
+
return {
|
|
92
|
+
provideCompletionItems(
|
|
93
|
+
model: monaco.editor.ITextModel,
|
|
94
|
+
position: monaco.Position,
|
|
95
|
+
_context: monaco.languages.CompletionContext,
|
|
96
|
+
_token: monaco.CancellationToken,
|
|
97
|
+
): monaco.languages.ProviderResult<monaco.languages.CompletionList> {
|
|
98
|
+
const word = model.getWordUntilPosition(position);
|
|
99
|
+
const range = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
|
|
100
|
+
|
|
101
|
+
const suggestions: monaco.languages.CompletionItem[] = [
|
|
102
|
+
...SQL_KEYWORDS.map((keyword) => ({
|
|
103
|
+
label: keyword,
|
|
104
|
+
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
105
|
+
insertText: keyword,
|
|
106
|
+
range: range,
|
|
107
|
+
sortText: '1' + keyword,
|
|
108
|
+
})),
|
|
109
|
+
...SQL_FUNCTIONS.map((func) => ({
|
|
110
|
+
label: func.name,
|
|
111
|
+
kind: monaco.languages.CompletionItemKind.Function,
|
|
112
|
+
insertText: func.name,
|
|
113
|
+
detail: func.signature,
|
|
114
|
+
documentation: func.description,
|
|
115
|
+
range: range,
|
|
116
|
+
sortText: '2' + func.name,
|
|
117
|
+
})),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
suggestions: suggestions,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import MonacoEditor from 'react-monaco-editor';
|
|
3
|
+
import * as monaco from 'monaco-editor';
|
|
4
|
+
import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { css } from '@emotion/css';
|
|
7
|
+
|
|
8
|
+
import { language, languageConfiguration } from './sql';
|
|
9
|
+
import { getSqlCompletionProvider } from './completion/getCompletionProvider';
|
|
10
|
+
// import { validateSql } from './validation';
|
|
11
|
+
|
|
12
|
+
interface SqlEditorProps {
|
|
13
|
+
size?: 'small' | 'middle' | 'large';
|
|
14
|
+
theme?: 'light' | 'dark';
|
|
15
|
+
value?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
enableAutocomplete?: boolean;
|
|
18
|
+
readOnly?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
onEnter?: (value: string) => void;
|
|
22
|
+
onBlur?: (value: string) => void;
|
|
23
|
+
editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SQL_LANG_ID = 'sql';
|
|
27
|
+
const SIZE_MAP: Record<
|
|
28
|
+
string,
|
|
29
|
+
{
|
|
30
|
+
className: string;
|
|
31
|
+
top: number;
|
|
32
|
+
bottom: number;
|
|
33
|
+
}
|
|
34
|
+
> = {
|
|
35
|
+
small: {
|
|
36
|
+
className: 'ant-input-sm',
|
|
37
|
+
top: 1,
|
|
38
|
+
bottom: 1,
|
|
39
|
+
},
|
|
40
|
+
middle: {
|
|
41
|
+
className: 'ant-input-md',
|
|
42
|
+
top: 1,
|
|
43
|
+
bottom: 1,
|
|
44
|
+
},
|
|
45
|
+
large: {
|
|
46
|
+
className: 'ant-input-lg',
|
|
47
|
+
top: 3,
|
|
48
|
+
bottom: 2,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const themeMap: Record<string, string> = {
|
|
53
|
+
light: 'sql-light',
|
|
54
|
+
dark: 'sql-dark',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const containerDisabledClassName = css`
|
|
58
|
+
.monaco-editor {
|
|
59
|
+
user-select: none;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const containerReadOnlyClassName = css`
|
|
65
|
+
.monaco-editor .cursors-layer > .cursor {
|
|
66
|
+
opacity: 0 !important;
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
export default function SqlEditor(props: SqlEditorProps) {
|
|
71
|
+
const id = uuidv4();
|
|
72
|
+
const {
|
|
73
|
+
size = 'middle',
|
|
74
|
+
theme = 'light',
|
|
75
|
+
value = '',
|
|
76
|
+
placeholder,
|
|
77
|
+
enableAutocomplete = true,
|
|
78
|
+
readOnly = false,
|
|
79
|
+
disabled = false,
|
|
80
|
+
onChange,
|
|
81
|
+
onEnter,
|
|
82
|
+
onBlur,
|
|
83
|
+
editorDidMount,
|
|
84
|
+
} = props;
|
|
85
|
+
|
|
86
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
const editorRef = useRef<monacoTypes.editor.IStandaloneCodeEditor | null>(null);
|
|
88
|
+
const modelRef = useRef<monaco.editor.ITextModel | null>(null);
|
|
89
|
+
const disposablesRef = useRef<monaco.IDisposable[]>([]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
// Register language
|
|
93
|
+
if (!monaco.languages.getLanguages().some((lang) => lang.id === SQL_LANG_ID)) {
|
|
94
|
+
monaco.languages.register({ id: SQL_LANG_ID });
|
|
95
|
+
monaco.languages.setMonarchTokensProvider(SQL_LANG_ID, language as any);
|
|
96
|
+
monaco.languages.setLanguageConfiguration(SQL_LANG_ID, languageConfiguration);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Register completion provider
|
|
100
|
+
if (enableAutocomplete) {
|
|
101
|
+
const disposable = monaco.languages.registerCompletionItemProvider(SQL_LANG_ID, getSqlCompletionProvider());
|
|
102
|
+
disposablesRef.current.push(disposable);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
disposablesRef.current.forEach((disposable) => disposable.dispose());
|
|
107
|
+
disposablesRef.current = [];
|
|
108
|
+
};
|
|
109
|
+
}, [enableAutocomplete]);
|
|
110
|
+
|
|
111
|
+
const handleEditorMount = (editor: monacoTypes.editor.IStandaloneCodeEditor) => {
|
|
112
|
+
editorRef.current = editor;
|
|
113
|
+
modelRef.current = editor.getModel();
|
|
114
|
+
|
|
115
|
+
monaco.editor.defineTheme('sql-light', {
|
|
116
|
+
base: 'vs',
|
|
117
|
+
inherit: true,
|
|
118
|
+
rules: [],
|
|
119
|
+
colors: {
|
|
120
|
+
'editor.background': '#00000000',
|
|
121
|
+
focusBorder: '#00000000',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
monaco.editor.defineTheme('sql-dark', {
|
|
126
|
+
base: 'vs-dark',
|
|
127
|
+
inherit: true,
|
|
128
|
+
rules: [],
|
|
129
|
+
colors: {
|
|
130
|
+
'editor.background': '#00000000',
|
|
131
|
+
focusBorder: '#00000000',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const isEditorFocused = editor.createContextKey<boolean>('isEditorFocused' + id, false);
|
|
136
|
+
// we setup on-blur
|
|
137
|
+
editor.onDidBlurEditorWidget(() => {
|
|
138
|
+
isEditorFocused.set(false);
|
|
139
|
+
onBlur?.(editor.getValue());
|
|
140
|
+
// reset the selection to the current position
|
|
141
|
+
const position = editor.getPosition();
|
|
142
|
+
if (position) {
|
|
143
|
+
const newSelection = new monaco.Selection(position.lineNumber, position.column, position.lineNumber, position.column);
|
|
144
|
+
editor.setSelection(newSelection);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
editor.onDidFocusEditorText(() => {
|
|
149
|
+
isEditorFocused.set(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// set the height of the editor container
|
|
153
|
+
const updateElementHeight = () => {
|
|
154
|
+
const containerDiv = containerRef.current;
|
|
155
|
+
if (containerDiv !== null) {
|
|
156
|
+
const pixelHeight = editor.getContentHeight();
|
|
157
|
+
containerDiv.style.height = `${pixelHeight}px`;
|
|
158
|
+
containerDiv.style.width = '100%';
|
|
159
|
+
const pixelWidth = containerDiv.clientWidth;
|
|
160
|
+
editor.layout({ width: pixelWidth, height: pixelHeight });
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
editor.onDidContentSizeChange(updateElementHeight);
|
|
165
|
+
updateElementHeight();
|
|
166
|
+
|
|
167
|
+
// Fixes Monaco capturing the search key binding and displaying a useless search box within the Editor.
|
|
168
|
+
monaco.editor.addKeybindingRule({
|
|
169
|
+
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF,
|
|
170
|
+
command: null,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// 设置 Shift + Enter 为在光标位置换行
|
|
174
|
+
editor.addCommand(
|
|
175
|
+
monaco.KeyMod.Shift | monaco.KeyCode.Enter,
|
|
176
|
+
() => {
|
|
177
|
+
// 在光标位置插入换行符
|
|
178
|
+
const position = editor.getPosition();
|
|
179
|
+
if (position) {
|
|
180
|
+
editor.executeEdits('shift-enter', [
|
|
181
|
+
{
|
|
182
|
+
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
|
183
|
+
text: '\n',
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
// 将光标移动到新行
|
|
187
|
+
editor.setPosition({
|
|
188
|
+
lineNumber: position.lineNumber + 1,
|
|
189
|
+
column: 1,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
'isEditorFocused' + id,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// 完全阻止 Enter 键的默认行为(包括换行)
|
|
197
|
+
monaco.editor.addKeybindingRule({
|
|
198
|
+
keybinding: monaco.KeyCode.Enter,
|
|
199
|
+
command: '-',
|
|
200
|
+
when: '!suggestWidgetVisible',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// handle: enter - 只有在没有建议窗口时才执行自定义行为
|
|
204
|
+
editor.addCommand(
|
|
205
|
+
monaco.KeyCode.Enter,
|
|
206
|
+
() => {
|
|
207
|
+
onEnter?.(editor.getValue());
|
|
208
|
+
},
|
|
209
|
+
'!suggestWidgetVisible && isEditorFocused' + id,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
editorDidMount?.(editor);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const themeValue = themeMap[theme];
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
className={
|
|
220
|
+
'ant-input' +
|
|
221
|
+
(size ? ` ${SIZE_MAP[size].className}` : '') +
|
|
222
|
+
(disabled ? ` ant-input-disabled ${containerDisabledClassName}` : '') +
|
|
223
|
+
(readOnly ? ` ${containerReadOnlyClassName}` : '')
|
|
224
|
+
}
|
|
225
|
+
>
|
|
226
|
+
<div ref={containerRef}>
|
|
227
|
+
<MonacoEditor
|
|
228
|
+
width='100%'
|
|
229
|
+
height='100%'
|
|
230
|
+
language={SQL_LANG_ID}
|
|
231
|
+
theme={themeValue}
|
|
232
|
+
value={value}
|
|
233
|
+
onChange={onChange}
|
|
234
|
+
editorDidMount={handleEditorMount}
|
|
235
|
+
options={{
|
|
236
|
+
minimap: { enabled: false },
|
|
237
|
+
autoClosingBrackets: 'always',
|
|
238
|
+
autoClosingQuotes: 'always',
|
|
239
|
+
autoIndent: 'full',
|
|
240
|
+
formatOnPaste: true,
|
|
241
|
+
formatOnType: true,
|
|
242
|
+
readOnly: readOnly || disabled,
|
|
243
|
+
scrollBeyondLastLine: false,
|
|
244
|
+
smoothScrolling: true,
|
|
245
|
+
tabSize: 2,
|
|
246
|
+
wordWrap: 'on',
|
|
247
|
+
automaticLayout: true,
|
|
248
|
+
glyphMargin: false,
|
|
249
|
+
lineNumbers: 'off',
|
|
250
|
+
lineNumbersMinChars: 0,
|
|
251
|
+
folding: false,
|
|
252
|
+
lineDecorationsWidth: 0,
|
|
253
|
+
overviewRulerBorder: false,
|
|
254
|
+
overviewRulerLanes: 0,
|
|
255
|
+
placeholder: placeholder,
|
|
256
|
+
renderLineHighlight: 'none',
|
|
257
|
+
occurrencesHighlight: 'off',
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|