mirror of
https://github.com/Microsoft/vscode
synced 2024-09-19 02:26:04 +00:00
fix #26275
This commit is contained in:
parent
1f3721743d
commit
b83ec1a0e1
|
@ -468,6 +468,43 @@ export function commonSuffixLength(a: string, b: string): number {
|
|||
return len;
|
||||
}
|
||||
|
||||
function substrEquals(a: string, aStart: number, aEnd: number, b: string, bStart: number, bEnd: number): boolean {
|
||||
while (aStart < aEnd && bStart < bEnd) {
|
||||
if (a[aStart] !== b[bStart]) {
|
||||
return false;
|
||||
}
|
||||
aStart += 1;
|
||||
bStart += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the overlap between the suffix of `a` and the prefix of `b`.
|
||||
* For instance `overlap("foobar", "arr, I'm a pirate") === 2`.
|
||||
*/
|
||||
export function overlap(a: string, b: string): number {
|
||||
let aEnd = a.length;
|
||||
let bEnd = b.length;
|
||||
let aStart = aEnd - bEnd;
|
||||
|
||||
if (aStart === 0) {
|
||||
return a === b ? aEnd : 0;
|
||||
} else if (aStart < 0) {
|
||||
bEnd += aStart;
|
||||
aStart = 0;
|
||||
}
|
||||
|
||||
while (aStart < aEnd && bEnd > 0) {
|
||||
if (substrEquals(a, aStart, aEnd, b, 0, bEnd)) {
|
||||
return bEnd;
|
||||
}
|
||||
bEnd -= 1;
|
||||
aStart += 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- unicode
|
||||
// http://en.wikipedia.org/wiki/Surrogate_pair
|
||||
// Returns the code point starting at a specified index in a string
|
||||
|
|
|
@ -93,6 +93,16 @@ suite('Strings', () => {
|
|||
assert.strictEqual(strings.format('Foo {0} Bar. {1}', '(foo)', '.test'), 'Foo (foo) Bar. .test');
|
||||
});
|
||||
|
||||
test('overlap', function () {
|
||||
assert.equal(strings.overlap('foobar', 'arr, I am a priate'), 2);
|
||||
assert.equal(strings.overlap('no', 'overlap'), 1);
|
||||
assert.equal(strings.overlap('no', '0verlap'), 0);
|
||||
assert.equal(strings.overlap('nothing', ''), 0);
|
||||
assert.equal(strings.overlap('', 'nothing'), 0);
|
||||
assert.equal(strings.overlap('full', 'full'), 4);
|
||||
assert.equal(strings.overlap('full', 'fulloverlap'), 4);
|
||||
});
|
||||
|
||||
test('computeLineStarts', function () {
|
||||
function assertLineStart(text: string, ...offsets: number[]): void {
|
||||
const actual = strings.computeLineStarts(text);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IModel } from 'vs/editor/common/editorCommon';
|
||||
import { ISuggestSupport, ISuggestResult, ISuggestion, LanguageId } from 'vs/editor/common/modes';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
@ -13,6 +12,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
|||
import { setSnippetSuggestSupport } from 'vs/editor/contrib/suggest/browser/suggest';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { overlap, compare, startsWith } from 'vs/base/common/strings';
|
||||
|
||||
export const ISnippetsService = createDecorator<ISnippetsService>('snippetService');
|
||||
|
||||
|
@ -23,6 +23,8 @@ export interface ISnippetsService {
|
|||
registerSnippets(languageId: LanguageId, snippets: ISnippet[], owner: string): void;
|
||||
|
||||
visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => void): void;
|
||||
|
||||
getSnippets(languageId: LanguageId): ISnippet[];
|
||||
}
|
||||
|
||||
export interface ISnippet {
|
||||
|
@ -33,11 +35,7 @@ export interface ISnippet {
|
|||
extensionName?: string;
|
||||
}
|
||||
|
||||
interface ISnippetSuggestion extends ISuggestion {
|
||||
disambiguateLabel: string;
|
||||
}
|
||||
|
||||
class SnippetsService implements ISnippetsService {
|
||||
export class SnippetsService implements ISnippetsService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
|
@ -49,14 +47,14 @@ class SnippetsService implements ISnippetsService {
|
|||
setSnippetSuggestSupport(new SnippetSuggestProvider(modeService, this));
|
||||
}
|
||||
|
||||
public registerSnippets(languageId: LanguageId, snippets: ISnippet[], fileName: string): void {
|
||||
registerSnippets(languageId: LanguageId, snippets: ISnippet[], fileName: string): void {
|
||||
if (!this._snippets.has(languageId)) {
|
||||
this._snippets.set(languageId, new Map<string, ISnippet[]>());
|
||||
}
|
||||
this._snippets.get(languageId).set(fileName, snippets);
|
||||
}
|
||||
|
||||
public visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => boolean): void {
|
||||
visitSnippets(languageId: LanguageId, accept: (snippet: ISnippet) => boolean): void {
|
||||
const modeSnippets = this._snippets.get(languageId);
|
||||
if (modeSnippets) {
|
||||
modeSnippets.forEach(snippets => {
|
||||
|
@ -67,6 +65,17 @@ class SnippetsService implements ISnippetsService {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSnippets(languageId: LanguageId): ISnippet[] {
|
||||
const modeSnippets = this._snippets.get(languageId);
|
||||
const ret: ISnippet[] = [];
|
||||
if (modeSnippets) {
|
||||
modeSnippets.forEach(snippets => {
|
||||
ret.push(...snippets);
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ISnippetsService, SnippetsService);
|
||||
|
@ -75,7 +84,13 @@ export interface ISimpleModel {
|
|||
getLineContent(lineNumber: number): string;
|
||||
}
|
||||
|
||||
class SnippetSuggestProvider implements ISuggestSupport {
|
||||
interface ISnippetSuggestion {
|
||||
suggestion: ISuggestion;
|
||||
snippet: ISnippet;
|
||||
}
|
||||
|
||||
|
||||
export class SnippetSuggestProvider implements ISuggestSupport {
|
||||
|
||||
constructor(
|
||||
@IModeService private _modeService: IModeService,
|
||||
|
@ -87,55 +102,57 @@ class SnippetSuggestProvider implements ISuggestSupport {
|
|||
provideCompletionItems(model: IModel, position: Position): ISuggestResult {
|
||||
|
||||
const languageId = this._getLanguageIdAtPosition(model, position);
|
||||
const suggestions: ISnippetSuggestion[] = [];
|
||||
const snippets = this._snippets.getSnippets(languageId);
|
||||
const items: ISnippetSuggestion[] = [];
|
||||
|
||||
const word = model.getWordAtPosition(position);
|
||||
const currentWord = word ? word.word.substring(0, position.column - word.startColumn).toLowerCase() : '';
|
||||
const currentFullWord = getNonWhitespacePrefix(model, position).toLowerCase();
|
||||
const lowWordUntil = model.getWordUntilPosition(position).word.toLowerCase();
|
||||
const lowLineUntil = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).toLowerCase();
|
||||
|
||||
this._snippets.visitSnippets(languageId, s => {
|
||||
const prefixLower = s.prefix.toLowerCase();
|
||||
for (const snippet of snippets) {
|
||||
|
||||
let overwriteBefore = 0;
|
||||
if (currentWord.length > 0) {
|
||||
// there is a word -> the prefix should match that
|
||||
if (strings.startsWith(prefixLower, currentWord)) {
|
||||
overwriteBefore = currentWord.length;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
const lowPrefix = snippet.prefix.toLowerCase();
|
||||
let overwriteBefore: number;
|
||||
|
||||
} else if (currentFullWord.length > currentWord.length) {
|
||||
// there is something -> fine if it matches
|
||||
overwriteBefore = strings.commonPrefixLength(prefixLower, currentFullWord);
|
||||
if (lowWordUntil.length > 0 && startsWith(lowPrefix, lowWordUntil)) {
|
||||
// cheap match on the (none-empty) current word
|
||||
overwriteBefore = lowWordUntil.length;
|
||||
|
||||
} else if (lowLineUntil.length > 0) {
|
||||
// compute overlap between snippet and line on text
|
||||
overwriteBefore = overlap(lowLineUntil, snippet.prefix.toLowerCase());
|
||||
}
|
||||
|
||||
// store in result
|
||||
suggestions.push({
|
||||
type: 'snippet',
|
||||
label: s.prefix,
|
||||
get disambiguateLabel() { return localize('snippetSuggest.longLabel', "{0}, {1}", s.prefix, s.name); },
|
||||
detail: s.extensionName || localize('detail.userSnippet', "User Snippet"),
|
||||
documentation: s.description,
|
||||
insertText: s.codeSnippet,
|
||||
sortText: `${s.prefix}-${s.extensionName || ''}`,
|
||||
noAutoAccept: true,
|
||||
snippetType: 'textmate',
|
||||
overwriteBefore
|
||||
});
|
||||
if (overwriteBefore !== 0) {
|
||||
|
||||
return true;
|
||||
});
|
||||
items.push({
|
||||
snippet,
|
||||
suggestion: {
|
||||
type: 'snippet',
|
||||
label: snippet.prefix,
|
||||
detail: snippet.extensionName || localize('detail.userSnippet', "User Snippet"),
|
||||
documentation: snippet.description,
|
||||
insertText: snippet.codeSnippet,
|
||||
sortText: `${snippet.prefix}-${snippet.extensionName || ''}`,
|
||||
noAutoAccept: true,
|
||||
snippetType: 'textmate',
|
||||
overwriteBefore
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dismbiguate suggestions with same labels
|
||||
let lastSuggestion: ISnippetSuggestion;
|
||||
for (const suggestion of suggestions.sort(SnippetSuggestProvider._compareSuggestionsByLabel)) {
|
||||
if (lastSuggestion && lastSuggestion.label === suggestion.label) {
|
||||
const suggestions: ISuggestion[] = [];
|
||||
let lastItem: ISnippetSuggestion;
|
||||
for (const item of items.sort(SnippetSuggestProvider._compareSuggestionsByLabel)) {
|
||||
if (lastItem && lastItem.suggestion.label === item.suggestion.label) {
|
||||
// use the disambiguateLabel instead of the actual label
|
||||
lastSuggestion.label = lastSuggestion.disambiguateLabel;
|
||||
suggestion.label = suggestion.disambiguateLabel;
|
||||
lastItem.suggestion.label = localize('snippetSuggest.longLabel', "{0}, {1}", lastItem.suggestion.label, lastItem.snippet.name);
|
||||
item.suggestion.label = localize('snippetSuggest.longLabel', "{0}, {1}", item.suggestion.label, item.snippet.name);
|
||||
}
|
||||
lastSuggestion = suggestion;
|
||||
lastItem = item;
|
||||
|
||||
suggestions.push(item.suggestion);
|
||||
}
|
||||
|
||||
return { suggestions };
|
||||
|
@ -154,8 +171,8 @@ class SnippetSuggestProvider implements ISuggestSupport {
|
|||
return languageId;
|
||||
}
|
||||
|
||||
private static _compareSuggestionsByLabel(a: ISuggestion, b: ISuggestion): number {
|
||||
return strings.compare(a.label, b.label);
|
||||
private static _compareSuggestionsByLabel(a: ISnippetSuggestion, b: ISnippetSuggestion): number {
|
||||
return compare(a.suggestion.label, b.suggestion.label);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as assert from 'assert';
|
||||
import { SnippetsService, ISnippet, SnippetSuggestProvider } from 'vs/workbench/parts/snippets/electron-browser/snippetsService';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
|
||||
import { Model } from 'vs/editor/common/model/model';
|
||||
|
||||
suite('SnippetsService', function () {
|
||||
|
||||
suiteSetup(function () {
|
||||
ModesRegistry.registerLanguage({
|
||||
id: 'fooLang',
|
||||
extensions: ['.fooLang',]
|
||||
});
|
||||
});
|
||||
|
||||
let modeService: ModeServiceImpl;
|
||||
let snippetService: SnippetsService;
|
||||
|
||||
setup(function () {
|
||||
modeService = new ModeServiceImpl();
|
||||
snippetService = new SnippetsService(modeService);
|
||||
|
||||
snippetService.registerSnippets(modeService.getLanguageIdentifier('fooLang').id, <ISnippet[]>[{
|
||||
prefix: 'bar',
|
||||
codeSnippet: 'barCodeSnippet',
|
||||
name: 'barTest',
|
||||
description: ''
|
||||
}, {
|
||||
prefix: 'bazz',
|
||||
codeSnippet: 'bazzCodeSnippet',
|
||||
name: 'bazzTest',
|
||||
description: ''
|
||||
}], 'fooFile.json');
|
||||
});
|
||||
|
||||
test('snippet completions - simple', function () {
|
||||
|
||||
const provider = new SnippetSuggestProvider(modeService, snippetService);
|
||||
const model = Model.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang'));
|
||||
|
||||
const result = provider.provideCompletionItems(model, new Position(1, 1));
|
||||
|
||||
assert.equal(result.incomplete, undefined);
|
||||
assert.equal(result.suggestions.length, 2);
|
||||
});
|
||||
|
||||
test('snippet completions - with prefix', function () {
|
||||
|
||||
const provider = new SnippetSuggestProvider(modeService, snippetService);
|
||||
const model = Model.createFromString('bar', undefined, modeService.getLanguageIdentifier('fooLang'));
|
||||
|
||||
const result = provider.provideCompletionItems(model, new Position(1, 4));
|
||||
|
||||
assert.equal(result.incomplete, undefined);
|
||||
assert.equal(result.suggestions.length, 1);
|
||||
assert.equal(result.suggestions[0].label, 'bar');
|
||||
assert.equal(result.suggestions[0].insertText, 'barCodeSnippet');
|
||||
});
|
||||
|
||||
test('Cannot use "<?php" as user snippet prefix anymore, #26275', function () {
|
||||
snippetService.registerSnippets(modeService.getLanguageIdentifier('fooLang').id, <ISnippet[]>[{
|
||||
prefix: '<?php',
|
||||
codeSnippet: 'insert me',
|
||||
name: '',
|
||||
description: ''
|
||||
}], 'barFile.json');
|
||||
|
||||
const provider = new SnippetSuggestProvider(modeService, snippetService);
|
||||
|
||||
let model = Model.createFromString('\t<?php', undefined, modeService.getLanguageIdentifier('fooLang'));
|
||||
let result = provider.provideCompletionItems(model, new Position(1, 7));
|
||||
assert.equal(result.suggestions.length, 1);
|
||||
model.dispose();
|
||||
|
||||
model = Model.createFromString('\t<?', undefined, modeService.getLanguageIdentifier('fooLang'));
|
||||
result = provider.provideCompletionItems(model, new Position(1, 4));
|
||||
assert.equal(result.suggestions.length, 1);
|
||||
model.dispose();
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue