Skip to content

Commit

Permalink
[Console] Fix bug with inline autocompletion (elastic#210187)
Browse files Browse the repository at this point in the history
  • Loading branch information
sabarasaba authored Feb 13, 2025
1 parent 3bf3dad commit baadf59
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Mock the function "populateContext" that accesses the autocomplete definitions
*/
import { monaco } from '@kbn/monaco';
import { MonacoEditorActionsProvider } from '../monaco_editor_actions_provider';

const mockPopulateContext = jest.fn();

Expand All @@ -26,6 +27,7 @@ import {
getDocumentationLinkFromAutocomplete,
getUrlPathCompletionItems,
shouldTriggerSuggestions,
getBodyCompletionItems,
} from './autocomplete_utils';

describe('autocomplete_utils', () => {
Expand Down Expand Up @@ -216,4 +218,80 @@ describe('autocomplete_utils', () => {
expect(items.length).toBe(5);
});
});

describe('inline JSON body completion', () => {
it('completes "term" inside {"query": {te}} without extra quotes or missing template', async () => {
// 1) Set up a mock monaco model with two lines of text
// - Line 1: GET index/_search
// - Line 2: {"query": {te}}
// In a real editor, requestStartLineNumber = 1 (0-based vs 1-based might differ),
// so we adjust accordingly in the test.
const mockModel = {
getLineContent: (lineNumber: number) => {
if (lineNumber === 1) {
// request line
return 'GET index/_search';
} else if (lineNumber === 2) {
// inline JSON with partial property 'te'
return '{"query": {te}}';
}
return '';
},
// getValueInRange will return everything from line 2 up to our position
getValueInRange: ({ startLineNumber, endLineNumber }: monaco.IRange) => {
if (startLineNumber === 2 && endLineNumber === 2) {
// partial body up to cursor (we can just return the entire line for simplicity)
return '{"query": {te}}';
}
return '';
},
getWordUntilPosition: () => ({
startColumn: 13, // approximate "te" start
endColumn: 15,
word: 'te',
}),
getLineMaxColumn: () => 999, // large max
} as unknown as monaco.editor.ITextModel;

// 2) The user is on line 2, at column ~15 (after 'te').
const mockPosition = {
lineNumber: 2,
column: 15,
} as monaco.Position;

mockPopulateContext.mockImplementation((...args) => {
const context = args[0][1];
context.autoCompleteSet = [
{
name: 'term',
},
];
});

// 4) We call getBodyCompletionItems, passing requestStartLineNumber = 1
// because line 1 has "GET index/_search", so line 2 is the body.
const mockEditor = {} as MonacoEditorActionsProvider;
const suggestions = await getBodyCompletionItems(
mockModel,
mockPosition,
1, // the line number where the request method/URL is
mockEditor
);

// 5) We should get 1 suggestion for "term"
expect(suggestions).toHaveLength(1);
const termSuggestion = suggestions[0];

// 6) Check the snippet text. For example, if your final snippet logic
// inserts `"term": $0`, we ensure there's no extra quote like ""term"
// and if you have a template for "term", we can check that too.
const insertText = termSuggestion.insertText;

// No double quotes at the start:
expect(insertText).not.toContain('""term"');
// Valid JSON snippet
expect(insertText).toContain('"term"');
expect(insertText).toContain('$0');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -348,33 +348,19 @@ const getInsertText = (
if (name === undefined) {
return '';
}
let insertText = '';
if (typeof name === 'string') {
const bodyContentLines = bodyContent.split('\n');
const currentContentLine = bodyContentLines[bodyContentLines.length - 1];
const incompleteFieldRegex = /.*"[^"]*$/;
if (incompleteFieldRegex.test(currentContentLine)) {
// The cursor is after an unmatched quote (e.g. '..."abc', '..."')
insertText = '';
} else {
// The cursor is at the beginning of a field so the insert text should start with a quote
insertText = '"';
}
if (insertValue && insertValue !== '{' && insertValue !== '[') {
insertText += `${insertValue}"`;
} else {
insertText += `${name}"`;
}
} else {
insertText = name + '';
}

// Always create the insert text with the name first, check the end of the body content
// to decide if we need to add a double quote after the name.
// This is done to avoid adding a double quote if the user is typing a value after the name.
let insertText = bodyContent.trim().endsWith('"') ? `${name}"` : `"${name}"`;

// check if there is template to add
const conditionalTemplate = getConditionalTemplate(name, bodyContent, context.endpoint);
if (conditionalTemplate) {
template = conditionalTemplate;
}
if (template !== undefined && context.addTemplate) {

if (template) {
let templateLines;
const { __raw, value: templateValue } = template;
if (__raw && templateValue) {
Expand All @@ -384,10 +370,16 @@ const getInsertText = (
}
insertText += ': ' + templateLines.join('\n');
} else if (value === '{') {
insertText += '{}';
insertText += ': {$0}';
} else if (value === '[') {
insertText += '[]';
insertText += ': [$0]';
} else if (insertValue && insertValue !== '{' && insertValue !== '[') {
insertText = `"${insertValue}"`;
insertText += ': $0';
} else {
insertText += ': $0';
}

// the string $0 is used to move the cursor between empty curly/square brackets
if (insertText.endsWith('{}')) {
insertText = insertText.substring(0, insertText.length - 2) + '{$0}';
Expand Down
27 changes: 27 additions & 0 deletions test/functional/apps/console/_autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(PageObjects.console.isAutocompleteVisible()).to.be.eql(true);
});

it('correctly autocompletes inline JSON', async () => {
// 1) Type the request line + inline body (two lines total).
await PageObjects.console.enterText('GET index/_search\n{"query": {t');

// 2) Trigger autocomplete
await PageObjects.console.sleepForDebouncePeriod();
await PageObjects.console.promptAutocomplete('e');

// 3) Wait for the autocomplete suggestions to appear
await retry.waitFor('autocomplete to be visible', () =>
PageObjects.console.isAutocompleteVisible()
);

// 4) Press Enter to accept the first suggestion (likely "term")
await PageObjects.console.pressEnter();

// 5) Now check the text in the editor
await retry.try(async () => {
const text = await PageObjects.console.getEditorText();
// Assert we do NOT invalid autocompletions such as `""term"` or `{term"`
expect(text).not.to.contain('""term"');
expect(text).not.to.contain('{term"');
// and that "term" was inserted
expect(text).to.contain('"term"');
});
});

it('should not show duplicate suggestions', async () => {
await PageObjects.console.enterText(`POST _ingest/pipeline/_simulate
{
Expand Down

0 comments on commit baadf59

Please sign in to comment.