💄 move snippet completion provider into its own file

This commit is contained in:
Johannes Rieken 2018-10-15 12:21:52 +02:00
parent 5a38563b0b
commit 2adaa4737a
4 changed files with 186 additions and 172 deletions

View file

@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { compare } from 'vs/base/common/strings';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, LanguageId } from 'vs/editor/common/modes';
import { IModeService } from 'vs/editor/common/services/modeService';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { localize } from 'vs/nls';
import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { Snippet, SnippetSource } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile';
export class SnippetCompletion implements CompletionItem {
label: string;
detail: string;
insertText: string;
documentation: MarkdownString;
range: IRange;
sortText: string;
noAutoAccept: boolean;
kind: CompletionItemKind;
insertTextIsSnippet: true;
constructor(
readonly snippet: Snippet,
range: IRange
) {
this.label = snippet.prefix;
this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source);
this.insertText = snippet.body;
this.range = range;
this.sortText = `${snippet.snippetSource === SnippetSource.Extension ? 'z' : 'a'}-${snippet.prefix}`;
this.noAutoAccept = true;
this.kind = CompletionItemKind.Snippet;
this.insertTextIsSnippet = true;
}
resolve(): this {
this.documentation = new MarkdownString().appendCodeblock('', new SnippetParser().text(this.snippet.codeSnippet));
this.insertText = this.snippet.codeSnippet;
return this;
}
static compareByLabel(a: SnippetCompletion, b: SnippetCompletion): number {
return compare(a.label, b.label);
}
}
export function matches(pattern: string, patternStart: number, word: string, wordStart: number): boolean {
while (patternStart < pattern.length && wordStart < word.length) {
if (pattern[patternStart] === word[wordStart]) {
patternStart += 1;
}
wordStart += 1;
}
return patternStart === pattern.length;
}
export class SnippetCompletionProvider implements CompletionItemProvider {
private static readonly _maxPrefix = 10000;
constructor(
@IModeService
private readonly _modeService: IModeService,
@ISnippetsService
private readonly _snippets: ISnippetsService
) {
//
}
provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext): Promise<CompletionList> {
if (position.column >= SnippetCompletionProvider._maxPrefix) {
return undefined;
}
const languageId = this._getLanguageIdAtPosition(model, position);
return this._snippets.getSnippets(languageId).then(snippets => {
let suggestions: SnippetCompletion[];
let pos = { lineNumber: position.lineNumber, column: 1 };
let lineOffsets: number[] = [];
let linePrefixLow = model.getLineContent(position.lineNumber).substr(0, position.column - 1).toLowerCase();
while (pos.column < position.column) {
let 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;
}
}
if (lineOffsets.length === 0) {
// no interesting spans found -> pick all snippets
suggestions = snippets.map(snippet => new SnippetCompletion(snippet, Range.fromPositions(position)));
}
else {
let consumed = new Set<Snippet>();
suggestions = [];
for (let start of lineOffsets) {
for (const snippet of snippets) {
if (!consumed.has(snippet) && matches(linePrefixLow, start, snippet.prefixLow, 0)) {
suggestions.push(new SnippetCompletion(snippet, Range.fromPositions(position.delta(0, -(linePrefixLow.length - start)), position)));
consumed.add(snippet);
}
}
}
}
// dismbiguate suggestions with same labels
suggestions.sort(SnippetCompletion.compareByLabel);
for (let i = 0; i < suggestions.length; i++) {
let item = suggestions[i];
let to = i + 1;
for (; to < suggestions.length && item.label === suggestions[to].label; to++) {
suggestions[to].label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[to].label, suggestions[to].snippet.name);
}
if (to > i + 1) {
suggestions[i].label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[i].label, suggestions[i].snippet.name);
i = to;
}
}
return { suggestions };
});
}
resolveCompletionItem?(model: ITextModel, position: Position, item: CompletionItem): CompletionItem {
return (item instanceof SnippetCompletion) ? item.resolve() : item;
}
private _getLanguageIdAtPosition(model: ITextModel, position: Position): LanguageId {
// validate the `languageId` to ensure this is a user
// facing language with a name and the chance to have
// snippets, else fall back to the outer language
model.tokenizeIfCheap(position.lineNumber);
let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column);
let { language } = this._modeService.getLanguageIdentifier(languageId);
if (!this._modeService.getLanguageName(language)) {
languageId = model.getLanguageIdentifier().id;
}
return languageId;
}
}

