Prometheus: Fix eager auto completion (#109128)
* fix auto completion * introduce tests * cleaning * remove unnecessary test
This commit is contained in:
@@ -48,6 +48,7 @@ const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
|
||||
suggest: getSuggestOptions(),
|
||||
suggestFontSize: 12,
|
||||
wordWrap: 'on',
|
||||
quickSuggestionsDelay: 250,
|
||||
};
|
||||
|
||||
// this number was chosen by testing various values. it might be necessary
|
||||
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
import { dateTime, TimeRange } from '@grafana/data';
|
||||
import type { Monaco, monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { DataProvider } from './data_provider';
|
||||
import { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider';
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('./completions');
|
||||
jest.mock('./situation');
|
||||
|
||||
const mockGetCompletions = jest.fn();
|
||||
const mockGetSituation = jest.fn();
|
||||
|
||||
jest.mock('./completions', () => ({
|
||||
getCompletions: (...args: Parameters<typeof mockGetCompletions>) => mockGetCompletions(...args),
|
||||
}));
|
||||
|
||||
jest.mock('./situation', () => ({
|
||||
getSituation: (...args: Parameters<typeof mockGetSituation>) => mockGetSituation(...args),
|
||||
}));
|
||||
|
||||
// Create proper Monaco mocks without 'any'
|
||||
const createMockMonaco = (): Monaco => {
|
||||
const mockRange = {
|
||||
lift: jest.fn((range: monacoTypes.IRange) => range),
|
||||
fromPositions: jest.fn((position: monacoTypes.Position) => ({
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endColumn: position.column,
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
languages: {
|
||||
CompletionItemKind: {
|
||||
Unit: 0,
|
||||
Variable: 1,
|
||||
Snippet: 2,
|
||||
Enum: 3,
|
||||
EnumMember: 4,
|
||||
Constructor: 5,
|
||||
} as const,
|
||||
},
|
||||
Range: mockRange,
|
||||
} as unknown as Monaco;
|
||||
};
|
||||
|
||||
const createMockModel = (
|
||||
value: string,
|
||||
mockWord: monacoTypes.editor.IWordAtPosition | null = null
|
||||
): monacoTypes.editor.ITextModel => {
|
||||
return {
|
||||
getValue: () => value,
|
||||
getValueInRange: jest.fn((range: monacoTypes.IRange) => {
|
||||
// Convert to 0-based indexing
|
||||
const startIndex = Math.max(0, range.startColumn - 1);
|
||||
const endIndex = Math.min(value.length, range.endColumn - 1);
|
||||
return value.substring(startIndex, endIndex);
|
||||
}),
|
||||
getWordAtPosition: jest.fn(() => mockWord),
|
||||
getOffsetAt: jest.fn((position: monacoTypes.Position) => position.column - 1),
|
||||
id: 'test-model',
|
||||
} as unknown as monacoTypes.editor.ITextModel;
|
||||
};
|
||||
|
||||
const createMockPosition = (column: number, lineNumber = 1): monacoTypes.Position =>
|
||||
({
|
||||
column,
|
||||
lineNumber,
|
||||
}) as monacoTypes.Position;
|
||||
|
||||
const createMockDataProvider = (): DataProvider => {
|
||||
return {
|
||||
monacoSettings: {
|
||||
setInputInRange: jest.fn(),
|
||||
suggestionsIncomplete: false,
|
||||
},
|
||||
} as unknown as DataProvider;
|
||||
};
|
||||
|
||||
const createMockTimeRange = (): TimeRange => ({
|
||||
from: dateTime(Date.now() - 3600000), // 1 hour ago
|
||||
to: dateTime(Date.now()),
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
});
|
||||
|
||||
describe('monaco-completion-provider', () => {
|
||||
let monaco: Monaco;
|
||||
let dataProvider: DataProvider;
|
||||
let timeRange: TimeRange;
|
||||
|
||||
beforeEach(() => {
|
||||
monaco = createMockMonaco();
|
||||
dataProvider = createMockDataProvider();
|
||||
timeRange = createMockTimeRange();
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
mockGetCompletions.mockResolvedValue([]);
|
||||
mockGetSituation.mockReturnValue({ type: 'METRIC_NAME' });
|
||||
|
||||
// Mock window.getSelection
|
||||
Object.defineProperty(window, 'getSelection', {
|
||||
writable: true,
|
||||
value: jest.fn(() => ({
|
||||
toString: () => '',
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getSuggestOptions', () => {
|
||||
it('should return options with showWords set to false', () => {
|
||||
const options = getSuggestOptions();
|
||||
expect(options).toEqual({
|
||||
showWords: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCompletionProvider', () => {
|
||||
it('should return provider and state objects', () => {
|
||||
const result = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
expect(result).toHaveProperty('provider');
|
||||
expect(result).toHaveProperty('state');
|
||||
expect(result.state).toHaveProperty('isManualTriggerRequested', false);
|
||||
expect(result.provider).toHaveProperty('triggerCharacters');
|
||||
expect(result.provider).toHaveProperty('provideCompletionItems');
|
||||
});
|
||||
|
||||
it('should have correct trigger characters', () => {
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
expect(provider.triggerCharacters).toEqual(['{', ',', '[', '(', '=', '~', ' ', '"']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provideCompletionItems', () => {
|
||||
it('should return empty suggestions when no situation is detected', async () => {
|
||||
mockGetSituation.mockReturnValue(null);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
const model = createMockModel('test');
|
||||
const position = createMockPosition(4);
|
||||
|
||||
const result = await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(result).toEqual({
|
||||
suggestions: [],
|
||||
incomplete: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call getCompletions with correct parameters for normal word', async () => {
|
||||
const mockWord = { word: 'grafana', startColumn: 1, endColumn: 7 };
|
||||
const model = createMockModel('grafana', mockWord);
|
||||
const position = createMockPosition(7);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(mockGetCompletions).toHaveBeenCalledWith(
|
||||
{ type: 'METRIC_NAME' },
|
||||
dataProvider,
|
||||
timeRange,
|
||||
'grafana',
|
||||
'full' // Should be 'full' because word length >= 3
|
||||
);
|
||||
});
|
||||
|
||||
it('should use partial trigger type for short words', async () => {
|
||||
const mockWord = { word: 'go', startColumn: 1, endColumn: 3 };
|
||||
const model = createMockModel('go', mockWord);
|
||||
const position = createMockPosition(3);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(mockGetCompletions).toHaveBeenCalledWith(
|
||||
{ type: 'METRIC_NAME' },
|
||||
dataProvider,
|
||||
timeRange,
|
||||
'go',
|
||||
'partial' // Should be 'partial' because word length < 3
|
||||
);
|
||||
});
|
||||
|
||||
it('should format completion items correctly', async () => {
|
||||
const mockCompletions = [
|
||||
{
|
||||
label: 'test_metric',
|
||||
detail: 'A test metric',
|
||||
insertText: 'test_metric',
|
||||
documentation: 'Test documentation',
|
||||
insertTextRules: undefined,
|
||||
type: 'METRIC_NAME' as const,
|
||||
triggerOnInsert: false,
|
||||
},
|
||||
];
|
||||
|
||||
mockGetCompletions.mockResolvedValue(mockCompletions);
|
||||
|
||||
const mockWord = { word: 'test', startColumn: 1, endColumn: 5 };
|
||||
const model = createMockModel('test', mockWord);
|
||||
const position = createMockPosition(5);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
const result = await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(result?.suggestions).toHaveLength(1);
|
||||
expect(result?.suggestions?.[0]).toMatchObject({
|
||||
label: 'test_metric',
|
||||
detail: 'A test metric',
|
||||
insertText: 'test_metric',
|
||||
documentation: 'Test documentation',
|
||||
kind: 5, // Constructor kind for METRIC_NAME
|
||||
sortText: '0',
|
||||
command: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add trigger command for items with triggerOnInsert', async () => {
|
||||
const mockCompletions = [
|
||||
{
|
||||
label: 'func(',
|
||||
insertText: 'func(',
|
||||
type: 'FUNCTION' as const,
|
||||
triggerOnInsert: true,
|
||||
},
|
||||
];
|
||||
|
||||
mockGetCompletions.mockResolvedValue(mockCompletions);
|
||||
|
||||
const model = createMockModel('func');
|
||||
const position = createMockPosition(4);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
const result = await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(result?.suggestions?.[0]?.command).toEqual({
|
||||
id: 'editor.action.triggerSuggest',
|
||||
title: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('manual trigger handling', () => {
|
||||
it('should use full trigger type for manual trigger', async () => {
|
||||
const mockWord = { word: 'te', startColumn: 1, endColumn: 3 };
|
||||
const model = createMockModel('te', mockWord);
|
||||
const position = createMockPosition(3);
|
||||
|
||||
const { provider, state } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
// Set manual trigger flag
|
||||
state.isManualTriggerRequested = true;
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(mockGetCompletions).toHaveBeenCalledWith(
|
||||
{ type: 'METRIC_NAME' },
|
||||
dataProvider,
|
||||
timeRange,
|
||||
'te',
|
||||
'full' // Should be 'full' despite short word length
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigger character handling', () => {
|
||||
const triggerCharacters = ['{', ',', '[', '(', '=', '~', ' ', '"'];
|
||||
|
||||
triggerCharacters.forEach((triggerChar) => {
|
||||
it(`should use full trigger type for trigger character "${triggerChar}"`, async () => {
|
||||
const testString = `grafana${triggerChar}`;
|
||||
const model = createMockModel(testString, null);
|
||||
const position = createMockPosition(testString.length + 1); // After trigger character (1-indexed)
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(mockGetCompletions).toHaveBeenCalledWith(
|
||||
{ type: 'METRIC_NAME' },
|
||||
dataProvider,
|
||||
timeRange,
|
||||
undefined, // No word at position after trigger char
|
||||
'full'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle trigger character at beginning of line', async () => {
|
||||
const model = createMockModel('{', null);
|
||||
const position = createMockPosition(2); // After the { character
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
// Should not fail and should still call getCompletions
|
||||
expect(mockGetCompletions).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('selection handling', () => {
|
||||
it('should adjust cursor position when text is selected', async () => {
|
||||
// Mock selected text
|
||||
Object.defineProperty(window, 'getSelection', {
|
||||
writable: true,
|
||||
value: jest.fn(() => ({
|
||||
toString: () => 'selected',
|
||||
})),
|
||||
});
|
||||
|
||||
const model = createMockModel('grafana selected');
|
||||
const position = createMockPosition(16); // End of string
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
// Should call getOffsetAt with adjusted position
|
||||
expect(model.getOffsetAt).toHaveBeenCalledWith({
|
||||
column: 8, // 16 - 8 (length of 'selected')
|
||||
lineNumber: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data provider integration', () => {
|
||||
it('should set input range on data provider', async () => {
|
||||
const mockWord = { word: 'test', startColumn: 1, endColumn: 5 };
|
||||
const model = createMockModel('test', mockWord);
|
||||
const position = createMockPosition(5);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(dataProvider.monacoSettings.setInputInRange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return incomplete status from data provider', async () => {
|
||||
dataProvider.monacoSettings.suggestionsIncomplete = true;
|
||||
mockGetCompletions.mockResolvedValue([]);
|
||||
|
||||
const model = createMockModel('test');
|
||||
const position = createMockPosition(4);
|
||||
|
||||
const { provider } = getCompletionProvider(monaco, dataProvider, timeRange);
|
||||
|
||||
const result = await (provider.provideCompletionItems as Function)(model, position);
|
||||
|
||||
expect(result?.incomplete).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+60
-105
@@ -9,6 +9,13 @@ import { NeverCaseError } from './util';
|
||||
|
||||
export type TriggerType = 'partial' | 'full';
|
||||
|
||||
export type MonacoQueryFieldLocalState = {
|
||||
isManualTriggerRequested: boolean;
|
||||
};
|
||||
|
||||
const TRIGGER_CHARACTERS = ['{', ',', '[', '(', '=', '~', ' ', '"'];
|
||||
const MIN_WORD_LENGTH_FOR_FULL_COMPLETIONS = 3;
|
||||
|
||||
export function getSuggestOptions(): monacoTypes.editor.ISuggestOptions {
|
||||
return {
|
||||
// monaco-editor sometimes provides suggestions automatically, i am not
|
||||
@@ -50,19 +57,16 @@ function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): mona
|
||||
}
|
||||
|
||||
function getTriggerType(
|
||||
context: monacoTypes.languages.CompletionContext,
|
||||
word: monacoTypes.editor.IWordAtPosition | null,
|
||||
model: monacoTypes.editor.ITextModel,
|
||||
position: monacoTypes.Position,
|
||||
isManualTrigger: boolean
|
||||
state: MonacoQueryFieldLocalState
|
||||
): TriggerType {
|
||||
// Manual trigger (Ctrl+Space)
|
||||
if (isManualTrigger) {
|
||||
// Manual trigger (Ctrl+Space) - always full completions
|
||||
if (state.isManualTriggerRequested) {
|
||||
return 'full';
|
||||
}
|
||||
|
||||
// Trigger characters
|
||||
const triggerChars = ['{', ',', '[', '(', '=', '~', ' ', '"'];
|
||||
const charBeforeCursor = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
@@ -70,12 +74,12 @@ function getTriggerType(
|
||||
endColumn: position.column,
|
||||
});
|
||||
|
||||
if (triggerChars.includes(charBeforeCursor)) {
|
||||
if (TRIGGER_CHARACTERS.includes(charBeforeCursor)) {
|
||||
return 'full';
|
||||
}
|
||||
|
||||
// Word length >= 3
|
||||
if (word && word.word.length >= 3) {
|
||||
// For typed words of sufficient length, use full completions
|
||||
if (word && word.word.length >= MIN_WORD_LENGTH_FOR_FULL_COMPLETIONS) {
|
||||
return 'full';
|
||||
}
|
||||
|
||||
@@ -86,20 +90,14 @@ export function getCompletionProvider(
|
||||
monaco: Monaco,
|
||||
dataProvider: DataProvider,
|
||||
timeRange: TimeRange
|
||||
): { provider: monacoTypes.languages.CompletionItemProvider; state: { isManualTriggerRequested: boolean } } {
|
||||
// Short debounce to catch rapid typing
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const DEBOUNCE_DELAY = 150; // Much shorter delay to catch rapid typing
|
||||
|
||||
// Simple local state
|
||||
const state = {
|
||||
): { provider: monacoTypes.languages.CompletionItemProvider; state: MonacoQueryFieldLocalState } {
|
||||
const state: MonacoQueryFieldLocalState = {
|
||||
isManualTriggerRequested: false,
|
||||
};
|
||||
|
||||
const provideCompletionItems = (
|
||||
model: monacoTypes.editor.ITextModel,
|
||||
position: monacoTypes.Position,
|
||||
context: monacoTypes.languages.CompletionContext
|
||||
position: monacoTypes.Position
|
||||
): monacoTypes.languages.ProviderResult<monacoTypes.languages.CompletionList> => {
|
||||
const word = model.getWordAtPosition(position);
|
||||
const range =
|
||||
@@ -112,111 +110,68 @@ export function getCompletionProvider(
|
||||
})
|
||||
: monaco.Range.fromPositions(position);
|
||||
|
||||
const isManualTrigger = state.isManualTriggerRequested;
|
||||
if (isManualTrigger) {
|
||||
state.isManualTriggerRequested = false;
|
||||
// Set input range for data provider
|
||||
dataProvider.monacoSettings.setInputInRange(model.getValueInRange(range));
|
||||
|
||||
// Get adjusted position for cursor/selection handling
|
||||
const adjustedPosition = getAdjustedPosition(position);
|
||||
const offset = model.getOffsetAt(adjustedPosition);
|
||||
const situation = getSituation(model.getValue(), offset);
|
||||
|
||||
// Early exit if no situation detected
|
||||
if (situation === null) {
|
||||
return Promise.resolve({ suggestions: [], incomplete: false });
|
||||
}
|
||||
|
||||
const triggerType: TriggerType = getTriggerType(context, word, model, position, isManualTrigger);
|
||||
const triggerType: TriggerType = getTriggerType(word, model, position, state);
|
||||
|
||||
// For immediate triggers (manual, trigger chars, or already 3+ chars), execute immediately
|
||||
const isImmediate = isManualTrigger || triggerType === 'full';
|
||||
return getCompletions(situation, dataProvider, timeRange, word?.word, triggerType).then((items) => {
|
||||
// Monaco by-default alphabetically orders the items.
|
||||
// We use a number-as-string sortkey to maintain our custom order
|
||||
const maxIndexDigits = items.length > 0 ? items.length.toString().length : 1;
|
||||
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({
|
||||
kind: getMonacoCompletionItemKind(item.type, monaco),
|
||||
label: item.label,
|
||||
insertText: item.insertText,
|
||||
insertTextRules: item.insertTextRules,
|
||||
detail: item.detail,
|
||||
documentation: item.documentation,
|
||||
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
|
||||
range,
|
||||
command: item.triggerOnInsert
|
||||
? {
|
||||
id: 'editor.action.triggerSuggest',
|
||||
title: '',
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
if (isImmediate) {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = null;
|
||||
}
|
||||
return executeCompletionLogic(model, position, range, dataProvider, timeRange, word?.word, triggerType);
|
||||
}
|
||||
|
||||
// For typing scenarios, use short debounce to catch rapid typing
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
debounceTimer = setTimeout(() => {
|
||||
// Re-check if we should use full completions after debounce
|
||||
const updatedWord = model.getWordAtPosition(position);
|
||||
const updatedTriggerType: TriggerType = getTriggerType(context, updatedWord, model, position, false)
|
||||
? 'full'
|
||||
: 'partial';
|
||||
|
||||
executeCompletionLogic(
|
||||
model,
|
||||
position,
|
||||
range,
|
||||
dataProvider,
|
||||
timeRange,
|
||||
updatedWord?.word,
|
||||
updatedTriggerType
|
||||
).then(resolve);
|
||||
}, DEBOUNCE_DELAY);
|
||||
return { suggestions, incomplete: dataProvider.monacoSettings.suggestionsIncomplete };
|
||||
});
|
||||
};
|
||||
|
||||
const executeCompletionLogic = async (
|
||||
model: monacoTypes.editor.ITextModel,
|
||||
position: monacoTypes.Position,
|
||||
range: monacoTypes.Range,
|
||||
dataProvider: DataProvider,
|
||||
timeRange: TimeRange,
|
||||
wordText?: string,
|
||||
triggerType: TriggerType = 'full'
|
||||
): Promise<monacoTypes.languages.CompletionList> => {
|
||||
// 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));
|
||||
// Helper function to handle position adjustment for selection
|
||||
function getAdjustedPosition(position: monacoTypes.Position): { column: number; lineNumber: number } {
|
||||
let adjustedColumn = position.column;
|
||||
|
||||
// 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 the user has selected text, adjust the cursor position to be at the start of the selection
|
||||
if (selectedText && selectedText.length > 0) {
|
||||
positionClone.column = positionClone.column - selectedText.length;
|
||||
adjustedColumn = Math.max(1, adjustedColumn - selectedText.length);
|
||||
}
|
||||
}
|
||||
|
||||
const offset = model.getOffsetAt(positionClone);
|
||||
const situation = getSituation(model.getValue(), offset);
|
||||
const completionsPromise =
|
||||
situation != null
|
||||
? getCompletions(situation, dataProvider, timeRange, wordText, triggerType)
|
||||
: 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;
|
||||
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({
|
||||
range,
|
||||
label: item.label,
|
||||
detail: item.detail,
|
||||
insertText: item.insertText,
|
||||
documentation: item.documentation,
|
||||
insertTextRules: item.insertTextRules,
|
||||
kind: getMonacoCompletionItemKind(item.type, monaco),
|
||||
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
|
||||
command: item.triggerOnInsert ? { id: 'editor.action.triggerSuggest', title: '' } : undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
incomplete: dataProvider.monacoSettings.suggestionsIncomplete,
|
||||
};
|
||||
});
|
||||
};
|
||||
return {
|
||||
column: adjustedColumn,
|
||||
lineNumber: position.lineNumber,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
provider: {
|
||||
triggerCharacters: ['{', ',', '[', '(', '=', '~', ' ', '"'],
|
||||
triggerCharacters: TRIGGER_CHARACTERS,
|
||||
provideCompletionItems,
|
||||
},
|
||||
state,
|
||||
|
||||
Reference in New Issue
Block a user