1+ import log from '../../shared/log' ;
12import { useDebouncedCallback } from 'use-debounce' ;
2- // organize-imports-ignore
3+ import * as React from 'react' ;
4+ import { SettingsContext } from '../Settings' ;
5+ import { Tooltip } from './Tooltip' ;
6+ import { INPUT_SYNC_PERIOD } from './Input' ;
7+
38// Must be loaded before other ace-builds imports
49import AceEditor from 'react-ace' ;
10+ // organize-imports-ignore
11+ import { Ace } from 'ace-builds' ;
12+ import ace from 'ace-builds/src-min-noconflict/ace' ;
13+ import langTools from 'ace-builds/src-min-noconflict/ext-language_tools' ;
514// Enables Ctrl-f
615import 'ace-builds/src-min-noconflict/ext-searchbox' ;
716// Enables syntax highlighting
@@ -15,13 +24,25 @@ import 'ace-builds/src-min-noconflict/mode-sql';
1524// UI theme
1625import 'ace-builds/src-min-noconflict/theme-github' ;
1726import 'ace-builds/src-min-noconflict/theme-dracula' ;
18- import * as React from 'react' ;
1927// Shortcuts support, TODO: support non-emacs
2028// This steals Ctrl-a so this should not be a default
2129//import 'ace-builds/src-min-noconflict/keybinding-emacs';
22- import { SettingsContext } from '../Settings' ;
23- import { Tooltip } from './Tooltip' ;
24- import { INPUT_SYNC_PERIOD } from './Input' ;
30+
31+ export function skipWhitespaceBackward ( it : Ace . TokenIterator ) {
32+ while ( ! it . getCurrentToken ( ) . value . trim ( ) ) {
33+ if ( ! it . stepBackward ( ) ) {
34+ return ;
35+ }
36+ }
37+ }
38+
39+ export function skipWhitespaceForward ( it : Ace . TokenIterator ) {
40+ while ( ! it . getCurrentToken ( ) . value . trim ( ) ) {
41+ if ( ! it . stepForward ( ) ) {
42+ return ;
43+ }
44+ }
45+ }
2546
2647export function CodeEditor ( {
2748 value,
@@ -30,6 +51,7 @@ export function CodeEditor({
3051 placeholder,
3152 disabled,
3253 onKeyDown,
54+ autocomplete,
3355 language,
3456 id,
3557 singleLine,
@@ -42,6 +64,10 @@ export function CodeEditor({
4264 disabled ?: boolean ;
4365 onKeyDown ?: ( e : React . KeyboardEvent ) => void ;
4466 placeholder ?: string ;
67+ autocomplete ?: (
68+ tokenIteratorFactory : ( ) => Ace . TokenIterator ,
69+ prefix : string
70+ ) => Array < Ace . Completion > ;
4571 language : string ;
4672 id : string ;
4773 singleLine ?: boolean ;
@@ -52,7 +78,7 @@ export function CodeEditor({
5278 state : { theme } ,
5379 } = React . useContext ( SettingsContext ) ;
5480
55- const [ editorNode , editorRef ] = React . useState < AceEditor > ( null ) ;
81+ const [ editorRef , setEditorRef ] = React . useState < AceEditor > ( null ) ;
5682 const debounced = useDebouncedCallback ( onChange , INPUT_SYNC_PERIOD ) ;
5783 // Flush on unmount
5884 React . useEffect (
@@ -65,35 +91,70 @@ export function CodeEditor({
6591 // Make sure editor resizes if the overall panel changes size. For
6692 // example this happens when the preview height changes.
6793 React . useEffect ( ( ) => {
68- if ( ! editorNode ) {
94+ if ( ! editorRef ) {
6995 return ;
7096 }
7197
72- const panel = editorNode . editor . container . closest ( '.panel' ) ;
98+ const panel = editorRef . editor . container . closest ( '.panel' ) ;
7399 const obs = new ResizeObserver ( function handleEditorResize ( ) {
74- editorNode . editor ?. resize ( ) ;
100+ editorRef . editor ?. resize ( ) ;
75101 } ) ;
76102 obs . observe ( panel ) ;
77103
78104 return ( ) => obs . disconnect ( ) ;
79- } , [ editorNode ] ) ;
105+ } , [ editorRef ] ) ;
80106
81107 // Resync value when outer changes
82108 React . useEffect ( ( ) => {
83- if ( ! editorNode || value == editorNode . editor . getValue ( ) ) {
109+ if ( ! editorRef || value == editorRef . editor . getValue ( ) ) {
84110 return ;
85111 }
86112
87113 // Without this the clearSelection call below moves the cursor to the end of the textarea destroying in-action edits
88- if ( editorNode . editor . container . contains ( document . activeElement ) ) {
114+ if ( editorRef . editor . container . contains ( document . activeElement ) ) {
89115 return ;
90116 }
91117
92- editorNode . editor . setValue ( value ) ;
118+ editorRef . editor . setValue ( value ) ;
93119 // setValue() also highlights the inserted values so this gets rid
94120 // of the highlight. Kind of a weird API really
95- editorNode . editor . clearSelection ( ) ;
96- } , [ value , editorNode ] ) ;
121+ editorRef . editor . clearSelection ( ) ;
122+ } , [ value , editorRef ] ) ;
123+
124+ React . useEffect ( ( ) => {
125+ if ( ! autocomplete ) {
126+ return ;
127+ }
128+
129+ const { TokenIterator } = ace . require ( 'ace/token_iterator' ) ;
130+
131+ const completer = {
132+ getCompletions : (
133+ editor : AceEditor ,
134+ session : Ace . EditSession ,
135+ pos : Ace . Point ,
136+ prefix : string ,
137+ callback : Ace . CompleterCallback
138+ ) => {
139+ // This gets registered globally which is kind of weird. //
140+ // So it needs to check again that the currently editing editor
141+ // is the one attached to this callback.
142+ if ( ! autocomplete || ( editorRef . editor as unknown ) !== editor ) {
143+ return callback ( null , [ ] ) ;
144+ }
145+
146+ try {
147+ const factory = ( ) => new TokenIterator ( session , pos . row , pos . column ) ;
148+ return callback ( null , autocomplete ( factory , prefix ) ) ;
149+ } catch ( e ) {
150+ log . error ( e ) ;
151+ return callback ( null , [ ] ) ;
152+ }
153+ } ,
154+ } ;
155+
156+ langTools . setCompleters ( [ completer ] ) ;
157+ } , [ autocomplete , editorRef ] ) ;
97158
98159 return (
99160 < div
@@ -103,15 +164,14 @@ export function CodeEditor({
103164 >
104165 { label && < label className = "label input-label" > { label } </ label > }
105166 < AceEditor
106- ref = { editorRef }
167+ ref = { setEditorRef }
107168 mode = { language }
108169 theme = { theme === 'dark' ? 'dracula' : 'github' }
109170 maxLines = { singleLine ? 1 : undefined }
110171 wrapEnabled = { true }
111- onBlur = {
112- ( ) =>
113- debounced . flush ( ) /* Simplifying this to onBlur={debounced.flush} doesn't work. */
114- }
172+ onBlur = { ( ) => {
173+ debounced . flush ( ) ; /* Simplifying this to onBlur={debounced.flush} doesn't work. */
174+ } }
115175 name = { id }
116176 defaultValue = { String ( value ) }
117177 onChange = { ( v ) => debounced ( v ) }
@@ -155,7 +215,11 @@ export function CodeEditor({
155215 setOptions = {
156216 singleLine
157217 ? { showLineNumbers : false , highlightActiveLine : false }
158- : undefined
218+ : {
219+ enableBasicAutocompletion : Boolean ( autocomplete ) ,
220+ enableLiveAutocompletion : Boolean ( autocomplete ) ,
221+ enableSnippets : Boolean ( autocomplete ) ,
222+ }
159223 }
160224 />
161225 { tooltip && < Tooltip > { tooltip } </ Tooltip > }
0 commit comments