Skip to content

Commit

Permalink
feat(compass-editor, compass-query-bar): provide auto completed query…
Browse files Browse the repository at this point in the history
… history matching user input COMPASS-8018 (#6040)
  • Loading branch information
VivianTNT authored Jul 22, 2024
1 parent 48ad532 commit 5ba1566
Show file tree
Hide file tree
Showing 15 changed files with 343 additions and 126 deletions.
64 changes: 32 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/compass-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@codemirror/autocomplete": "^6.4.0",
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.1.2",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/lang-json": "^6.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,30 @@ describe('query autocompleter', function () {

context('when autocompleting outside of stage', function () {
context('with empty pipeline', function () {
it('should return stages', function () {
it('should return stages', async function () {
const completions = getCompletions('[{ $');

expect(
completions.map((completion) => completion.label).sort()
(await completions).map((completion) => completion.label).sort()
).to.deep.eq([...STAGE_OPERATOR_NAMES].sort());
});
});

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

expect(
completions.map((completion) => completion.label).sort()
(await completions).map((completion) => completion.label).sort()
).to.deep.eq([...STAGE_OPERATOR_NAMES].sort());
});
});

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

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

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

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

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

expect(completions.map((completion) => completion.label)).to.deep.eq([
'$foo',
'$bar',
]);
expect(
(await completions).map((completion) => completion.label)
).to.deep.eq(['$foo', '$bar']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import { createQueryWithHistoryAutocompleter } from './query-autocompleter-with-history';
import { setupCodemirrorCompleter } from '../../test/completer';
import type { SavedQuery } from '../../dist/codemirror/query-history-autocompleter';
import { createQuery } from './query-history-autocompleter';

describe('query history autocompleter', function () {
const { getCompletions, cleanup } = setupCodemirrorCompleter(
Expand Down Expand Up @@ -65,26 +66,31 @@ describe('query history autocompleter', function () {

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

it('returns all saved queries as completions on click', function () {
it('returns all saved queries as completions on click', async function () {
expect(
getCompletions('{}', savedQueries, undefined, mockOnApply)
await getCompletions('{}', savedQueries, undefined, mockOnApply)
).to.have.lengthOf(5);
});

it('returns normal autocompletion when user starts typing', function () {
it('returns combined completions when user starts typing', async function () {
expect(
getCompletions('foo', savedQueries, undefined, mockOnApply)
).to.have.lengthOf(45);
await getCompletions('foo', savedQueries, undefined, mockOnApply)
).to.have.lengthOf(50);
});

it('completes "any text" when inside a string', function () {
it('completes "any text" when inside a string', async function () {
const prettifiedSavedQueries = savedQueries.map((query) =>
createQuery(query)
);
expect(
getCompletions(
'{ bar: 1, buz: 2, foo: "b',
savedQueries,
undefined,
mockOnApply
(
await getCompletions(
'{ bar: 1, buz: 2, foo: "b',
savedQueries,
undefined,
mockOnApply
)
).map((completion) => completion.label)
).to.deep.eq(['bar', '1', 'buz', '2', 'foo']);
).to.deep.eq(['bar', '1', 'buz', '2', 'foo', ...prettifiedSavedQueries]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
createQueryHistoryAutocompleter,
} from './query-history-autocompleter';
import { createQueryAutocompleter } from './query-autocompleter';

import type {
CompletionSource,
CompletionContext,
CompletionSection,
Completion,
} from '@codemirror/autocomplete';
import type { CompletionOptions } from '../autocompleter';
import { css } from '@mongodb-js/compass-components';

export const createQueryWithHistoryAutocompleter = (
recentQueries: SavedQuery[],
Expand All @@ -21,10 +23,55 @@ export const createQueryWithHistoryAutocompleter = (
);

const originalQueryAutocompleter = createQueryAutocompleter(options);
const historySection: CompletionSection = {
name: 'Query History',
header: renderDottedLine,
};

return async function fullCompletions(context: CompletionContext) {
if (context.state.doc.toString() === '{}') {
return queryHistoryAutocompleter(context);
}

const combinedOptions: Completion[] = [];
// completions assigned to a section appear below ones that are not assigned
const baseCompletions = await originalQueryAutocompleter(context);
const historyCompletions = await queryHistoryAutocompleter(context);

return function fullCompletions(context: CompletionContext) {
if (context.state.doc.toString() !== '{}')
return originalQueryAutocompleter(context);
return queryHistoryAutocompleter(context);
if (baseCompletions) {
combinedOptions.push(
...baseCompletions.options.map((option) => ({
...option,
}))
);
}
if (historyCompletions) {
combinedOptions.push(
...historyCompletions.options.map((option) => ({
...option,
section: historySection,
}))
);
}

return {
from: Math.min(
historyCompletions?.from ?? context.pos,
baseCompletions?.from ?? context.pos
),
options: combinedOptions,
};
};
};

const sectionHeaderStyles = css({
display: 'list-item',
borderBottom: '1px dashed #ccc',
margin: `5px 0`,
});

function renderDottedLine(): HTMLElement {
const header = document.createElement('div');
header.className = sectionHeaderStyles;
return header;
}
30 changes: 16 additions & 14 deletions packages/compass-editor/src/codemirror/query-autocompleter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,33 @@ describe('query autocompleter', function () {

after(cleanup);

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

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

it('completes "any text" when inside a string', function () {
it('completes "any text" when inside a string', async function () {
expect(
getCompletions('{ bar: 1, buz: 2, foo: "b').map(
(await getCompletions('{ bar: 1, buz: 2, foo: "b')).map(
(completion) => completion.label
)
).to.deep.eq(['bar', '1', 'buz', '2', 'foo']);
});

it('escapes field names that are not valid identifiers', function () {
it('escapes field names that are not valid identifiers', async function () {
expect(
getCompletions('{ $m', {
fields: [
'field name with spaces',
'dots.and+what@not',
'quotes"in"quotes',
],
})
(
await getCompletions('{ $m', {
fields: [
'field name with spaces',
'dots.and+what@not',
'quotes"in"quotes',
],
} as any)
)
.filter((completion) => completion.detail?.startsWith('field'))
.map((completion) => completion.apply)
).to.deep.eq([
Expand Down
Loading

0 comments on commit 5ba1566

Please sign in to comment.