Skip to content

Commit

Permalink
align snippet completion logic with vscode
Browse files Browse the repository at this point in the history
fix snippet suggestion does not replace the input string
fix snippet suggestion always displayed all items

Signed-off-by: yewei <yeweiasia@gmail.com>
  • Loading branch information
yeweiasia committed Aug 20, 2019
1 parent e8a29ca commit a13b602
Showing 1 changed file with 102 additions and 6 deletions.
108 changes: 102 additions & 6 deletions packages/monaco/src/browser/monaco-snippet-suggest-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,96 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as jsoncparser from 'jsonc-parser';
import { injectable, inject } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common';
import { CompletionTriggerKind } from '@theia/languages/lib/browser';

@injectable()
export class MonacoSnippetSuggestProvider implements monaco.modes.ISuggestSupport {

private static readonly _maxPrefix = 10000;

@inject(FileSystem)
protected readonly filesystem: FileSystem;

protected readonly snippets = new Map<string, MonacoSnippetSuggestion[]>();
protected readonly snippets = new Map<string, Snippet[]>();
protected readonly pendingSnippets = new Map<string, Promise<void>[]>();

async provideCompletionItems(model: monaco.editor.ITextModel): Promise<monaco.modes.ISuggestResult> {
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position,
context: monaco.modes.SuggestContext): Promise<monaco.modes.ISuggestResult | undefined> {

// copied and modified from https://github.com/microsoft/vscode/blob/master/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts
if (position.column >= MonacoSnippetSuggestProvider._maxPrefix) {
return undefined;
}

if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && context.triggerCharacter === ' ') {
// no snippets when suggestions have been triggered by space
return undefined;
}

const languageId = model.getModeId(); // TODO: look up a language id at the position
await this.loadSnippets(languageId);
const suggestions = this.snippets.get(languageId) || [];
const snippetsForLanguage = this.snippets.get(languageId) || [];

let suggestions: MonacoSnippetSuggestion[];
const pos = { lineNumber: position.lineNumber, column: 1 };
const lineOffsets: number[] = [];
const linePrefixLow = model.getLineContent(position.lineNumber).substr(0, position.column - 1).toLowerCase();
const endsInWhitespace = linePrefixLow.match(/\s$/);

while (pos.column < position.column) {
const word = model.getWordAtPosition(pos);
if (word) {
// at a word
lineOffsets.push(word.startColumn - 1);
pos.column = word.endColumn + 1;
if (word.endColumn - 1 < linePrefixLow.length && !/\s/.test(linePrefixLow[word.endColumn - 1])) {
lineOffsets.push(word.endColumn - 1);
}
} else if (!/\s/.test(linePrefixLow[pos.column - 1])) {
// at a none-whitespace character
lineOffsets.push(pos.column - 1);
pos.column += 1;
} else {
// always advance!
pos.column += 1;
}
}

const availableSnippets = new Set<Snippet>();
snippetsForLanguage.forEach(availableSnippets.add, availableSnippets);
suggestions = [];
for (const start of lineOffsets) {
availableSnippets.forEach(snippet => {
if (this.isPatternInWord(linePrefixLow, start, linePrefixLow.length, snippet.prefix.toLowerCase(), 0, snippet.prefix.length)) {
suggestions.push(new MonacoSnippetSuggestion(snippet,
monaco.Range.fromPositions(this.newPositionWithDelta(position, 0, -(linePrefixLow.length - start)), position)));
availableSnippets.delete(snippet);
}
});
}
if (endsInWhitespace || lineOffsets.length === 0) {
// add remaing snippets when the current prefix ends in whitespace or when no
// interesting positions have been found
availableSnippets.forEach(snippet => {
suggestions.push(new MonacoSnippetSuggestion(snippet, monaco.Range.fromPositions(position)));
});
}

// dismbiguate suggestions with same labels
suggestions.sort(MonacoSnippetSuggestion.compareByLabel);
return { suggestions };
}

resolveCompletionItem(_: monaco.editor.ITextModel, __: monaco.Position, item: monaco.modes.ISuggestion): monaco.modes.ISuggestion {
resolveCompletionItem(textModel: monaco.editor.ITextModel, position: monaco.Position, item: monaco.modes.ISuggestion): monaco.modes.ISuggestion {
return item instanceof MonacoSnippetSuggestion ? item.resolve() : item;
}

Expand Down Expand Up @@ -125,12 +193,32 @@ export class MonacoSnippetSuggestProvider implements monaco.modes.ISuggestSuppor
for (const snippet of snippets) {
for (const scope of snippet.scopes) {
const languageSnippets = this.snippets.get(scope) || [];
languageSnippets.push(new MonacoSnippetSuggestion(snippet));
languageSnippets.push(snippet);
this.snippets.set(scope, languageSnippets);
}
}
}

isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}

newPositionWithDelta(position: monaco.Position, deltaLineNumber: number = 0, deltaColumn: number = 0): monaco.Position {
const newLineNumber = position.lineNumber + deltaLineNumber;
const newColumn = position.column + deltaColumn;
if (newLineNumber === position.lineNumber && newColumn === position.column) {
return position;
} else {
return new monaco.Position(newLineNumber, newColumn);
}
}

}

export interface SnippetLoadOptions {
Expand Down Expand Up @@ -167,18 +255,22 @@ export class MonacoSnippetSuggestion implements monaco.modes.ISuggestion {
readonly label: string;
readonly detail: string;
readonly sortText: string;
readonly range: monaco.IRange;
readonly noAutoAccept = true;
readonly type: 'snippet' = 'snippet';
readonly snippetType: 'textmate' = 'textmate';
readonly overwriteBefore?: number;

insertText: string;
documentation?: monaco.IMarkdownString;

constructor(protected readonly snippet: Snippet) {
constructor(protected readonly snippet: Snippet, range: monaco.IRange) {
this.label = snippet.prefix;
this.detail = `${snippet.description || snippet.name} (${snippet.source})`;
this.insertText = snippet.body;
this.sortText = `z-${snippet.prefix}`;
this.range = range;
this.overwriteBefore = this.range.endColumn - this.range.startColumn;
}

protected resolved = false;
Expand All @@ -191,4 +283,8 @@ export class MonacoSnippetSuggestion implements monaco.modes.ISuggestion {
return this;
}

static compareByLabel(a: MonacoSnippetSuggestion, b: MonacoSnippetSuggestion): number {
return a.label > b.label ? 1 : a.label < b.label ? -1 : 0;
}

}

0 comments on commit a13b602

Please sign in to comment.