Skip to content

Commit

Permalink
extract autocomplete logic + add support for constructor autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
alisonelizabeth committed Oct 14, 2020
1 parent ab6ef61 commit 1de9f52
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ interface Method {
return: string;
}

interface Constructor {
declaring: string;
parameters: string[];
}

interface ContextClass {
name: string;
imported: boolean;
constructors: any[];
constructors: Constructor[];
static_methods: Method[];
methods: Method[];
static_fields: Field[];
Expand Down Expand Up @@ -101,6 +106,7 @@ const indexToLetterMap: {
5: 'f',
};

// TODO for now assuming we will always have parameters and return value
const getMethodDescription = (
methodName: string,
parameters: string[],
Expand All @@ -123,6 +129,38 @@ const getMethodDescription = (
return `${methodName}(${parameterDescription}): ${returnValue}`;
};

export const getPainlessConstructorsToAutocomplete = (
range: monaco.IRange
): monaco.languages.CompletionItem[] => {
const painlessConstructors = context.classes
.filter(({ constructors }) => constructors.length > 0)
.map(({ constructors }) => constructors)
.flat();

return (
painlessConstructors
// There are sometimes multiple definitions for the same constructor
// This method filters them out so we don't display more than once in autocomplete
.filter((constructor, index, constructorArray) => {
return (
constructorArray.findIndex(({ declaring }) => declaring === constructor.declaring) ===
index
);
})
.map(({ declaring }) => {
const constructorName = declaring.split('.').pop() || declaring; // TODO probably need something more sophisticated here

return {
label: constructorName,
kind: monaco.languages.CompletionItemKind.Constructor,
documentation: `Constructor ${constructorName}`,
insertText: constructorName,
range,
};
})
);
};

export const getPainlessClassToAutocomplete = (
className: string,
range: monaco.IRange
Expand Down
73 changes: 73 additions & 0 deletions x-pack/plugins/painless_lab/public/services/completion_adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { monaco } from '@kbn/monaco';

import {
getPainlessClassToAutocomplete,
painlessTypes,
getPainlessClassesToAutocomplete,
getPainlessConstructorsToAutocomplete,
} from './autocomplete_utils';

export class PainlessCompletionAdapter implements monaco.languages.CompletionItemProvider {
// constructor(private _worker: WorkerAccessor) {}

public get triggerCharacters(): string[] {
return ['.'];
}

provideCompletionItems(
model: monaco.editor.IReadOnlyModel,
position: monaco.Position,
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken
): Promise<monaco.languages.CompletionList> {
// Active line characters, e.g., "boolean isInCircle"
const activeCharacters = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 0,
endLineNumber: position.lineNumber,
endColumn: position.column,
});

const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};

// Array of the active line words, e.g., [boolean, isInCircle]
const words = activeCharacters.replace('\t', '').split(' ');
// What the user is currently typing
const activeTyping = words[words.length - 1];
// If the active typing contains dot notation, we assume we need to access the object's properties
const isProperty = activeTyping.split('.').length === 2;
const hasDeclaredType = words.length === 2 && painlessTypes.includes(words[0]);
const isConstructor = words[words.length - 2] === 'new';

let autocompleteSuggestions: monaco.languages.CompletionItem[] = [];

if (isConstructor) {
autocompleteSuggestions = getPainlessConstructorsToAutocomplete(range);
} else if (isProperty) {
const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0];

autocompleteSuggestions = getPainlessClassToAutocomplete(className, range);
} else {
// If the preceding word is a type, e.g., "boolean", we assume the user is declaring a variable and skip autocomplete
if (!hasDeclaredType) {
autocompleteSuggestions = getPainlessClassesToAutocomplete(range);
}
}

return Promise.resolve({
isIncomplete: false,
suggestions: autocompleteSuggestions,
});
}
}
54 changes: 2 additions & 52 deletions x-pack/plugins/painless_lab/public/services/language_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ import workerSrc from 'raw-loader!monaco-editor/min/vs/base/worker/workerMain.js

import { monacoPainlessLang } from '../lib';

import {
getPainlessClassToAutocomplete,
painlessTypes,
getPainlessClassesToAutocomplete,
} from './autocomplete_utils';
import { PainlessCompletionAdapter } from './completion_adapter';

const LANGUAGE_ID = 'painless';

Expand All @@ -31,53 +27,7 @@ export class LanguageService {
public setup() {
monaco.languages.register({ id: LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, monacoPainlessLang);

monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, {
triggerCharacters: ['.'],
provideCompletionItems(model, position) {
// Active line characters, e.g., "boolean isInCircle"
const activeCharacters = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 0,
endLineNumber: position.lineNumber,
endColumn: position.column,
});

const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};

// Array of the active line words, e.g., [boolean, isInCircle]
const words = activeCharacters.replace('\t', '').split(' ');
// What the user is currently typing
const activeTyping = words[words.length - 1];
// If the active typing contains dot notation, we assume we need to access the object's properties
const isProperty = activeTyping.split('.').length === 2;
// If the active typing contains a type, we skip autocomplete, e.g., "boolean"
const isType = words.length === 2 && painlessTypes.includes(words[0]);

let autocompleteSuggestions: monaco.languages.CompletionItem[] = [];

if (isProperty) {
const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0];

autocompleteSuggestions = getPainlessClassToAutocomplete(className, range);
} else {
if (!isType) {
autocompleteSuggestions = getPainlessClassesToAutocomplete(range);
}
}

return {
incomplete: true,
suggestions: autocompleteSuggestions,
};
},
});
monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, new PainlessCompletionAdapter());

if (CAN_CREATE_WORKER) {
this.originalMonacoEnvironment = (window as any).MonacoEnvironment;
Expand Down

0 comments on commit 1de9f52

Please sign in to comment.