Skip to content

Commit 39933d8

Browse files
committed
Add autocomplete for some stage operators
1 parent 1216506 commit 39933d8

File tree

3 files changed

+145
-8
lines changed

3 files changed

+145
-8
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ ci/
4141
.env
4242
venv/
4343
mongodb-datasource/
44-
grafana-storage/
44+
grafana-storage/
45+
46+
mongo-docs/

src/autocomplete.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useRef, useEffect } from 'react';
2+
import { type Monaco, type monacoTypes } from '@grafana/ui';
3+
import { languages } from 'monaco-editor';
4+
5+
// Supports JSON only right now
6+
class CompletionProvider implements monacoTypes.languages.CompletionItemProvider {
7+
constructor(
8+
private readonly monaco: Monaco,
9+
private readonly editor: monacoTypes.editor.IStandaloneCodeEditor,
10+
) {}
11+
12+
provideCompletionItems(
13+
model: monacoTypes.editor.ITextModel,
14+
position: monacoTypes.Position,
15+
context: monacoTypes.languages.CompletionContext,
16+
token: monacoTypes.CancellationToken,
17+
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CompletionList> {
18+
if (this.editor.getModel()?.id !== model.id) {
19+
return { suggestions: [] };
20+
}
21+
22+
const textUntilPosition = model.getValueInRange({
23+
startLineNumber: 1,
24+
startColumn: 1,
25+
endLineNumber: position.lineNumber,
26+
endColumn: position.column,
27+
});
28+
29+
// Check if the current position is inside a bracket
30+
const match = textUntilPosition.match(/\s*\{\s*("[^"]*"\s*:\s*"[^"]*"\s*,\s*)*([^"]*)?$/);
31+
if (!match) {
32+
return { suggestions: [] };
33+
}
34+
35+
const word = model.getWordUntilPosition(position);
36+
if (!word) {
37+
return { suggestions: [] };
38+
}
39+
40+
const range = {
41+
startLineNumber: position.lineNumber,
42+
endLineNumber: position.lineNumber,
43+
startColumn: word.startColumn,
44+
endColumn: word.endColumn,
45+
};
46+
47+
return {
48+
suggestions: [
49+
{
50+
label: '"$match"',
51+
kind: languages.CompletionItemKind.Function,
52+
insertText: '"\\$match": {\n\t${1:query}$0\n}',
53+
range: range,
54+
detail: 'stage',
55+
documentation: 'Filters documents based on a specified query predicate.',
56+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
57+
},
58+
{
59+
label: '"$project"',
60+
kind: languages.CompletionItemKind.Function,
61+
insertText: '"\\$project": {\n\t${1:specification(s)}$0\n}',
62+
range: range,
63+
detail: 'stage',
64+
documentation: 'Passes along the documents with the requested fields to the next stage in the pipeline.',
65+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
66+
},
67+
{
68+
label: '"$limit"',
69+
kind: languages.CompletionItemKind.Function,
70+
insertText: '"\\$match": ${1:number}',
71+
range: range,
72+
detail: 'stage',
73+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
74+
},
75+
{
76+
label: '"$lookup"',
77+
kind: languages.CompletionItemKind.Function,
78+
insertText:
79+
'"\\$lookup": {\n\t"from": ${1:collection}$0,\n\t"localField": ${2:field},\n\t"foreignField": ${3:field},\n\t"as": ${4:result}\n}',
80+
range: range,
81+
detail: 'stage',
82+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
83+
},
84+
{
85+
label: '"$sort"',
86+
kind: languages.CompletionItemKind.Function,
87+
insertText: '"\\$sort": {\n\t${1:field1}$0: ${2:sortOrder}\n}',
88+
range: range,
89+
detail: 'stage',
90+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
91+
},
92+
{
93+
label: '"$facet"',
94+
kind: languages.CompletionItemKind.Function,
95+
insertText: '"\\$facet": {\n\t${1:outputFieldN}$0: [ ${2:stageN}, ${3:...} ]\n}',
96+
range: range,
97+
detail: 'stage',
98+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
99+
},
100+
{
101+
label: '"$addFields"',
102+
kind: languages.CompletionItemKind.Function,
103+
insertText: '"\\$addFields": {\n\t${1:newField}: ${2:expression}$0, ${3:...}\n}',
104+
range: range,
105+
detail: 'stage',
106+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
107+
},
108+
{
109+
label: '"$count"',
110+
kind: languages.CompletionItemKind.Function,
111+
insertText: '"\\$count": "${1:string}"',
112+
range: range,
113+
detail: 'stage',
114+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
115+
},
116+
],
117+
};
118+
}
119+
}
120+
121+
export function useAutocomplete() {
122+
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
123+
useEffect(() => {
124+
return () => {
125+
autocompleteDisposeFun.current?.();
126+
};
127+
}, []);
128+
129+
return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
130+
const provider = new CompletionProvider(monaco, editor);
131+
const { dispose } = monaco.languages.registerCompletionItemProvider('json', provider);
132+
autocompleteDisposeFun.current = dispose;
133+
};
134+
}

src/components/QueryEditor.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { CoreApp, FeatureState, QueryEditorProps, SelectableValue } from '@grafa
1818
import { DataSource } from '../datasource';
1919
import { MongoDataSourceOptions, MongoQuery, QueryLanguage, QueryType, DEFAULT_QUERY } from '../types';
2020
import { parseJsQuery, parseJsQueryLegacy, validateJsonQueryText, validatePositiveNumber } from '../utils';
21-
import { editor } from 'monaco-editor';
21+
import type { monacoTypes } from '@grafana/ui';
22+
import { useAutocomplete } from '../autocomplete';
2223
import './QueryEditor.css';
2324

2425
type Props = QueryEditorProps<DataSource, MongoQuery, MongoDataSourceOptions>;
@@ -43,9 +44,10 @@ const languageOptions: Array<SelectableValue<string>> = [
4344
];
4445

4546
export function QueryEditor({ query, onChange, app }: Props) {
46-
const codeEditorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
47+
const codeEditorRef = useRef<monacoTypes.editor.IStandaloneCodeEditor | null>(null);
4748
const [queryTextError, setQueryTextError] = useState<string | null>(null);
4849
const [isOpen, setIsOpen] = useState(false);
50+
const setupAutocompleteFn = useAutocomplete();
4951

5052
const [maxTimeMSText, setMaxTimeMSText] = useState<string>(
5153
query.aggregateMaxTimeMS ? query.aggregateMaxTimeMS.toString() : '',
@@ -129,10 +131,6 @@ export function QueryEditor({ query, onChange, app }: Props) {
129131
onChange({ ...query, aggregateComment: event.target.value });
130132
};
131133

132-
const onCodeEditorDidMount = (e: editor.IStandaloneCodeEditor) => {
133-
codeEditorRef.current = e;
134-
};
135-
136134
const onFormatQueryText = () => {
137135
if (codeEditorRef.current) {
138136
codeEditorRef.current.getAction('editor.action.formatDocument').run();
@@ -277,7 +275,10 @@ export function QueryEditor({ query, onChange, app }: Props) {
277275
invalid={queryTextError != null}
278276
>
279277
<CodeEditor
280-
onEditorDidMount={onCodeEditorDidMount}
278+
onEditorDidMount={(editor, monaco) => {
279+
codeEditorRef.current = editor;
280+
setupAutocompleteFn(editor, monaco);
281+
}}
281282
width="100%"
282283
height={300}
283284
language={

0 commit comments

Comments
 (0)