add concept of top level snippet and allow to get snippets without language filter

This commit is contained in:
Johannes
2022-07-15 15:38:55 +02:00
parent b8e6d89295
commit 36846c19db
7 changed files with 95 additions and 42 deletions
@@ -95,6 +95,7 @@ class InsertSnippetAction extends EditorAction {
if (snippet) {
return resolve(new Snippet(
false,
[],
'',
'',
@@ -16,6 +16,7 @@ export interface ISnippetGetOptions {
includeDisabledSnippets?: boolean;
includeNoPrefixSnippets?: boolean;
noRecencySort?: boolean;
topLevelSnippets?: boolean;
}
export interface ISnippetsService {
@@ -30,7 +31,7 @@ export interface ISnippetsService {
updateUsageTimestamp(snippet: Snippet): void;
getSnippets(languageId: string, opt?: ISnippetGetOptions): Promise<Snippet[]>;
getSnippets(languageId: string | undefined, opt?: ISnippetGetOptions): Promise<Snippet[]>;
getSnippetsSync(languageId: string, opt?: ISnippetGetOptions): Snippet[];
}
@@ -42,6 +43,10 @@ const snippetSchemaProperties: IJSONSchemaMap = {
description: nls.localize('snippetSchema.json.prefix', 'The prefix to use when selecting the snippet in intellisense'),
type: ['string', 'array']
},
isTopLevel: {
description: nls.localize('snippetSchema.json.isTopLevel', 'The snippet is only applicable to empty files.'),
type: 'string'
},
body: {
markdownDescription: nls.localize('snippetSchema.json.body', 'The snippet content. Use `$1`, `${1:defaultText}` to define cursor positions, use `$0` for the final cursor position. Insert variable values with `${varName}` and `${varName:defaultText}`, e.g. `This is file: $TM_FILENAME`.'),
type: ['string', 'array'],
@@ -8,7 +8,6 @@ import { localize } from 'vs/nls';
import { extname, basename } from 'vs/base/common/path';
import { SnippetParser, Variable, Placeholder, Text } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { KnownSnippetVariableNames } from 'vs/editor/contrib/snippet/browser/snippetVariables';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -106,6 +105,7 @@ export class Snippet {
readonly prefixLow: string;
constructor(
readonly isTopLevel: boolean,
readonly scopes: string[],
readonly name: string,
readonly prefix: string,
@@ -143,8 +143,9 @@ export class Snippet {
interface JsonSerializedSnippet {
isTopLevel?: boolean;
body: string | string[];
scope: string;
scope?: string;
prefix: string | string[] | undefined;
description: string;
}
@@ -260,7 +261,7 @@ export class SnippetFile {
private _parseSnippet(name: string, snippet: JsonSerializedSnippet, bucket: Snippet[]): void {
let { prefix, body, description } = snippet;
let { isTopLevel, prefix, body, description } = snippet;
if (!prefix) {
prefix = '';
@@ -281,7 +282,7 @@ export class SnippetFile {
if (this.defaultScopes) {
scopes = this.defaultScopes;
} else if (typeof snippet.scope === 'string') {
scopes = snippet.scope.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s));
scopes = snippet.scope.split(',').map(s => s.trim()).filter(Boolean);
} else {
scopes = [];
}
@@ -305,6 +306,7 @@ export class SnippetFile {
for (const _prefix of Array.isArray(prefix) ? prefix : Iterable.single(prefix)) {
bucket.push(new Snippet(
Boolean(isTopLevel),
scopes,
name,
_prefix,
@@ -31,6 +31,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { insertInto } from 'vs/base/common/arrays';
namespace snippetExt {
@@ -265,16 +266,25 @@ class SnippetsService implements ISnippetsService {
return this._files.values();
}
async getSnippets(languageId: string, opts?: ISnippetGetOptions): Promise<Snippet[]> {
async getSnippets(languageId: string | undefined, opts?: ISnippetGetOptions): Promise<Snippet[]> {
await this._joinSnippets();
const result: Snippet[] = [];
const promises: Promise<any>[] = [];
if (this._languageService.isRegisteredLanguageId(languageId)) {
if (languageId) {
if (this._languageService.isRegisteredLanguageId(languageId)) {
for (const file of this._files.values()) {
promises.push(file.load()
.then(file => file.select(languageId, result))
.catch(err => this._logService.error(err, file.location.toString()))
);
}
}
} else {
for (const file of this._files.values()) {
promises.push(file.load()
.then(file => file.select(languageId, result))
.then(file => insertInto(result, result.length, file.data))
.catch(err => this._logService.error(err, file.location.toString()))
);
}
@@ -297,10 +307,25 @@ class SnippetsService implements ISnippetsService {
}
private _filterAndSortSnippets(snippets: Snippet[], opts?: ISnippetGetOptions): Snippet[] {
const result = snippets.filter(snippet => {
return (snippet.prefix || opts?.includeNoPrefixSnippets) // prefix or no-prefix wanted
&& (this.isEnabled(snippet) || opts?.includeDisabledSnippets); // enabled or disabled wanted
});
const result: Snippet[] = [];
for (const snippet of snippets) {
if (!snippet.prefix && !opts?.includeNoPrefixSnippets) {
// prefix or no-prefix wanted
continue;
}
if (!this.isEnabled(snippet) && !opts?.includeDisabledSnippets) {
// enabled or disabled wanted
continue;
}
if (typeof opts?.topLevelSnippets === 'boolean' && opts.topLevelSnippets !== snippet.isTopLevel) {
// isTopLevel requested but mismatching
continue;
}
result.push(snippet);
}
return result.sort((a, b) => {
let result = 0;
@@ -25,12 +25,12 @@ suite('Snippets', function () {
assert.strictEqual(bucket.length, 0);
file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [
new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(['foo'], 'FooSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(['bar'], 'BarSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(['bar.comment'], 'BarSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(['bar.strings'], 'BarSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(['bazz', 'bazz'], 'BazzSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['foo'], 'FooSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['bar'], 'BarSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['bar.comment'], 'BarSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['bar.strings'], 'BarSnippet2', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['bazz', 'bazz'], 'BazzSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
]);
bucket = [];
@@ -57,8 +57,8 @@ suite('Snippets', function () {
test('SnippetFile#select - any scope', function () {
const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [
new Snippet([], 'AnySnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, [], 'AnySnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
new Snippet(false, ['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User, generateUuid()),
]);
const bucket: Snippet[] = [];
@@ -70,7 +70,7 @@ suite('Snippets', function () {
test('Snippet#needsClipboard', function () {
function assertNeedsClipboard(body: string, expected: boolean): void {
const snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User, generateUuid());
const snippet = new Snippet(false, ['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User, generateUuid());
assert.strictEqual(snippet.needsClipboard, expected);
assert.strictEqual(SnippetParser.guessNeedsClipboard(body), expected);
@@ -87,7 +87,7 @@ suite('Snippets', function () {
test('Snippet#isTrivial', function () {
function assertIsTrivial(body: string, expected: boolean): void {
const snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User, generateUuid());
const snippet = new Snippet(false, ['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User, generateUuid());
assert.strictEqual(snippet.isTrivial, expected);
}
@@ -10,7 +10,7 @@ import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/sn
suite('SnippetRewrite', function () {
function assertRewrite(input: string, expected: string | boolean): void {
const actual = new Snippet(['foo'], 'foo', 'foo', 'foo', input, 'foo', SnippetSource.User, generateUuid());
const actual = new Snippet(false, ['foo'], 'foo', 'foo', 'foo', input, 'foo', SnippetSource.User, generateUuid());
if (typeof expected === 'boolean') {
assert.strictEqual(actual.codeSnippet, input);
} else {
@@ -48,7 +48,7 @@ suite('SnippetRewrite', function () {
});
test('lazy bogous variable rewrite', function () {
const snippet = new Snippet(['fooLang'], 'foo', 'prefix', 'desc', 'This is ${bogous} because it is a ${var}', 'source', SnippetSource.Extension, generateUuid());
const snippet = new Snippet(false, ['fooLang'], 'foo', 'prefix', 'desc', 'This is ${bogous} because it is a ${var}', 'source', SnippetSource.Extension, generateUuid());
assert.strictEqual(snippet.body, 'This is ${bogous} because it is a ${var}');
assert.strictEqual(snippet.codeSnippet, 'This is ${1:bogous} because it is a ${2:var}');
assert.strictEqual(snippet.isBogous, true);
@@ -57,6 +57,7 @@ suite('SnippetsService', function () {
extensions: ['.fooLang',]
}));
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'barTest',
'bar',
@@ -66,6 +67,7 @@ suite('SnippetsService', function () {
SnippetSource.User,
generateUuid()
), new Snippet(
false,
['fooLang'],
'bazzTest',
'bazz',
@@ -126,8 +128,8 @@ suite('SnippetsService', function () {
});
test('snippet completions - with different prefixes', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'barTest',
'bar',
@@ -137,6 +139,7 @@ suite('SnippetsService', function () {
SnippetSource.User,
generateUuid()
), new Snippet(
false,
['fooLang'],
'name',
'bar-bar',
@@ -208,6 +211,7 @@ suite('SnippetsService', function () {
test('Cannot use "<?php" as user snippet prefix anymore, #26275', function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'',
'<?php',
@@ -244,6 +248,7 @@ suite('SnippetsService', function () {
test('No user snippets in suggestions, when inside the code, #30508', function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'',
'foo',
@@ -267,6 +272,7 @@ suite('SnippetsService', function () {
test('SnippetSuggest - ensure extension snippets come last ', function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'second',
'second',
@@ -276,6 +282,7 @@ suite('SnippetsService', function () {
SnippetSource.Extension,
generateUuid()
), new Snippet(
false,
['fooLang'],
'first',
'first',
@@ -305,6 +312,7 @@ suite('SnippetsService', function () {
test('Dash in snippets prefix broken #53945', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'p-a',
'p-a',
@@ -330,6 +338,7 @@ suite('SnippetsService', function () {
test('No snippets suggestion on long lines beyond character 100 #58807', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'bug',
'bug',
@@ -350,6 +359,7 @@ suite('SnippetsService', function () {
test('Type colon will trigger snippet #60746', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'bug',
'bug',
@@ -370,6 +380,7 @@ suite('SnippetsService', function () {
test('substring of prefix can\'t trigger snippet #60737', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'mytemplate',
'mytemplate',
@@ -394,6 +405,7 @@ suite('SnippetsService', function () {
test('No snippets suggestion beyond character 100 if not at end of line #60247', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'bug',
'bug',
@@ -419,6 +431,7 @@ suite('SnippetsService', function () {
}));
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'bug',
'-a-bug',
@@ -439,6 +452,7 @@ suite('SnippetsService', function () {
test('No snippets shown when triggering completions at whitespace on line that already has text #62335', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'bug',
'bug',
@@ -459,6 +473,7 @@ suite('SnippetsService', function () {
test('Snippet prefix with special chars and numbers does not work #62906', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'noblockwdelay',
'<<',
@@ -468,6 +483,7 @@ suite('SnippetsService', function () {
SnippetSource.User,
generateUuid()
), new Snippet(
false,
['fooLang'],
'noblockwdelay',
'11',
@@ -499,6 +515,7 @@ suite('SnippetsService', function () {
test('Snippet replace range', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'notWordTest',
'not word',
@@ -542,6 +559,7 @@ suite('SnippetsService', function () {
test('Snippet replace-range incorrect #108894', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'eng',
'eng',
@@ -575,6 +593,7 @@ suite('SnippetsService', function () {
}));
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'PSCustomObject',
'[PSCustomObject]',
@@ -601,6 +620,7 @@ suite('SnippetsService', function () {
test('Leading whitespace in snippet prefix #123860', async function () {
snippetService = new SimpleSnippetService([new Snippet(
false,
['fooLang'],
'cite-name',
' cite',
@@ -627,8 +647,8 @@ suite('SnippetsService', function () {
test('still show suggestions in string when disable string suggestion #136611', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'aaa', 'aaa', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 'bbb', 'bbb', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'aaa', 'aaa', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'bbb', 'bbb', '', 'value', '', SnippetSource.User, generateUuid()),
// new Snippet(['fooLang'], '\'ccc', '\'ccc', '', 'value', '', SnippetSource.User, generateUuid())
]);
@@ -649,9 +669,9 @@ suite('SnippetsService', function () {
test('still show suggestions in string when disable string suggestion #136611', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'aaa', 'aaa', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 'bbb', 'bbb', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], '\'ccc', '\'ccc', '', 'value', '', SnippetSource.User, generateUuid())
new Snippet(false, ['fooLang'], 'aaa', 'aaa', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'bbb', 'bbb', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], '\'ccc', '\'ccc', '', 'value', '', SnippetSource.User, generateUuid())
]);
const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService());
@@ -670,9 +690,9 @@ suite('SnippetsService', function () {
test('Snippet suggestions are too eager #138707 (word)', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'tys', 'tys', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 'hell_or_tell', 'hell_or_tell', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'tys', 'tys', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'hell_or_tell', 'hell_or_tell', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User, generateUuid()),
]);
const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService());
@@ -691,9 +711,9 @@ suite('SnippetsService', function () {
test('Snippet suggestions are too eager #138707 (no word)', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'tys', 'tys', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 't', 't', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'tys', 'tys', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 't', 't', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User, generateUuid()),
]);
const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService());
@@ -712,8 +732,8 @@ suite('SnippetsService', function () {
test('Snippet suggestions are too eager #138707 (word/word)', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'async arrow function', 'async arrow function', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 'foobarrrrrr', 'foobarrrrrr', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'async arrow function', 'async arrow function', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'foobarrrrrr', 'foobarrrrrr', '', 'value', '', SnippetSource.User, generateUuid()),
]);
const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService());
@@ -732,7 +752,7 @@ suite('SnippetsService', function () {
test('Strange and useless autosuggestion #region/#endregion PHP #140039', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'reg', '#region', '', 'value', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'reg', '#region', '', 'value', '', SnippetSource.User, generateUuid()),
]);
@@ -750,9 +770,9 @@ suite('SnippetsService', function () {
test.skip('Snippets disappear with . key #145960', async function () {
snippetService = new SimpleSnippetService([
new Snippet(['fooLang'], 'div', 'div', '', 'div', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 'div.', 'div.', '', 'div.', '', SnippetSource.User, generateUuid()),
new Snippet(['fooLang'], 'div#', 'div#', '', 'div#', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'div', 'div', '', 'div', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'div.', 'div.', '', 'div.', '', SnippetSource.User, generateUuid()),
new Snippet(false, ['fooLang'], 'div#', 'div#', '', 'div#', '', SnippetSource.User, generateUuid()),
]);
const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService());