提交 b83ec1a0 编写于 作者: J Johannes Rieken

fix #26275

上级 1f372174
......@@ -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 word = model.getWordAtPosition(position);
const currentWord = word ? word.word.substring(0, position.column - word.startColumn).toLowerCase() : '';
const currentFullWord = getNonWhitespacePrefix(model, position).toLowerCase();
this._snippets.visitSnippets(languageId, s => {
const prefixLower = s.prefix.toLowerCase();
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 snippets = this._snippets.getSnippets(languageId);
const items: ISnippetSuggestion[] = [];
} else if (currentFullWord.length > currentWord.length) {
// there is something -> fine if it matches
overwriteBefore = strings.commonPrefixLength(prefixLower, currentFullWord);
}
const lowWordUntil = model.getWordUntilPosition(position).word.toLowerCase();
const lowLineUntil = model.getLineContent(position.lineNumber).substr(Math.max(0, position.column - 100), position.column - 1).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
});
for (const snippet of snippets) {
const lowPrefix = snippet.prefix.toLowerCase();
let overwriteBefore: number;
return true;
});
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());
}
if (overwriteBefore !== 0) {
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);
}
}
......
/*---------------------------------------------------------------------------------------------
* 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();
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册