View file

@ -4,32 +4,29 @@
*--------------------------------------------------------------------------------------------*/
import { basename, extname, join } from 'path';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { dispose, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { combinedDisposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { values } from 'vs/base/common/map';
import * as resources from 'vs/base/common/resources';
import { compare, endsWith, isFalsyOrWhitespace } from 'vs/base/common/strings';
import { endsWith, isFalsyOrWhitespace } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { ITextModel } from 'vs/editor/common/model';
import { CompletionItem, CompletionList, CompletionItemProvider, LanguageId, CompletionContext, CompletionItemKind } from 'vs/editor/common/modes';
import { LanguageId } from 'vs/editor/common/modes';
import { IModeService } from 'vs/editor/common/services/modeService';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/suggest';
import { localize } from 'vs/nls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
import { FileChangeType, IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { Snippet, SnippetFile, SnippetSource } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile';
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService';
import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { IRange, Range } from 'vs/editor/common/core/range';
import { SnippetCompletionProvider } from './snippetCompletionProvider';
namespace snippetExt {
@ -152,7 +149,7 @@ class SnippetsService implements ISnippetsService {
this._initWorkspaceSnippets();
})));
setSnippetSuggestSupport(new SnippetSuggestProvider(this._modeService, this));
setSnippetSuggestSupport(new SnippetCompletionProvider(this._modeService, this));
}
dispose(): void {
@ -320,153 +317,6 @@ export interface ISimpleModel {
getLineContent(lineNumber: number): string;
}
export class SnippetSuggestion implements CompletionItem {
label: string;
detail: string;
insertText: string;
documentation: MarkdownString;
range: IRange;
sortText: string;
noAutoAccept: boolean;
kind: CompletionItemKind;
insertTextIsSnippet: true;
constructor(
readonly snippet: Snippet,
range: IRange
) {
this.label = snippet.prefix;
this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source);
this.insertText = snippet.body;
this.range = range;
this.sortText = `${snippet.snippetSource === SnippetSource.Extension ? 'z' : 'a'}-${snippet.prefix}`;
this.noAutoAccept = true;
this.kind = CompletionItemKind.Snippet;
this.insertTextIsSnippet = true;
}
resolve(): this {
this.documentation = new MarkdownString().appendCodeblock('', new SnippetParser().text(this.snippet.codeSnippet));
this.insertText = this.snippet.codeSnippet;
return this;
}
static compareByLabel(a: SnippetSuggestion, b: SnippetSuggestion): number {
return compare(a.label, b.label);
}
}
export class SnippetSuggestProvider implements CompletionItemProvider {
private static readonly _maxPrefix = 10000;
constructor(
@IModeService private readonly _modeService: IModeService,
@ISnippetsService private readonly _snippets: ISnippetsService
) {
//
}
provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext): Promise<CompletionList> {
if (position.column >= SnippetSuggestProvider._maxPrefix) {
return undefined;
}
const languageId = this._getLanguageIdAtPosition(model, position);
return this._snippets.getSnippets(languageId).then(snippets => {
let suggestions: SnippetSuggestion[];
let pos = { lineNumber: position.lineNumber, column: 1 };
let lineOffsets: number[] = [];
let linePrefixLow = model.getLineContent(position.lineNumber).substr(0, position.column - 1).toLowerCase();
while (pos.column < position.column) {
let 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;
}
}
if (lineOffsets.length === 0) {
// no interesting spans found -> pick all snippets
suggestions = snippets.map(snippet => new SnippetSuggestion(snippet, Range.fromPositions(position)));
} else {
let consumed = new Set<Snippet>();
suggestions = [];
for (let start of lineOffsets) {
for (const snippet of snippets) {
if (!consumed.has(snippet) && matches(linePrefixLow, start, snippet.prefixLow, 0)) {
suggestions.push(new SnippetSuggestion(snippet, Range.fromPositions(position.delta(0, -(linePrefixLow.length - start)), position)));
consumed.add(snippet);
}
}
}
}
// dismbiguate suggestions with same labels
suggestions.sort(SnippetSuggestion.compareByLabel);
for (let i = 0; i < suggestions.length; i++) {
let item = suggestions[i];
let to = i + 1;
for (; to < suggestions.length && item.label === suggestions[to].label; to++) {
suggestions[to].label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[to].label, suggestions[to].snippet.name);
}
if (to > i + 1) {
suggestions[i].label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[i].label, suggestions[i].snippet.name);
i = to;
}
}
return { suggestions };
});
}
resolveCompletionItem?(model: ITextModel, position: Position, item: CompletionItem): CompletionItem {
return (item instanceof SnippetSuggestion) ? item.resolve() : item;
}
private _getLanguageIdAtPosition(model: ITextModel, position: Position): LanguageId {
// validate the `languageId` to ensure this is a user
// facing language with a name and the chance to have
// snippets, else fall back to the outer language
model.tokenizeIfCheap(position.lineNumber);
let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column);
let { language } = this._modeService.getLanguageIdentifier(languageId);
if (!this._modeService.getLanguageName(language)) {
languageId = model.getLanguageIdentifier().id;
}
return languageId;
}
}
function matches(pattern: string, patternStart: number, word: string, wordStart: number): boolean {
while (patternStart < pattern.length && wordStart < word.length) {
if (pattern[patternStart] === word[wordStart]) {
patternStart += 1;
}
wordStart += 1;
}
return patternStart === pattern.length;
}
export function getNonWhitespacePrefix(model: ISimpleModel, position: Position): string {
/**
* Do not analyze more characters

View file

@ -7,7 +7,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
import { RawContextKey, IContextKeyService, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ISnippetsService } from 'vs/workbench/parts/snippets/electron-browser/snippets.contribution';
import { getNonWhitespacePrefix, SnippetSuggestion } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { getNonWhitespacePrefix } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { endsWith } from 'vs/base/common/strings';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as editorCommon from 'vs/editor/common/editorCommon';
@ -18,6 +18,7 @@ import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/suggest';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Snippet } from 'vs/workbench/parts/snippets/electron-browser/snippetsFile';
import { SnippetCompletion } from 'vs/workbench/parts/snippets/electron-browser/snippetCompletionProvider';
export class TabCompletionController implements editorCommon.IEditorContribution {
@ -128,7 +129,7 @@ export class TabCompletionController implements editorCommon.IEditorContribution
showSimpleSuggestions(this._editor, this._activeSnippets.map(snippet => {
const position = this._editor.getPosition();
const range = Range.fromPositions(position.delta(0, -snippet.prefix.length), position);
return new SnippetSuggestion(snippet, range);
return new SnippetCompletion(snippet, range);
}));
}
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { SnippetSuggestProvider } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
import { SnippetCompletionProvider } from 'vs/workbench/parts/snippets/electron-browser/snippetCompletionProvider';
import { Position } from 'vs/editor/common/core/position';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
@ -65,7 +65,7 @@ suite('SnippetsService', function () {
test('snippet completions - simple', function () {
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
const model = TextModel.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), suggestContext).then(result => {
@ -76,7 +76,7 @@ suite('SnippetsService', function () {
test('snippet completions - with prefix', function () {
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
const model = TextModel.createFromString('bar', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 4), suggestContext).then(result => {
@ -108,7 +108,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
const model = TextModel.createFromString('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang'));
await provider.provideCompletionItems(model, new Position(1, 3), suggestContext).then(result => {
@ -153,7 +153,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('\t<?php', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 7), suggestContext).then(result => {
@ -188,7 +188,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('<head>\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), suggestContext).then(result => {
@ -218,7 +218,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang'));
return provider.provideCompletionItems(model, new Position(1, 1), suggestContext).then(result => {
@ -239,7 +239,7 @@ suite('SnippetsService', function () {
'',
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('p-', undefined, modeService.getLanguageIdentifier('fooLang'));
@ -264,7 +264,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 158), suggestContext);
@ -283,7 +283,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString(':', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 2), suggestContext);
@ -302,7 +302,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('template', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 9), suggestContext);
@ -322,7 +322,7 @@ suite('SnippetsService', function () {
SnippetSource.User
)]);
const provider = new SnippetSuggestProvider(modeService, snippetService);
const provider = new SnippetCompletionProvider(modeService, snippetService);
let model = TextModel.createFromString('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', undefined, modeService.getLanguageIdentifier('fooLang'));
let result = await provider.provideCompletionItems(model, new Position(1, 158), suggestContext);