Skip to content

Commit d6bf8ac

Browse files
committed
Add codelense
1 parent 34415ec commit d6bf8ac

File tree

5 files changed

+158
-21
lines changed

5 files changed

+158
-21
lines changed

package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@grafana/schema": "^10.4.0",
7070
"@grafana/ui": "^11.2.0",
7171
"@mongodb-js/mongodb-constants": "^0.11.1",
72+
"jsonc-parser": "^3.3.1",
7273
"react": "18.2.0",
7374
"react-dom": "18.2.0",
7475
"shadowrealm-api": "^0.8.3",

src/components/QueryEditorRaw.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CodeEditor, type MonacoEditor } from '@grafana/ui';
33
import { useAutocomplete } from '../editor/autocomplete';
44
import { useValidation } from '../editor/validation';
55
import { useHover } from '../editor/hover';
6+
import { useCodeLens } from '../editor/codelens';
67

78
interface QueryEditorRawProps {
89
query: string;
@@ -20,6 +21,7 @@ export function QueryEditorRaw({ query, onBlur, language, width, height, fontSiz
2021
const setupAutocompleteFn = useAutocomplete();
2122
const setupHoverFn = useHover();
2223
const setupValidationFn = useValidation();
24+
const setupCodeLensFn = useCodeLens();
2325

2426
const formatQuery = useCallback(() => {
2527
if (monacoRef.current) {
@@ -35,6 +37,7 @@ export function QueryEditorRaw({ query, onBlur, language, width, height, fontSiz
3537
setupValidationFn(editor, monaco);
3638
setupAutocompleteFn(editor, monaco);
3739
setupHoverFn(editor, monaco);
40+
setupCodeLensFn(editor, monaco);
3841
}}
3942
height={height || '240px'}
4043
width={width ? `${width - 2}px` : undefined}
@@ -43,7 +46,7 @@ export function QueryEditorRaw({ query, onBlur, language, width, height, fontSiz
4346
value={query}
4447
showMiniMap={false}
4548
showLineNumbers={true}
46-
monacoOptions={fontSize ? { fontSize: fontSize } : undefined}
49+
monacoOptions={fontSize ? { fontSize: fontSize, codeLens: true } : undefined}
4750
/>
4851
{children && children({ formatQuery })}
4952
</div>

src/editor/autocomplete.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,18 @@ class CompletionProvider implements monacoTypes.languages.CompletionItemProvider
4040
endColumn: word.endColumn,
4141
};
4242

43-
const stageSuggestions: languages.CompletionItem[] = STAGE_OPERATORS.map((stage) => ({
44-
label: `"${stage.name}"`,
45-
kind: languages.CompletionItemKind.Function,
46-
insertText: `"\\${stage.name}": ${stage.snippet}`,
47-
range: range,
48-
detail: stage.meta,
49-
documentation: stage.description,
50-
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
51-
}));
43+
const stageSuggestions: languages.CompletionItem[] = STAGE_OPERATORS.map((stage) => {
44+
// Add double quotation marks
45+
const snippet = stage.snippet.replace(/(\s*)([a-zA-Z]+)\s*: /g, '$1"$2": ');
46+
return {
47+
label: `"${stage.name}"`,
48+
kind: languages.CompletionItemKind.Function,
49+
insertText: `"\\${stage.name}": ${snippet}`,
50+
range: range,
51+
detail: stage.meta,
52+
documentation: stage.description,
53+
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
54+
}});
5255

5356
const expressionSuggestions: languages.CompletionItem[] = [...EXPRESSION_OPERATORS, ...ACCUMULATORS, ...CONVERSION_OPERATORS, ...QUERY_OPERATORS].map((expression) => ({
5457
label: `"${expression.name}"`,

src/editor/codelens.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { monacoTypes, type MonacoEditor, type Monaco } from '@grafana/ui';
2+
import { JSONPath, ParseErrorCode, type JSONVisitor, visit } from 'jsonc-parser';
3+
import { useEffect, useRef } from 'react';
4+
5+
interface ParsedAggregateStages {
6+
name: string;
7+
startLine: number;
8+
startCharacter: number;
9+
length: number;
10+
}
11+
12+
class CodeLensVisitor implements JSONVisitor {
13+
private _currentLevel: number;
14+
private _stages: ParsedAggregateStages[];
15+
private _hasError: boolean;
16+
17+
constructor() {
18+
this._hasError = false;
19+
this._currentLevel = 0;
20+
this._stages = [];
21+
}
22+
23+
onObjectBegin = (
24+
offset: number,
25+
length: number,
26+
startLine: number,
27+
startCharacter: number,
28+
pathSupplier: () => JSONPath,
29+
) => {
30+
if (this._hasError) {
31+
return false;
32+
}
33+
this._currentLevel += 1;
34+
};
35+
36+
onObjectProperty = (
37+
property: string,
38+
offset: number,
39+
length: number,
40+
startLine: number,
41+
startCharacter: number,
42+
pathSupplier: () => JSONPath,
43+
) => {
44+
if (this._currentLevel === 1) {
45+
this._stages.push({
46+
name: property,
47+
startLine: startLine,
48+
startCharacter: startCharacter,
49+
length: length,
50+
});
51+
}
52+
};
53+
54+
onObjectEnd = (offset: number, length: number, startLine: number, startCharacter: number) => {
55+
this._currentLevel -= 1;
56+
};
57+
58+
onError = (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => {
59+
this._hasError = true;
60+
};
61+
62+
public get stages(): ParsedAggregateStages[] {
63+
return this._stages;
64+
}
65+
66+
public get hasError(): boolean {
67+
return this._hasError;
68+
}
69+
}
70+
71+
class CodeLensProvider implements monacoTypes.languages.CodeLensProvider {
72+
constructor(private readonly editor: MonacoEditor) {}
73+
74+
provideCodeLenses(
75+
model: monacoTypes.editor.ITextModel,
76+
_token: monacoTypes.CancellationToken,
77+
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CodeLensList> {
78+
if (this.editor.getModel()?.id !== model.id) {
79+
return null;
80+
}
81+
82+
const text = model.getValue();
83+
const visitor = new CodeLensVisitor();
84+
visit(text, visitor);
85+
86+
if (visitor.hasError) {
87+
return null;
88+
}
89+
90+
const stages = visitor.stages;
91+
return {
92+
lenses: stages.map((stage) => ({
93+
range: {
94+
startLineNumber: stage.startLine,
95+
startColumn: stage.startCharacter,
96+
endLineNumber: stage.startLine,
97+
endColumn: stage.startCharacter + stage.length,
98+
},
99+
command: {
100+
id: 'mongodb.aggregate.stage',
101+
title: `Stage: ${stage.name}`,
102+
},
103+
})),
104+
dispose: () => {},
105+
};
106+
}
107+
108+
resolveCodeLens(
109+
_model: monacoTypes.editor.ITextModel,
110+
codeLens: monacoTypes.languages.CodeLens,
111+
_token: monacoTypes.CancellationToken,
112+
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CodeLens> {
113+
return codeLens;
114+
}
115+
}
116+
117+
export function useCodeLens() {
118+
const codeLensDisposeFun = useRef<(() => void) | null>(null);
119+
useEffect(() => {
120+
return () => {
121+
codeLensDisposeFun.current?.();
122+
};
123+
}, []);
124+
125+
return (editor: MonacoEditor, monaco: Monaco) => {
126+
const provider = new CodeLensProvider(editor);
127+
const { dispose } = monaco.languages.registerCodeLensProvider('json', provider);
128+
codeLensDisposeFun.current = dispose;
129+
};
130+
}

0 commit comments

Comments
 (0)