Skip to content
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
100 changes: 86 additions & 14 deletions src/platform/packages/shared/kbn-monaco/src/console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,51 +36,123 @@ export { CONSOLE_THEME_ID } from './language';

export const CONSOLE_TRIGGER_CHARS = ['/', '.', '_', ',', '?', '=', '&', '"'];

const requestMethodRe = /^\s*(GET|POST|PUT|DELETE|HEAD|PATCH)\b/i;
const esqlRequestLineRe = /^\s*post\s+\/?_query(?:\/async)?(?:\s|\?|$)/i;
/**
* Safeguards for request-line lookup. We scan backwards from the cursor until we find the nearest
* request method line (GET/POST/...), but we cap the amount of work to avoid a potentially large
* number of `getLineContent()` calls on very long documents.
*
* If these limits are hit, ES|QL context detection is skipped and we fall back to the
* actions provider (preserving completion behavior, just without ES|QL suggestions).
*/
const MAX_REQUEST_LINE_LOOKBACK_LINES = 2000;
const MAX_REQUEST_LINE_LOOKBACK_CHARS = 100_000;

const findEsqlRequestLineNumber = (
model: monaco.editor.ITextModel,
positionLineNumber: number
): number | undefined => {
for (
let lineNumber = positionLineNumber, scannedLines = 0, scannedChars = 0;
lineNumber >= 1 &&
scannedLines < MAX_REQUEST_LINE_LOOKBACK_LINES &&
scannedChars < MAX_REQUEST_LINE_LOOKBACK_CHARS;
lineNumber--, scannedLines++
) {
const line = model.getLineContent(lineNumber);
scannedChars += line.length + 1;
if (requestMethodRe.test(line)) {
// Only treat this as an ES|QL request if the request line matches POST _query(/async)?...
return esqlRequestLineRe.test(line) ? lineNumber : undefined;
}
}

return undefined;
};

const getRequestTextBeforeCursor = (
model: monaco.editor.ITextModel,
requestLineNumber: number,
position: monaco.Position
): string => {
return model.getValueInRange({
startLineNumber: requestLineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
};

/**
* @description This language definition is used for the console input panel
*/
export const ConsoleLang: LangModuleType = {
ID: CONSOLE_LANG_ID,
lexerRules,
languageConfiguration,
foldingRangeProvider,
getSuggestionProvider: (
esqlCallbacks: Pick<ESQLCallbacks, 'getSources' | 'getPolicies'>,
actionsProvider: MutableRefObject<any>
esqlCallbacks: Pick<ESQLCallbacks, 'getSources' | 'getPolicies'> | undefined,
actionsProvider: MutableRefObject<{
provideCompletionItems: monaco.languages.CompletionItemProvider['provideCompletionItems'];
} | null>
): monaco.languages.CompletionItemProvider => {
return {
// force suggestions when these characters are used
triggerCharacters: [...CONSOLE_TRIGGER_CHARS, ...ESQL_AUTOCOMPLETE_TRIGGER_CHARS],
provideCompletionItems: async (
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken
) => {
const fullText = model.getValue();
const cursorOffset = model.getOffsetAt(position);
const textBeforeCursor = fullText.slice(0, cursorOffset);
// NOTE: Materializing the full editor content (e.g. via `model.getValue()`) can be very
// expensive for large inputs (like pasted JSON with huge string fields). We only do ES|QL
// context detection when the cursor is within a POST /_query request.
const delegateToActionsProvider = () => {
const actions = actionsProvider.current;
return (
actions?.provideCompletionItems(model, position, context, token) ?? {
suggestions: [],
}
);
};

const esqlRequestLineNumber = findEsqlRequestLineNumber(model, position.lineNumber);
if (!esqlRequestLineNumber) {
return delegateToActionsProvider();
}

const requestTextBeforeCursor = getRequestTextBeforeCursor(
model,
esqlRequestLineNumber,
position
);
const { insideTripleQuotes, insideEsqlQuery, esqlQueryIndex } =
checkForTripleQuotesAndEsqlQuery(textBeforeCursor);
checkForTripleQuotesAndEsqlQuery(requestTextBeforeCursor);

if (esqlCallbacks && insideEsqlQuery) {
const queryText = textBeforeCursor.slice(esqlQueryIndex, cursorOffset);
const queryText = requestTextBeforeCursor.slice(esqlQueryIndex);
const unescapedQuery = unescapeInvalidChars(queryText);
const esqlSuggestions = await suggest(
unescapedQuery,
unescapedQuery.length,
context,
esqlCallbacks
);
return {
const completionList: monaco.languages.CompletionList = {
// @ts-expect-error because of range typing: https://github.com/microsoft/monaco-editor/issues/4638
suggestions: wrapAsMonacoSuggestions(
esqlSuggestions,
queryText,
false,
!insideTripleQuotes
),
};
} else if (actionsProvider.current) {
return actionsProvider.current?.provideCompletionItems(model, position, context);
return completionList;
}
return {
suggestions: [],
};
return delegateToActionsProvider();
},
};
},
Expand Down
Loading