mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Console] Fix bug with inline autocompletion (#210187)
This commit is contained in:
parent
3bf3dad7a0
commit
baadf59aa2
3 changed files with 120 additions and 23 deletions
|
@ -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();
|
||||
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
getDocumentationLinkFromAutocomplete,
|
||||
getUrlPathCompletionItems,
|
||||
shouldTriggerSuggestions,
|
||||
getBodyCompletionItems,
|
||||
} from './autocomplete_utils';
|
||||
|
||||
describe('autocomplete_utils', () => {
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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}';
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue