import type * as monacoTypes from 'monaco-editor'; import { NeverCaseError } from '../util'; import { DataProvider } from './DataProvider'; import { getSituation } from './situation'; import { getCompletions } from './completions'; export type { monacoTypes }; export type Monaco = typeof monacoTypes; export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE'; function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { switch (type) { case 'DURATION': return monaco.languages.CompletionItemKind.Unit; case 'FUNCTION': return monaco.languages.CompletionItemKind.Variable; case 'HISTORY': return monaco.languages.CompletionItemKind.Snippet; case 'LABEL_NAME': return monaco.languages.CompletionItemKind.Enum; case 'LABEL_VALUE': return monaco.languages.CompletionItemKind.EnumMember; case 'METRIC_NAME': return monaco.languages.CompletionItemKind.Constructor; default: throw new NeverCaseError(type); } } export function getCompletionProvider(monaco: Monaco, dataProvider: DataProvider): monacoTypes.languages.CompletionItemProvider { const provideCompletionItems = ( model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, ): monacoTypes.languages.ProviderResult => { const word = model.getWordAtPosition(position); const range = word != null ? monaco.Range.lift({ startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, // Only replace the prefix that was typed before the cursor. Using // word.endColumn here also replaces the identifier suffix when // completion is invoked in the middle of an existing metric name. endColumn: position.column, }) : monaco.Range.fromPositions(position); // documentation says `position` will be "adjusted" in `getOffsetAt` // i don't know what that means, to be sure i clone it const positionClone = { column: position.column, lineNumber: position.lineNumber, }; dataProvider.monacoSettings.setInputInRange(model.getValueInRange(range)); // Check to see if the browser supports window.getSelection() if (window.getSelection) { const selectedText = window.getSelection()?.toString(); // If the user has selected text, adjust the cursor position to be at the start of the selection, instead of the end if (selectedText && selectedText.length > 0) { positionClone.column = positionClone.column - selectedText.length; } } const offset = model.getOffsetAt(positionClone); const situation = getSituation(model.getValue(), offset); const completionsPromise = situation != null ? getCompletions(situation, dataProvider) : Promise.resolve([]); return completionsPromise.then((items) => { // monaco by-default alphabetically orders the items. // to stop it, we use a number-as-string sortkey, // so that monaco keeps the order we use const maxIndexDigits = items.length.toString().length; // Determine the completion range based on situation type let completionRange = range; if (situation && situation.type === 'IN_LABEL_SELECTOR_WITH_LABEL_NAME' && situation.betweenQuotes) { // For label values within quotes, replace from the start of the value to the current position const valueStartPosition = model.getPositionAt(situation.valueStartPos); completionRange = monaco.Range.lift({ startLineNumber: valueStartPosition.lineNumber, endLineNumber: position.lineNumber, startColumn: valueStartPosition.column, endColumn: position.column, }); } const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({ kind: getMonacoCompletionItemKind(item.type, monaco), label: item.label, insertText: item.insertText, detail: item.detail, documentation: item.documentation, sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have range: completionRange, command: { // Label-name completions intentionally chain into the next // completion step. All other accepted items should dismiss the // suggest widget instead of leaving an obsolete list visible. id: item.triggerOnInsert ? 'editor.action.triggerSuggest' : 'hideSuggestWidget', title: '', }, })); return { suggestions, incomplete: dataProvider.monacoSettings.suggestionsIncomplete }; }); }; return { triggerCharacters: ['{', ',', '[', '(', '=', '~', ' ', '"'], provideCompletionItems, }; }