@fc-components/monaco-editor 0.1.16 → 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.
@@ -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
+ }
package/src/sql/sql.ts ADDED
@@ -0,0 +1,250 @@
1
+ export const languageConfiguration = {
2
+ wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
3
+ comments: {
4
+ lineComment: '--',
5
+ blockComment: ['/*', '*/'] as [string, string],
6
+ },
7
+ brackets: [
8
+ ['{', '}'],
9
+ ['[', ']'],
10
+ ['(', ')'],
11
+ ] as any,
12
+ autoClosingPairs: [
13
+ { open: '{', close: '}' },
14
+ { open: '[', close: ']' },
15
+ { open: '(', close: ')' },
16
+ { open: '"', close: '"' },
17
+ { open: "'", close: "'" },
18
+ { open: '`', close: '`' },
19
+ ] as any,
20
+ surroundingPairs: [
21
+ { open: '{', close: '}' },
22
+ { open: '[', close: ']' },
23
+ { open: '(', close: ')' },
24
+ { open: '"', close: '"' },
25
+ { open: "'", close: "'" },
26
+ { open: '`', close: '`' },
27
+ ] as any,
28
+ folding: {
29
+ offSide: false,
30
+ },
31
+ };
32
+
33
+ // SQL keywords
34
+ const keywords = [
35
+ 'SELECT',
36
+ 'FROM',
37
+ 'WHERE',
38
+ 'AND',
39
+ 'OR',
40
+ 'NOT',
41
+ 'JOIN',
42
+ 'INNER',
43
+ 'LEFT',
44
+ 'RIGHT',
45
+ 'FULL',
46
+ 'OUTER',
47
+ 'ON',
48
+ 'ORDER',
49
+ 'BY',
50
+ 'GROUP',
51
+ 'HAVING',
52
+ 'LIMIT',
53
+ 'OFFSET',
54
+ 'INSERT',
55
+ 'INTO',
56
+ 'VALUES',
57
+ 'UPDATE',
58
+ 'SET',
59
+ 'DELETE',
60
+ 'CREATE',
61
+ 'TABLE',
62
+ 'ALTER',
63
+ 'DROP',
64
+ 'PRIMARY',
65
+ 'KEY',
66
+ 'FOREIGN',
67
+ 'CONSTRAINT',
68
+ 'UNIQUE',
69
+ 'INDEX',
70
+ 'VIEW',
71
+ 'DATABASE',
72
+ 'SCHEMA',
73
+ 'AS',
74
+ 'DISTINCT',
75
+ 'CASE',
76
+ 'WHEN',
77
+ 'THEN',
78
+ 'ELSE',
79
+ 'END',
80
+ 'CAST',
81
+ 'BETWEEN',
82
+ 'IN',
83
+ 'LIKE',
84
+ 'IS',
85
+ 'NULL',
86
+ 'TRUE',
87
+ 'FALSE',
88
+ 'WITH',
89
+ 'UNION',
90
+ 'EXCEPT',
91
+ 'INTERSECT',
92
+ ];
93
+
94
+ export const language = {
95
+ defaultToken: '',
96
+ tokenPostfix: '.sql',
97
+ ignoreCase: true,
98
+
99
+ brackets: [
100
+ { open: '(', close: ')', token: 'delimiter.parenthesis' },
101
+ { open: '{', close: '}', token: 'delimiter.curly' },
102
+ { open: '[', close: ']', token: 'delimiter.square' },
103
+ ],
104
+
105
+ keywords,
106
+
107
+ operators: ['=', '>', '<', '!', '%', '&', '|', '^', '~', '?', ':', '+', '-', '*', '/'],
108
+
109
+ builtinFunctions: [
110
+ 'COUNT',
111
+ 'SUM',
112
+ 'AVG',
113
+ 'MIN',
114
+ 'MAX',
115
+ 'UPPER',
116
+ 'LOWER',
117
+ 'LENGTH',
118
+ 'SUBSTRING',
119
+ 'TRIM',
120
+ 'ROUND',
121
+ 'ABS',
122
+ 'COALESCE',
123
+ 'NULLIF',
124
+ 'IFNULL',
125
+ 'CONCAT',
126
+ 'DATE',
127
+ 'NOW',
128
+ 'YEAR',
129
+ 'MONTH',
130
+ 'DAY',
131
+ 'HOUR',
132
+ 'MINUTE',
133
+ 'SECOND',
134
+ ],
135
+
136
+ escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
137
+
138
+ digits: /\d+(_+\d+)*/,
139
+
140
+ octaldigits: /[0-7]+(_+[0-7]+)*/,
141
+
142
+ hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
143
+
144
+ regexpctl: /[(){}\[\]\|;,.?*+^$\\]/,
145
+
146
+ regexpattern: /(\{[0-9]+\})|(\{[0-9]*,[0-9]*\})|(\?(?:\?)?|[*+]|\^|\$|\|\\)/,
147
+
148
+ tokenizer: {
149
+ root: [
150
+ { include: '@comments' },
151
+ { include: '@whitespace' },
152
+ { include: '@pseudo-columns' },
153
+ [/[;,.]/, 'delimiter'],
154
+ [/[{}()\[\]]/, '@brackets'],
155
+ { include: '@builtinVariables' },
156
+ { include: '@numbers' },
157
+ { include: '@strings' },
158
+ [/[a-zA-Z_#][a-zA-Z0-9_$#@]*(?=\s*\()/, { cases: { '@builtinFunctions': 'keyword.function', '@default': 'identifier.function' } }],
159
+ [
160
+ /[a-zA-Z_#][a-zA-Z0-9_$#@]*/,
161
+ {
162
+ cases: {
163
+ '@keywords': 'keyword',
164
+ '@default': 'identifier',
165
+ },
166
+ },
167
+ ],
168
+ [/[<>=!%&+\-*/|~^]/, 'operator'],
169
+ ],
170
+
171
+ whitespace: [[/\s+/, 'white']],
172
+
173
+ comments: [
174
+ [/--+.*/, 'comment'],
175
+ [/\/\*/, { token: 'comment.quote', next: '@comment' }],
176
+ ],
177
+
178
+ comment: [
179
+ [/[^*/]+/, 'comment'],
180
+ [/\*\//, { token: 'comment.quote', next: '@pop' }],
181
+ [/./, 'comment'],
182
+ ],
183
+
184
+ 'pseudo-columns': [
185
+ [
186
+ /[$][A-Za-z_][A-Za-z0-9_]*/,
187
+ {
188
+ cases: {
189
+ '@keywords': 'keyword',
190
+ '@default': 'variable',
191
+ },
192
+ },
193
+ ],
194
+ [
195
+ /@[A-Za-z_][A-Za-z0-9_]*/,
196
+ {
197
+ cases: {
198
+ '@keywords': 'keyword',
199
+ '@default': 'variable',
200
+ },
201
+ },
202
+ ],
203
+ ],
204
+
205
+ builtinVariables: [
206
+ [
207
+ /@@?[a-zA-Z_][a-zA-Z0-9_]*/,
208
+ {
209
+ cases: {
210
+ '@keywords': 'keyword',
211
+ '@default': 'variable',
212
+ },
213
+ },
214
+ ],
215
+ ],
216
+
217
+ numbers: [
218
+ [/0[xX][0-9a-fA-F]*[0-9a-fA-F]/, 'number.hex'],
219
+ [/0[0-7]+(?!\d)/, 'number.octal'],
220
+ [/(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/, 'number'],
221
+ ],
222
+
223
+ strings: [
224
+ [/'/, { token: 'string', next: '@string' }],
225
+ [/"/, { token: 'string.double', next: '@string_double' }],
226
+ [/`/, { token: 'string.backtick', next: '@string_backtick' }],
227
+ ],
228
+
229
+ string: [
230
+ [/[^'\\]+/, 'string'],
231
+ [/@escapes/, 'string.escape'],
232
+ [/\\./, 'string.escape.invalid'],
233
+ [/'/, { token: 'string', next: '@pop' }],
234
+ ],
235
+
236
+ string_double: [
237
+ [/[^"\\]+/, 'string.double'],
238
+ [/@escapes/, 'string.escape'],
239
+ [/\\./, 'string.escape.invalid'],
240
+ [/"/, { token: 'string.double', next: '@pop' }],
241
+ ],
242
+
243
+ string_backtick: [
244
+ [/[^`\\]+/, 'string.backtick'],
245
+ [/@escapes/, 'string.escape'],
246
+ [/\\./, 'string.escape.invalid'],
247
+ [/`/, { token: 'string.backtick', next: '@pop' }],
248
+ ],
249
+ },
250
+ };
@@ -0,0 +1,8 @@
1
+ export interface SqlEditorMarker {
2
+ startLineNumber: number;
3
+ startColumn: number;
4
+ endLineNumber: number;
5
+ endColumn: number;
6
+ message: string;
7
+ severity: 'Error' | 'Warning' | 'Information';
8
+ }
@@ -0,0 +1,92 @@
1
+ import * as monaco from 'monaco-editor';
2
+
3
+ const SQL_LANG_ID = 'sql';
4
+
5
+ export const validateSql = (sql: string): Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] => {
6
+ const markers: Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] = [];
7
+
8
+ if (!sql || sql.trim().length === 0) {
9
+ return markers;
10
+ }
11
+
12
+ // Basic SQL validation - check for common syntax issues
13
+ const lines = sql.split('\n');
14
+
15
+ lines.forEach((line, index) => {
16
+ const lineNumber = index + 1;
17
+
18
+ // Check for incomplete quotes
19
+ const singleQuotes = (line.match(/'/g) || []).length;
20
+ const doubleQuotes = (line.match(/"/g) || []).length;
21
+ const backticks = (line.match(/`/g) || []).length;
22
+
23
+ if (singleQuotes % 2 !== 0) {
24
+ markers.push({
25
+ severity: monaco.MarkerSeverity.Warning,
26
+ startLineNumber: lineNumber,
27
+ startColumn: 1,
28
+ endLineNumber: lineNumber,
29
+ endColumn: line.length + 1,
30
+ message: "Unclosed single quote '",
31
+ code: 'SQL001',
32
+ source: SQL_LANG_ID,
33
+ });
34
+ }
35
+
36
+ if (doubleQuotes % 2 !== 0) {
37
+ markers.push({
38
+ severity: monaco.MarkerSeverity.Warning,
39
+ startLineNumber: lineNumber,
40
+ startColumn: 1,
41
+ endLineNumber: lineNumber,
42
+ endColumn: line.length + 1,
43
+ message: 'Unclosed double quote "',
44
+ code: 'SQL002',
45
+ source: SQL_LANG_ID,
46
+ });
47
+ }
48
+
49
+ if (backticks % 2 !== 0) {
50
+ markers.push({
51
+ severity: monaco.MarkerSeverity.Warning,
52
+ startLineNumber: lineNumber,
53
+ startColumn: 1,
54
+ endLineNumber: lineNumber,
55
+ endColumn: line.length + 1,
56
+ message: 'Unclosed backtick `',
57
+ code: 'SQL003',
58
+ source: SQL_LANG_ID,
59
+ });
60
+ }
61
+
62
+ // Check for unmatched parentheses (basic check)
63
+ const openParens = (line.match(/\(/g) || []).length;
64
+ const closeParens = (line.match(/\)/g) || []).length;
65
+
66
+ if (openParens > closeParens) {
67
+ markers.push({
68
+ severity: monaco.MarkerSeverity.Warning,
69
+ startLineNumber: lineNumber,
70
+ startColumn: 1,
71
+ endLineNumber: lineNumber,
72
+ endColumn: line.length + 1,
73
+ message: 'Unmatched opening parenthesis',
74
+ code: 'SQL004',
75
+ source: SQL_LANG_ID,
76
+ });
77
+ } else if (closeParens > openParens) {
78
+ markers.push({
79
+ severity: monaco.MarkerSeverity.Warning,
80
+ startLineNumber: lineNumber,
81
+ startColumn: 1,
82
+ endLineNumber: lineNumber,
83
+ endColumn: line.length + 1,
84
+ message: 'Unmatched closing parenthesis',
85
+ code: 'SQL005',
86
+ source: SQL_LANG_ID,
87
+ });
88
+ }
89
+ });
90
+
91
+ return markers;
92
+ };