mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-31 01:00:29 -04:00
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
171 lines
6.4 KiB
TypeScript
171 lines
6.4 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
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 { CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, LanguageId, CompletionItemInsertTextRule, CompletionContext, CompletionTriggerKind } 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/contrib/snippets/browser/snippets.contribution';
|
|
import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
|
|
|
|
export class SnippetCompletion implements CompletionItem {
|
|
|
|
label: string;
|
|
detail: string;
|
|
insertText: string;
|
|
documentation: MarkdownString;
|
|
range: IRange;
|
|
sortText: string;
|
|
kind: CompletionItemKind;
|
|
insertTextRules: CompletionItemInsertTextRule;
|
|
|
|
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.kind = CompletionItemKind.Snippet;
|
|
this.insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;
|
|
}
|
|
|
|
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> | undefined {
|
|
|
|
if (position.column >= SnippetCompletionProvider._maxPrefix) {
|
|
return undefined;
|
|
}
|
|
|
|
if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && context.triggerCharacter === ' ') {
|
|
// no snippets when suggestions have been triggered by space
|
|
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();
|
|
let endsInWhitespace = linePrefixLow.match(/\s$/);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
let availableSnippets = new Set<Snippet>();
|
|
snippets.forEach(availableSnippets.add, availableSnippets);
|
|
suggestions = [];
|
|
for (let start of lineOffsets) {
|
|
availableSnippets.forEach(snippet => {
|
|
if (matches(linePrefixLow, start, snippet.prefixLow, 0)) {
|
|
suggestions.push(new SnippetCompletion(snippet, Range.fromPositions(position.delta(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 SnippetCompletion(snippet, Range.fromPositions(position)));
|
|
});
|
|
}
|
|
|
|
|
|
// 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);
|
|
const languageIdentifier = this._modeService.getLanguageIdentifier(languageId);
|
|
if (languageIdentifier && !this._modeService.getLanguageName(languageIdentifier.language)) {
|
|
languageId = model.getLanguageIdentifier().id;
|
|
}
|
|
return languageId;
|
|
}
|
|
}
|