Skip to content

Commit dca77c3

Browse files
authored
feat(compass-editor): add links to the documentation to the agg and stage autocompleter suggestions COMPASS-6688 (#4218)
feat(compass-editor): add links to the documentation to the agg and stage autocompleter suggestions
1 parent d1c7455 commit dca77c3

File tree

5 files changed

+94
-44
lines changed

5 files changed

+94
-44
lines changed

packages/compass-editor/src/codemirror/ace-compat-autocompleter.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,27 @@ export function mapMongoDBCompletionToCodemirrorCompletion(
2121
? completion.value
2222
: wrapField(completion.value, escape === 'always'),
2323
detail: completion.meta?.startsWith('field') ? 'field' : completion.meta,
24-
info: completion.description,
24+
info() {
25+
if (!completion.description) {
26+
return null;
27+
}
28+
29+
const infoNode = document.createElement('div');
30+
infoNode.classList.add('completion-info');
31+
infoNode.addEventListener('mousedown', (evt) => {
32+
// If we are clicking a link inside the info block, we have to prevent
33+
// default browser behavior that will remove the focus from the editor
34+
// and cause the autocompleter to dissapear before browser handles the
35+
// actual click. This is very similar to how codemirror handles clicks
36+
// on the list items
37+
// @see {@link https://github.com/codemirror/autocomplete/blob/82480a7d51d60ad933808e42f6189d841a5a6bc8/src/tooltip.ts#L96-L97}
38+
if ((evt.target as HTMLElement).nodeName === 'A') {
39+
evt.preventDefault();
40+
}
41+
});
42+
infoNode.innerHTML = completion.description;
43+
return infoNode;
44+
},
2545
};
2646

2747
if (completion.snippet) {

packages/compass-editor/src/codemirror/aggregation-autocompleter.ts

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,15 @@ import { STAGE_OPERATOR_NAMES } from '@mongodb-js/mongodb-constants';
33
import type { CompleteOptions } from '../autocompleter';
44
import { completer } from '../autocompleter';
55
import type { Token } from './utils';
6+
import { parents } from './utils';
7+
import { getPropertyNameFromPropertyToken, isTokenEqual } from './utils';
8+
import { aggLink, padLines } from './utils';
69
import { createCompletionResultForIdPrefix } from './ace-compat-autocompleter';
710
import { createAceCompatAutocompleter } from './ace-compat-autocompleter';
811
import { createStageAutocompleter } from './stage-autocompleter';
912

1013
const StageOperatorNames = new Set(STAGE_OPERATOR_NAMES as string[]);
1114

12-
function* parents(token: Token) {
13-
let parent: Token | null = token;
14-
while ((parent = parent.parent)) {
15-
yield parent;
16-
}
17-
}
18-
19-
function removeQuotes(str: string) {
20-
return str.replace(/(^('|")|('|")$)/g, '');
21-
}
22-
23-
// lezer tokens are immutable, we check position in syntax tree to make sure we
24-
// are looking at the same token
25-
function isTokenEqual(a: Token, b: Token) {
26-
return a.from === b.from && a.to === b.to;
27-
}
28-
29-
function getPropertyNameFromPropertyToken(
30-
editorState: EditorState,
31-
propertyToken: Token
32-
): string {
33-
if (!propertyToken.firstChild) {
34-
return '';
35-
}
36-
return removeQuotes(getTokenText(editorState, propertyToken.firstChild));
37-
}
38-
39-
function padLines(str: string, pad = ' ') {
40-
return str
41-
.split('\n')
42-
.map((line) => `${pad}${line}`)
43-
.join('\n');
44-
}
45-
46-
function getTokenText(editorState: EditorState, token: Token) {
47-
return editorState.sliceDoc(token.from, token.to);
48-
}
49-
5015
function getStageNameForToken(
5116
editorState: EditorState,
5217
token: Token
@@ -93,6 +58,11 @@ export function createAggregationAutocompleter(
9358
const opName = completion.value;
9459
return {
9560
...completion,
61+
...(completion.description && {
62+
description:
63+
`<p>${aggLink(opName)} pipeline stage</p>` +
64+
`<p>${completion.description}</p>`,
65+
}),
9666
...(completion.snippet && {
9767
snippet: !isInsideBlock
9868
? `{\n${padLines(`${opName}: ${completion.snippet}`)}\n}`

packages/compass-editor/src/codemirror/stage-autocompleter.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import {
55
createAceCompatAutocompleter,
66
createCompletionResultForIdPrefix,
77
} from './ace-compat-autocompleter';
8-
import { completeWordsInString } from './utils';
8+
import { aggLink, completeWordsInString } from './utils';
99
import { createQueryAutocompleter } from './query-autocompleter';
1010

11-
/**
12-
* Autocompleter for the document object, only autocompletes field names in the
13-
* appropriate format (either escaped or not) both for javascript and json modes
14-
*/
1511
export const createStageAutocompleter = ({
1612
stageOperator,
1713
...options
@@ -36,6 +32,15 @@ export const createStageAutocompleter = ({
3632
? (['accumulator', 'accumulator:*'] as const)
3733
: []),
3834
],
35+
}).map((completion) => {
36+
if (completion.meta?.startsWith('expr:')) {
37+
return {
38+
...completion,
39+
description: `<p>${aggLink(completion.value)} pipeline operator</p>`,
40+
};
41+
}
42+
43+
return completion;
3944
});
4045

4146
return createAceCompatAutocompleter({

packages/compass-editor/src/codemirror/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { completeAnyWord, ifIn } from '@codemirror/autocomplete';
22
import type { CompletionContext } from '@codemirror/autocomplete';
33
import { syntaxTree } from '@codemirror/language';
4+
import type { EditorState } from '@codemirror/state';
45

56
export const completeWordsInString = ifIn(['String'], completeAnyWord);
67

@@ -10,6 +11,49 @@ export function resolveTokenAtCursor(context: CompletionContext) {
1011

1112
export type Token = ReturnType<typeof resolveTokenAtCursor>;
1213

14+
export function aggLink(op: string): string {
15+
op = op.replace(/^\$/, '');
16+
return `<a target="_blank" href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/${op}/">$${op}</a>`;
17+
}
18+
19+
export function padLines(str: string, pad = ' ') {
20+
return str
21+
.split('\n')
22+
.map((line) => `${pad}${line}`)
23+
.join('\n');
24+
}
25+
26+
export function* parents(token: Token) {
27+
let parent: Token | null = token;
28+
while ((parent = parent.parent)) {
29+
yield parent;
30+
}
31+
}
32+
33+
export function removeQuotes(str: string) {
34+
return str.replace(/(^('|")|('|")$)/g, '');
35+
}
36+
37+
// lezer tokens are immutable, we check position in syntax tree to make sure we
38+
// are looking at the same token
39+
export function isTokenEqual(a: Token, b: Token) {
40+
return a.from === b.from && a.to === b.to;
41+
}
42+
43+
export function getPropertyNameFromPropertyToken(
44+
editorState: EditorState,
45+
propertyToken: Token
46+
): string {
47+
if (!propertyToken.firstChild) {
48+
return '';
49+
}
50+
return removeQuotes(getTokenText(editorState, propertyToken.firstChild));
51+
}
52+
53+
export function getTokenText(editorState: EditorState, token: Token) {
54+
return editorState.sliceDoc(token.from, token.to);
55+
}
56+
1357
/**
1458
* Returns the list of possible ancestors of the token.
1559
*/

packages/compass-editor/src/json-editor.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,17 @@ function getStylesForTheme(theme: CodemirrorThemeType) {
304304
fontWeight: 'bold',
305305
textDecoration: 'none',
306306
},
307+
'& .cm-tooltip .completion-info p': {
308+
margin: 0,
309+
marginTop: `${spacing[2]}px`,
310+
marginBottom: `${spacing[2]}px`,
311+
},
312+
'& .cm-tooltip .completion-info p:first-child': {
313+
marginTop: 0,
314+
},
315+
'& .cm-tooltip .completion-info p:last-child': {
316+
marginBottom: 0,
317+
},
307318
'& .cm-widgetBuffer': {
308319
// Default is text-top which causes weird 1px added to the line height
309320
// when widget (in our case this is placeholder widget) is shown in the

0 commit comments

Comments
 (0)