Skip to content

Commit 5ba1566

Browse files
authored
feat(compass-editor, compass-query-bar): provide auto completed query history matching user input COMPASS-8018 (#6040)
1 parent 48ad532 commit 5ba1566

15 files changed

+343
-126
lines changed

package-lock.json

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

packages/compass-editor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"typescript": "^5.0.4"
6464
},
6565
"dependencies": {
66-
"@codemirror/autocomplete": "^6.4.0",
66+
"@codemirror/autocomplete": "^6.17.0",
6767
"@codemirror/commands": "^6.1.2",
6868
"@codemirror/lang-javascript": "^6.1.2",
6969
"@codemirror/lang-json": "^6.0.1",

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,30 @@ describe('query autocompleter', function () {
1212

1313
context('when autocompleting outside of stage', function () {
1414
context('with empty pipeline', function () {
15-
it('should return stages', function () {
15+
it('should return stages', async function () {
1616
const completions = getCompletions('[{ $');
1717

1818
expect(
19-
completions.map((completion) => completion.label).sort()
19+
(await completions).map((completion) => completion.label).sort()
2020
).to.deep.eq([...STAGE_OPERATOR_NAMES].sort());
2121
});
2222
});
2323

2424
context('with other stages in the pipeline', function () {
25-
it('should return stages', function () {
25+
it('should return stages', async function () {
2626
const completions = getCompletions('[{$match:{foo: 1}},{$');
2727

2828
expect(
29-
completions.map((completion) => completion.label).sort()
29+
(await completions).map((completion) => completion.label).sort()
3030
).to.deep.eq([...STAGE_OPERATOR_NAMES].sort());
3131
});
3232
});
3333

3434
context('inside block', function () {
35-
it('should not suggest blocks in snippets', function () {
35+
it('should not suggest blocks in snippets', async function () {
3636
const completions = getCompletions(`[{ /** comment */ $`);
3737

38-
completions.forEach((completion) => {
38+
(await completions).forEach((completion) => {
3939
const snippet = applySnippet(completion);
4040
expect(snippet).to.match(
4141
/^[^{]/,
@@ -46,10 +46,10 @@ describe('query autocompleter', function () {
4646
});
4747

4848
context('outside block', function () {
49-
it('should have blocks in snippets', function () {
49+
it('should have blocks in snippets', async function () {
5050
const completions = getCompletions(`[{ $match: {foo: 1} }, $`);
5151

52-
completions.forEach((completion) => {
52+
(await completions).forEach((completion) => {
5353
const snippet = applySnippet(completion);
5454
expect(snippet).to.match(
5555
/^{/,
@@ -61,15 +61,14 @@ describe('query autocompleter', function () {
6161
});
6262

6363
context('when autocompleting inside the stage', function () {
64-
it('should return stage completer results', function () {
64+
it('should return stage completer results', async function () {
6565
const completions = getCompletions('[{$bucket: { _id: "$', {
6666
fields: [{ name: 'foo' }, { name: 'bar' }],
6767
});
6868

69-
expect(completions.map((completion) => completion.label)).to.deep.eq([
70-
'$foo',
71-
'$bar',
72-
]);
69+
expect(
70+
(await completions).map((completion) => completion.label)
71+
).to.deep.eq(['$foo', '$bar']);
7372
});
7473
});
7574
});

packages/compass-editor/src/codemirror/query-autocompleter-with-history.test.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import { createQueryWithHistoryAutocompleter } from './query-autocompleter-with-history';
33
import { setupCodemirrorCompleter } from '../../test/completer';
44
import type { SavedQuery } from '../../dist/codemirror/query-history-autocompleter';
5+
import { createQuery } from './query-history-autocompleter';
56

67
describe('query history autocompleter', function () {
78
const { getCompletions, cleanup } = setupCodemirrorCompleter(
@@ -65,26 +66,31 @@ describe('query history autocompleter', function () {
6566

6667
const mockOnApply: (query: SavedQuery['queryProperties']) => any = () => {};
6768

68-
it('returns all saved queries as completions on click', function () {
69+
it('returns all saved queries as completions on click', async function () {
6970
expect(
70-
getCompletions('{}', savedQueries, undefined, mockOnApply)
71+
await getCompletions('{}', savedQueries, undefined, mockOnApply)
7172
).to.have.lengthOf(5);
7273
});
7374

74-
it('returns normal autocompletion when user starts typing', function () {
75+
it('returns combined completions when user starts typing', async function () {
7576
expect(
76-
getCompletions('foo', savedQueries, undefined, mockOnApply)
77-
).to.have.lengthOf(45);
77+
await getCompletions('foo', savedQueries, undefined, mockOnApply)
78+
).to.have.lengthOf(50);
7879
});
7980

80-
it('completes "any text" when inside a string', function () {
81+
it('completes "any text" when inside a string', async function () {
82+
const prettifiedSavedQueries = savedQueries.map((query) =>
83+
createQuery(query)
84+
);
8185
expect(
82-
getCompletions(
83-
'{ bar: 1, buz: 2, foo: "b',
84-
savedQueries,
85-
undefined,
86-
mockOnApply
86+
(
87+
await getCompletions(
88+
'{ bar: 1, buz: 2, foo: "b',
89+
savedQueries,
90+
undefined,
91+
mockOnApply
92+
)
8793
).map((completion) => completion.label)
88-
).to.deep.eq(['bar', '1', 'buz', '2', 'foo']);
94+
).to.deep.eq(['bar', '1', 'buz', '2', 'foo', ...prettifiedSavedQueries]);
8995
});
9096
});

packages/compass-editor/src/codemirror/query-autocompleter-with-history.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import {
33
createQueryHistoryAutocompleter,
44
} from './query-history-autocompleter';
55
import { createQueryAutocompleter } from './query-autocompleter';
6-
76
import type {
87
CompletionSource,
98
CompletionContext,
9+
CompletionSection,
10+
Completion,
1011
} from '@codemirror/autocomplete';
1112
import type { CompletionOptions } from '../autocompleter';
13+
import { css } from '@mongodb-js/compass-components';
1214

1315
export const createQueryWithHistoryAutocompleter = (
1416
recentQueries: SavedQuery[],
@@ -21,10 +23,55 @@ export const createQueryWithHistoryAutocompleter = (
2123
);
2224

2325
const originalQueryAutocompleter = createQueryAutocompleter(options);
26+
const historySection: CompletionSection = {
27+
name: 'Query History',
28+
header: renderDottedLine,
29+
};
30+
31+
return async function fullCompletions(context: CompletionContext) {
32+
if (context.state.doc.toString() === '{}') {
33+
return queryHistoryAutocompleter(context);
34+
}
35+
36+
const combinedOptions: Completion[] = [];
37+
// completions assigned to a section appear below ones that are not assigned
38+
const baseCompletions = await originalQueryAutocompleter(context);
39+
const historyCompletions = await queryHistoryAutocompleter(context);
2440

25-
return function fullCompletions(context: CompletionContext) {
26-
if (context.state.doc.toString() !== '{}')
27-
return originalQueryAutocompleter(context);
28-
return queryHistoryAutocompleter(context);
41+
if (baseCompletions) {
42+
combinedOptions.push(
43+
...baseCompletions.options.map((option) => ({
44+
...option,
45+
}))
46+
);
47+
}
48+
if (historyCompletions) {
49+
combinedOptions.push(
50+
...historyCompletions.options.map((option) => ({
51+
...option,
52+
section: historySection,
53+
}))
54+
);
55+
}
56+
57+
return {
58+
from: Math.min(
59+
historyCompletions?.from ?? context.pos,
60+
baseCompletions?.from ?? context.pos
61+
),
62+
options: combinedOptions,
63+
};
2964
};
3065
};
66+
67+
const sectionHeaderStyles = css({
68+
display: 'list-item',
69+
borderBottom: '1px dashed #ccc',
70+
margin: `5px 0`,
71+
});
72+
73+
function renderDottedLine(): HTMLElement {
74+
const header = document.createElement('div');
75+
header.className = sectionHeaderStyles;
76+
return header;
77+
}

packages/compass-editor/src/codemirror/query-autocompleter.test.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,33 @@ describe('query autocompleter', function () {
99

1010
after(cleanup);
1111

12-
it('returns all completions when current token is vaguely matches identifier', function () {
13-
expect(getCompletions('foo')).to.have.lengthOf(45);
12+
it('returns all completions when current token is vaguely matches identifier', async function () {
13+
expect(await getCompletions('foo')).to.have.lengthOf(45);
1414
});
1515

16-
it("doesn't return anything when not matching identifier", function () {
17-
expect(getCompletions('[')).to.have.lengthOf(0);
16+
it("doesn't return anything when not matching identifier", async function () {
17+
expect(await getCompletions('[')).to.have.lengthOf(0);
1818
});
1919

20-
it('completes "any text" when inside a string', function () {
20+
it('completes "any text" when inside a string', async function () {
2121
expect(
22-
getCompletions('{ bar: 1, buz: 2, foo: "b').map(
22+
(await getCompletions('{ bar: 1, buz: 2, foo: "b')).map(
2323
(completion) => completion.label
2424
)
2525
).to.deep.eq(['bar', '1', 'buz', '2', 'foo']);
2626
});
2727

28-
it('escapes field names that are not valid identifiers', function () {
28+
it('escapes field names that are not valid identifiers', async function () {
2929
expect(
30-
getCompletions('{ $m', {
31-
fields: [
32-
'field name with spaces',
33-
'dots.and+what@not',
34-
'quotes"in"quotes',
35-
],
36-
})
30+
(
31+
await getCompletions('{ $m', {
32+
fields: [
33+
'field name with spaces',
34+
'dots.and+what@not',
35+
'quotes"in"quotes',
36+
],
37+
} as any)
38+
)
3739
.filter((completion) => completion.detail?.startsWith('field'))
3840
.map((completion) => completion.apply)
3941
).to.deep.eq([

0 commit comments

Comments
 (0)