Skip to content

Commit 4b1641f

Browse files
authored
Add basic autocomplete support (#213)
* Start basic autocomplete * Basic autocomplete working * Fix for height * Fix for fmt * Fix for nested fields * Formats * Bump coverage * Duplicate ace type definition * Add copyright
1 parent f9f4c0f commit 4b1641f

File tree

15 files changed

+274
-71
lines changed

15 files changed

+274
-71
lines changed

.github/workflows/docker.yml

Lines changed: 0 additions & 29 deletions
This file was deleted.

ee/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"typeRoots": [
1313
"../node_modules/@types",
1414
"../type-overrides",
15-
"./node_modules/@types"
15+
"./node_modules/@types",
16+
"./type-overrides"
1617
],
1718
"resolveJsonModule": true
1819
},

ee/type-overrides/ace.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright 2022 Multiprocess Labs LLC
2+
3+
declare module 'ace-builds/src-min-noconflict/ace';
4+
declare module 'ace-builds/src-min-noconflict/ext-language_tools';

jest.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ module.exports = {
44
process.platform === 'linux'
55
? {
66
global: {
7-
statements: 55,
7+
statements: 54,
88
branches: 41,
9-
functions: 36,
10-
lines: 55,
9+
functions: 35,
10+
lines: 54,
1111
},
1212
}
1313
: undefined,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"dependencies": {
2929
"@tabler/icons": "^1.56.0",
3030
"ace-builds": "^1.4.13",
31-
"better-sqlite3": "multiprocessio/better-sqlite3#11866f1",
31+
"better-sqlite3": "^7.5.1",
3232
"chart.js": "^3.5.1",
3333
"cookie-parser": "^1.4.5",
3434
"core-js": "^3.21.1",

type-overrides/ace.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare module 'ace-builds/src-min-noconflict/ace';
2+
declare module 'ace-builds/src-min-noconflict/ext-language_tools';

ui/components/CodeEditor.tsx

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import log from '../../shared/log';
12
import { 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
49
import 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
615
import '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
1625
import 'ace-builds/src-min-noconflict/theme-github';
1726
import '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

2647
export 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>}

ui/components/ContentTypePicker.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ export function ContentTypePicker({
6767
</option>
6868
<option value="text/apache2error">Apache2 Error Logs</option>
6969
<option value="text/nginxaccess">Nginx Access Logs</option>
70-
<option value="application/jsonlines">
71-
Newline-delimited JSON
72-
</option>
7370
<option value="text/regexplines">Newline-delimited Regex</option>
7471
</optgroup>
7572
</Select>

ui/panels/DatabasePanel.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Ace } from 'ace-builds';
12
import * as React from 'react';
23
import { DOCS_ROOT } from '../../shared/constants';
34
import { NoConnectorError } from '../../shared/errors';
@@ -6,6 +7,7 @@ import {
67
ConnectorInfo,
78
DatabaseConnectorInfo,
89
DatabasePanelInfo,
10+
PanelInfo,
911
TimeSeriesRange as TimeSeriesRangeT,
1012
} from '../../shared/state';
1113
import { panelRPC } from '../asyncRPC';
@@ -16,6 +18,12 @@ import { ServerPicker } from '../components/ServerPicker';
1618
import { TimeSeriesRange } from '../components/TimeSeriesRange';
1719
import { VENDORS } from '../connectors';
1820
import { ProjectContext } from '../state';
21+
import {
22+
builtinCompletions,
23+
dotAccessPanelShapeCompletions,
24+
panelNameCompletions,
25+
stringPanelShapeCompletions,
26+
} from './ProgramPanel';
1927
import { PanelBodyProps, PanelDetailsProps, PanelUIDetails } from './types';
2028

2129
export async function evalDatabasePanel(
@@ -217,11 +225,13 @@ export function DatabasePanelDetails({
217225
export function DatabasePanelBody({
218226
updatePanel,
219227
panel,
228+
panels,
220229
keyboardShortcuts,
221230
}: PanelBodyProps<DatabasePanelInfo>) {
222231
return (
223232
<CodeEditor
224-
id={panel.id}
233+
autocomplete={makeAutocomplete(panels.filter((p) => p.id !== panel.id))}
234+
id={'editor-' + panel.id}
225235
onKeyDown={keyboardShortcuts}
226236
value={panel.content}
227237
onChange={(value: string) => {
@@ -234,6 +244,21 @@ export function DatabasePanelBody({
234244
);
235245
}
236246

247+
export function makeAutocomplete(panels: Array<PanelInfo>) {
248+
return (tokenIteratorFactory: () => Ace.TokenIterator, prefix: string) => {
249+
return [
250+
...builtinCompletions(tokenIteratorFactory).filter(
251+
(c) => !c.value.startsWith('DM_setPanel')
252+
),
253+
...panelNameCompletions(tokenIteratorFactory, panels),
254+
...dotAccessPanelShapeCompletions(tokenIteratorFactory, panels),
255+
...stringPanelShapeCompletions(tokenIteratorFactory, panels),
256+
]
257+
.flat()
258+
.filter((c) => c && c.value.startsWith(prefix));
259+
};
260+
}
261+
237262
export function DatabaseInfo({ panel }: { panel: DatabasePanelInfo }) {
238263
const { connectors } = React.useContext(ProjectContext).state;
239264
const connector = connectors.find(

ui/panels/FilterAggregatePanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function FilterAggregatePanelDetails({
137137
<div className="form-row">
138138
<CodeEditor
139139
singleLine
140-
id={panel.id + 'filter'}
140+
id={'filter-' + panel.id}
141141
label="Filter"
142142
placeholder="x LIKE '%town%' AND y IN (1, 2)"
143143
value={panel.filagg.filter}

0 commit comments

Comments
 (0)