|
1 | 1 | import {
|
2 | 2 | useCallback,
|
3 | 3 | useEffect,
|
4 |
| - useState, |
| 4 | + useImperativeHandle, |
| 5 | + useRef, |
5 | 6 | } from "react";
|
6 | 7 |
|
7 | 8 | import {
|
8 | 9 | Editor,
|
9 | 10 | EditorProps,
|
10 | 11 | useMonaco,
|
11 | 12 | } from "@monaco-editor/react";
|
12 |
| -import {language as sqlLanguage} from "monaco-editor/esm/vs/basic-languages/sql/sql.js"; |
| 13 | +import {theme} from "antd"; |
| 14 | +import color from "color"; |
13 | 15 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
14 | 16 |
|
15 | 17 | import "./monaco-loader";
|
16 | 18 |
|
17 | 19 |
|
18 |
| -const MAX_VISIBLE_LINES: number = 5; |
| 20 | +type SqlEditorRef = { |
| 21 | + focus: () => void; |
| 22 | +}; |
| 23 | + |
| 24 | +type SqlEditorProps = Omit<EditorProps, "language"> & React.RefAttributes<SqlEditorRef> & { |
| 25 | + disabled: boolean; |
19 | 26 |
|
20 |
| -type SqlEditorProps = Omit<EditorProps, "language">; |
| 27 | + /** Callback when the editor is mounted and ref is ready to use. */ |
| 28 | + onEditorReady?: () => void; |
| 29 | +}; |
21 | 30 |
|
22 | 31 | /**
|
23 |
| - * Monaco editor with highlighting and autocomplete for SQL syntax. |
| 32 | + * Monaco editor with highlighting for SQL syntax. |
24 | 33 | *
|
25 | 34 | * @param props
|
26 | 35 | * @return
|
27 | 36 | */
|
28 | 37 | const SqlEditor = (props: SqlEditorProps) => {
|
| 38 | + const {ref, disabled, onEditorReady, ...editorProps} = props; |
| 39 | + const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null); |
29 | 40 | const monacoEditor = useMonaco();
|
30 |
| - |
| 41 | + const {token} = theme.useToken(); |
| 42 | + |
| 43 | + useImperativeHandle(ref, () => ({ |
| 44 | + focus: () => { |
| 45 | + editorRef.current?.focus(); |
| 46 | + }, |
| 47 | + }), []); |
| 48 | + |
| 49 | + const handleEditorDidMount = useCallback(( |
| 50 | + editor: monaco.editor.IStandaloneCodeEditor, |
| 51 | + ) => { |
| 52 | + editorRef.current = editor; |
| 53 | + onEditorReady?.(); |
| 54 | + }, [onEditorReady]); |
| 55 | + |
| 56 | + // Define disabled theme for monaco editor |
31 | 57 | useEffect(() => {
|
32 | 58 | if (null === monacoEditor) {
|
33 |
| - return () => { |
34 |
| - }; |
| 59 | + return; |
35 | 60 | }
|
36 |
| - |
37 |
| - // Adds autocomplete suggestions for SQL keywords on editor load |
38 |
| - const provider = monacoEditor.languages.registerCompletionItemProvider("sql", { |
39 |
| - provideCompletionItems: (model, position) => { |
40 |
| - const word = model.getWordUntilPosition(position); |
41 |
| - const range = { |
42 |
| - startLineNumber: position.lineNumber, |
43 |
| - endLineNumber: position.lineNumber, |
44 |
| - startColumn: word.startColumn, |
45 |
| - endColumn: word.endColumn, |
46 |
| - }; |
47 |
| - const suggestions = sqlLanguage.keywords.map((keyword: string) => ({ |
48 |
| - detail: "Presto SQL (CLP)", |
49 |
| - insertText: `${keyword} `, |
50 |
| - kind: monacoEditor.languages.CompletionItemKind.Keyword, |
51 |
| - label: keyword, |
52 |
| - range: range, |
53 |
| - })); |
54 |
| - |
55 |
| - // When SQL keyword suggestions appear (e.g., after "SELECT a"), hitting Enter |
56 |
| - // accepts the first suggestion. To prevent accidental auto-completion |
57 |
| - // in multi-line queries and to allow users to dismiss suggestions more easily, |
58 |
| - // we make the current input the first suggestion. |
59 |
| - // Users can then use arrow keys to select a keyword if needed. |
60 |
| - const typedWord = model.getValueInRange(range); |
61 |
| - if (0 < typedWord.length) { |
62 |
| - suggestions.push({ |
63 |
| - detail: "Current", |
64 |
| - insertText: `${typedWord}\n`, |
65 |
| - kind: monaco.languages.CompletionItemKind.Text, |
66 |
| - label: typedWord, |
67 |
| - range: range, |
68 |
| - }); |
69 |
| - } |
70 |
| - |
71 |
| - return { |
72 |
| - suggestions: suggestions, |
73 |
| - incomplete: true, |
74 |
| - }; |
| 61 | + monacoEditor.editor.defineTheme("disabled-theme", { |
| 62 | + base: "vs", |
| 63 | + inherit: true, |
| 64 | + rules: [], |
| 65 | + colors: { |
| 66 | + "editor.background": color(token.colorBgContainerDisabled).hexa(), |
| 67 | + "editor.foreground": color(token.colorTextDisabled).hexa(), |
| 68 | + |
| 69 | + // transparent |
| 70 | + "focusBorder": "#00000000", |
75 | 71 | },
|
76 |
| - triggerCharacters: [ |
77 |
| - " ", |
78 |
| - "\n", |
79 |
| - ], |
80 |
| - }); |
81 |
| - |
82 |
| - return () => { |
83 |
| - provider.dispose(); |
84 |
| - }; |
85 |
| - }, [monacoEditor]); |
86 |
| - |
87 |
| - const [isContentMultiline, setIsContentMultiline] = useState<boolean>(false); |
88 |
| - |
89 |
| - const handleMonacoMount = useCallback((editor: monaco.editor.IStandaloneCodeEditor) => { |
90 |
| - editor.onDidContentSizeChange((ev) => { |
91 |
| - if (false === ev.contentHeightChanged) { |
92 |
| - return; |
93 |
| - } |
94 |
| - if (null === monacoEditor) { |
95 |
| - throw new Error("Unexpected null Monaco instance"); |
96 |
| - } |
97 |
| - const domNode = editor.getDomNode(); |
98 |
| - if (null === domNode) { |
99 |
| - throw new Error("Unexpected null editor DOM node"); |
100 |
| - } |
101 |
| - const model = editor.getModel(); |
102 |
| - if (null === model) { |
103 |
| - throw new Error("Unexpected null editor model"); |
104 |
| - } |
105 |
| - const lineHeight = editor.getOption(monacoEditor.editor.EditorOption.lineHeight); |
106 |
| - const contentHeight = editor.getContentHeight(); |
107 |
| - const approxWrappedLines = Math.round(contentHeight / lineHeight); |
108 |
| - setIsContentMultiline(1 < approxWrappedLines); |
109 |
| - if (MAX_VISIBLE_LINES >= approxWrappedLines) { |
110 |
| - domNode.style.height = `${contentHeight}px`; |
111 |
| - } else { |
112 |
| - domNode.style.height = `${lineHeight * MAX_VISIBLE_LINES}px`; |
113 |
| - } |
114 | 72 | });
|
115 |
| - }, [monacoEditor]); |
| 73 | + }, [ |
| 74 | + monacoEditor, |
| 75 | + token, |
| 76 | + ]); |
116 | 77 |
|
117 | 78 | return (
|
118 |
| - <Editor |
119 |
| - language={"sql"} |
120 |
| - |
121 |
| - // Use white background while loading (default is grey) so transition to editor with |
122 |
| - // white background is less jarring. |
123 |
| - loading={<div style={{backgroundColor: "white", height: "100%", width: "100%"}}/>} |
124 |
| - options={{ |
125 |
| - automaticLayout: true, |
126 |
| - folding: isContentMultiline, |
127 |
| - fontSize: 20, |
128 |
| - lineHeight: 30, |
129 |
| - lineNumbers: isContentMultiline ? |
130 |
| - "on" : |
131 |
| - "off", |
132 |
| - lineNumbersMinChars: 2, |
133 |
| - minimap: {enabled: false}, |
134 |
| - overviewRulerBorder: false, |
135 |
| - placeholder: "Enter your SQL query", |
136 |
| - renderLineHighlightOnlyWhenFocus: true, |
137 |
| - scrollBeyondLastLine: false, |
138 |
| - wordWrap: "on", |
139 |
| - }} |
140 |
| - onMount={handleMonacoMount} |
141 |
| - {...props}/> |
| 79 | + <div |
| 80 | + style={ |
| 81 | + disabled ? |
| 82 | + {pointerEvents: "none"} : |
| 83 | + {} |
| 84 | + } |
| 85 | + > |
| 86 | + <Editor |
| 87 | + language={"sql"} |
| 88 | + loading={ |
| 89 | + <div |
| 90 | + style={{ |
| 91 | + backgroundColor: "white", |
| 92 | + height: "100%", |
| 93 | + width: "100%", |
| 94 | + }}/> |
| 95 | + } |
| 96 | + options={{ |
| 97 | + automaticLayout: true, |
| 98 | + folding: false, |
| 99 | + fontSize: 16, |
| 100 | + lineNumbers: "off", |
| 101 | + minimap: {enabled: false}, |
| 102 | + overviewRulerBorder: false, |
| 103 | + placeholder: "Enter your SQL query", |
| 104 | + renderLineHighlightOnlyWhenFocus: true, |
| 105 | + scrollBeyondLastLine: false, |
| 106 | + wordWrap: "on", |
| 107 | + }} |
| 108 | + theme={disabled ? |
| 109 | + "disabled-theme" : |
| 110 | + "light"} |
| 111 | + onMount={handleEditorDidMount} |
| 112 | + {...editorProps}/> |
| 113 | + </div> |
142 | 114 | );
|
143 | 115 | };
|
144 | 116 |
|
145 | 117 | export default SqlEditor;
|
| 118 | +export type {SqlEditorRef}; |
0 commit comments