Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Console Monaco migration] Autocomplete fixes #184032

Merged
merged 13 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { CSSProperties, Dispatch } from 'react';
import { debounce } from 'lodash';
import { debounce, range } from 'lodash';
import { ConsoleParsedRequestsProvider, getParsedRequestsProvider, monaco } from '@kbn/monaco';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/react-kibana-mount';
Expand Down Expand Up @@ -347,7 +347,7 @@ export class MonacoEditorActionsProvider {
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext
) {
): Promise<monaco.languages.CompletionList> {
// determine autocomplete type
const autocompleteType = await this.getAutocompleteType(model, position);
if (!autocompleteType) {
Expand Down Expand Up @@ -384,7 +384,12 @@ export class MonacoEditorActionsProvider {
position.lineNumber
);
const requestStartLineNumber = requests[0].startLineNumber;
const suggestions = getBodyCompletionItems(model, position, requestStartLineNumber);
const suggestions = await getBodyCompletionItems(
model,
position,
requestStartLineNumber,
this
);
return {
suggestions,
};
Expand All @@ -394,12 +399,12 @@ export class MonacoEditorActionsProvider {
suggestions: [],
};
}
public provideCompletionItems(
public async provideCompletionItems(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken
): monaco.languages.ProviderResult<monaco.languages.CompletionList> {
): Promise<monaco.languages.CompletionList> {
return this.getSuggestions(model, position, context);
}

Expand Down Expand Up @@ -565,4 +570,24 @@ export class MonacoEditorActionsProvider {
this.editor.setPosition({ lineNumber: firstRequestAfter.endLineNumber, column: 1 });
}
}

/*
* This function is to get an array of line contents
* from startLine to endLine including both line numbers
*/
public getLines(startLine: number, endLine: number): string[] {
const model = this.editor.getModel();
if (!model) {
return [];
}
// range returns an array not including the end of the range, so we need to add 1
return range(startLine, endLine + 1).map((lineNumber) => model.getLineContent(lineNumber));
}

/*
* This function returns the current position of the cursor
*/
public getCurrentPosition(): monaco.IPosition {
return this.editor.getPosition() ?? { lineNumber: 1, column: 1 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
*/

import { monaco } from '@kbn/monaco';
import { MonacoEditorActionsProvider } from '../monaco_editor_actions_provider';
import {
getEndpointBodyCompleteComponents,
getGlobalAutocompleteComponents,
getTopLevelUrlCompleteComponents,
getUnmatchedEndpointComponents,
} from '../../../../../lib/kb';
import { AutoCompleteContext, ResultTerm } from '../../../../../lib/autocomplete/types';
import {
AutoCompleteContext,
type DataAutoCompleteRulesOneOf,
ResultTerm,
} from '../../../../../lib/autocomplete/types';
import { populateContext } from '../../../../../lib/autocomplete/engine';
import type { EditorRequest } from '../types';
import { parseBody, parseLine, parseUrl } from './tokens_utils';
Expand Down Expand Up @@ -133,8 +138,8 @@ export const getUrlPathCompletionItems = (
// map autocomplete items to completion items
.map((item) => {
return {
label: item.name!,
insertText: item.name!,
label: item.name + '',
insertText: item.name + '',
detail: item.meta ?? i18nTexts.endpoint,
// the kind is only used to configure the icon
kind: monaco.languages.CompletionItemKind.Constant,
Expand Down Expand Up @@ -195,8 +200,8 @@ export const getUrlParamsCompletionItems = (
// map autocomplete items to completion items
.map((item) => {
return {
label: item.name!,
insertText: item.name!,
label: item.name + '',
insertText: item.name + '',
detail: item.meta ?? i18nTexts.param,
// the kind is only used to configure the icon
kind: monaco.languages.CompletionItemKind.Constant,
Expand All @@ -211,11 +216,12 @@ export const getUrlParamsCompletionItems = (
/*
* This function returns an array of completion items for the request body params
*/
export const getBodyCompletionItems = (
export const getBodyCompletionItems = async (
model: monaco.editor.ITextModel,
position: monaco.Position,
requestStartLineNumber: number
): monaco.languages.CompletionItem[] => {
requestStartLineNumber: number,
editor: MonacoEditorActionsProvider
): Promise<monaco.languages.CompletionItem[]> => {
const { lineNumber, column } = position;

// get the content on the method+url line
Expand Down Expand Up @@ -244,62 +250,91 @@ export const getBodyCompletionItems = (
} else {
components = getUnmatchedEndpointComponents();
}
populateContext(bodyTokens, context, undefined, true, components);

if (context.autoCompleteSet && context.autoCompleteSet.length > 0) {
const wordUntilPosition = model.getWordUntilPosition(position);
// if there is " after the cursor, replace it
let endColumn = position.column;
const charAfterPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column + 1,
});
if (charAfterPosition === '"') {
endColumn = endColumn + 1;
}
const range = {
startLineNumber: position.lineNumber,
// replace the whole word with the suggestion
startColumn: wordUntilPosition.startColumn,
endLineNumber: position.lineNumber,
endColumn,
};
return (
context.autoCompleteSet
// filter autocomplete items without a name
.filter(({ name }) => Boolean(name))
// map autocomplete items to completion items
.map((item) => {
const suggestion = {
// convert name to a string
label: item.name + '',
insertText: getInsertText(item, bodyContent),
detail: i18nTexts.api,
// the kind is only used to configure the icon
kind: monaco.languages.CompletionItemKind.Constant,
range,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
};
return suggestion;
})
);
context.editor = editor;
context.requestStartRow = requestStartLineNumber;
populateContext(bodyTokens, context, editor, true, components);
if (!context) {
return [];
}
return [];
if (context.asyncResultsState?.isLoading && context.asyncResultsState) {
const results = await context.asyncResultsState.results;
return getSuggestions(model, position, results, context, bodyContent);
}

return getSuggestions(model, position, context.autoCompleteSet ?? [], context, bodyContent);
};

const getSuggestions = (
model: monaco.editor.ITextModel,
position: monaco.Position,
autocompleteSet: ResultTerm[],
context: AutoCompleteContext,
bodyContent: string
) => {
const wordUntilPosition = model.getWordUntilPosition(position);
// if there is " after the cursor, replace it
let endColumn = position.column;
const charAfterPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column + 1,
});
if (charAfterPosition === '"') {
endColumn = endColumn + 1;
}
const range = {
startLineNumber: position.lineNumber,
// replace the whole word with the suggestion
startColumn: wordUntilPosition.startColumn,
endLineNumber: position.lineNumber,
endColumn,
};
return (
autocompleteSet
// filter out items that don't have name
.filter(({ name }) => name !== undefined)
// map autocomplete items to completion items
.map((item) => {
const suggestion = {
// convert name to a string
label: item.name + '',
insertText: getInsertText(item, bodyContent, context),
detail: i18nTexts.api,
// the kind is only used to configure the icon
kind: monaco.languages.CompletionItemKind.Constant,
range,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
};
return suggestion;
})
);
};
const getInsertText = (
{ name, insertValue, template, value }: ResultTerm,
bodyContent: string
bodyContent: string,
context: AutoCompleteContext
): string => {
let insertText = bodyContent.endsWith('"') ? '' : '"';
if (insertValue && insertValue !== '{' && insertValue !== '[') {
insertText += `${insertValue}"`;
if (name === undefined) {
return '';
}
let insertText = '';
if (typeof name === 'string') {
insertText = bodyContent.endsWith('"') ? '' : '"';
if (insertValue && insertValue !== '{' && insertValue !== '[') {
insertText += `${insertValue}"`;
} else {
insertText += `${name}"`;
}
} else {
insertText += `${name}"`;
insertText = name + '';
}

// check if there is template to add
const conditionalTemplate = getConditionalTemplate(name, bodyContent, context.endpoint);
if (conditionalTemplate) {
template = conditionalTemplate;
}
if (template !== undefined) {
let templateLines;
const { __raw, value: templateValue } = template;
Expand All @@ -314,5 +349,42 @@ const getInsertText = (
} else if (value === '[') {
insertText += '[]';
}
// 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}';
}
if (insertText.endsWith('[]')) {
insertText = insertText.substring(0, insertText.length - 2) + '[$0]';
}
return insertText;
};

const getConditionalTemplate = (
name: string | boolean,
bodyContent: string,
endpoint: AutoCompleteContext['endpoint']
) => {
if (typeof name !== 'string' || !endpoint || !endpoint.data_autocomplete_rules) {
return;
}
// get the autocomplete rules for the request body
const { data_autocomplete_rules: autocompleteRules } = endpoint;
// get the rules for this property name
const rules = autocompleteRules[name];
// check if the rules have "__one_of" property
if (!rules || typeof rules !== 'object' || !('__one_of' in rules)) {
return;
}
const oneOfRules = rules.__one_of as DataAutoCompleteRulesOneOf[];
// try to match one of the rules to the body content
const matchedRule = oneOfRules.find((rule) => {
if (rule.__condition && rule.__condition.lines_regex) {
return new RegExp(rule.__condition.lines_regex, 'm').test(bodyContent);
}
return false;
});
// use the template from the matched rule
if (matchedRule && matchedRule.__template) {
return matchedRule.__template;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ describe('tokens_utils', () => {
value: '{"property1":{"nested1":"value","nested2":{}},"',
tokens: ['{'],
},
{
value: '{\n "explain": false,\n "',
tokens: ['{'],
},
];
for (const testCase of testCases) {
const { value, tokens } = testCase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export const parseBody = (value: string): string[] => {
break;
}
case 'f': {
if (peek(1) === 'a' && peek(2) === 'l' && peek(3) === 's' && peek(3) === 'e') {
if (peek(1) === 'a' && peek(2) === 'l' && peek(3) === 's' && peek(4) === 'e') {
next();
next();
next();
Expand Down
9 changes: 5 additions & 4 deletions src/plugins/console/public/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,8 @@ export default function ({
// if not on the first line
if (context.rangeToReplace && context.rangeToReplace.start?.lineNumber > 1) {
const prevTokenLineNumber = position.lineNumber;
const line = context.editor?.getLineValue(prevTokenLineNumber) ?? '';
const editorFromContext = context.editor as CoreEditor | undefined;
const line = editorFromContext?.getLineValue(prevTokenLineNumber) ?? '';
const prevLineLength = line.length;
const linesToEnter = context.rangeToReplace.end.lineNumber - prevTokenLineNumber;

Expand Down Expand Up @@ -1188,7 +1189,7 @@ export default function ({
context: AutoCompleteContext;
completer?: { insertMatch: (v: unknown) => void };
} = {
value: term.name,
value: term.name + '',
meta: 'API',
score: 0,
context,
Expand All @@ -1206,8 +1207,8 @@ export default function ({
);

terms.sort(function (
t1: { score: number; name?: string },
t2: { score: number; name?: string }
t1: { score: number; name?: string | boolean },
t2: { score: number; name?: string | boolean }
) {
/* score sorts from high to low */
if (t1.score > t2.score) {
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/console/public/lib/autocomplete/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
* Side Public License, v 1.
*/

import { MonacoEditorActionsProvider } from '../../application/containers/editor/monaco/monaco_editor_actions_provider';
import { CoreEditor, Range, Token } from '../../types';

export interface ResultTerm {
meta?: string;
context?: AutoCompleteContext;
insertValue?: string;
name?: string;
name?: string | boolean;
value?: string;
score?: number;
template?: { __raw?: boolean; value?: string; [key: string]: unknown };
Expand Down Expand Up @@ -53,7 +54,7 @@ export interface AutoCompleteContext {
replacingToken?: boolean;
rangeToReplace?: Range;
autoCompleteType?: null | string;
editor?: CoreEditor;
editor?: CoreEditor | MonacoEditorActionsProvider;

/**
* The tokenized user input that prompted the current autocomplete at the cursor. This can be out of sync with
Expand Down