Merge VS Code 1.31.1 (#4283)

This commit is contained in:
Matt Irvine
2019-03-15 13:09:45 -07:00
committed by GitHub
parent 7d31575149
commit 86bac90001
1716 changed files with 53308 additions and 48375 deletions

View File

@@ -3,21 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore, FuzzyScorer } from 'vs/base/common/filters';
import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore, FuzzyScorer, FuzzyScore } from 'vs/base/common/filters';
import { isDisposable } from 'vs/base/common/lifecycle';
import { CompletionList, CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes';
import { ISuggestionItem, ensureLowerCaseVariants } from './suggest';
import { CompletionItem } from './suggest';
import { InternalSuggestOptions, EDITOR_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
import { CharCode } from 'vs/base/common/charCode';
export interface ICompletionItem extends ISuggestionItem {
matches?: number[];
score?: number;
idx?: number;
distance?: number;
word?: string;
}
type StrictCompletionItem = Required<CompletionItem>;
/* __GDPR__FRAGMENT__
"ICompletionStats" : {
@@ -47,7 +41,7 @@ const enum Refilter {
export class CompletionModel {
private readonly _items: ICompletionItem[];
private readonly _items: CompletionItem[];
private readonly _column: number;
private readonly _wordDistance: WordDistance;
private readonly _options: InternalSuggestOptions;
@@ -55,12 +49,12 @@ export class CompletionModel {
private _lineContext: LineContext;
private _refilterKind: Refilter;
private _filteredItems: ICompletionItem[];
private _filteredItems: StrictCompletionItem[];
private _isIncomplete: Set<CompletionItemProvider>;
private _stats: ICompletionStats;
constructor(
items: ISuggestionItem[],
items: CompletionItem[],
column: number,
lineContext: LineContext,
wordDistance: WordDistance,
@@ -105,7 +99,7 @@ export class CompletionModel {
}
}
get items(): ICompletionItem[] {
get items(): CompletionItem[] {
this._ensureCachedState();
return this._filteredItems;
}
@@ -115,10 +109,10 @@ export class CompletionModel {
return this._isIncomplete;
}
adopt(except: Set<CompletionItemProvider>): ISuggestionItem[] {
let res = new Array<ISuggestionItem>();
adopt(except: Set<CompletionItemProvider>): CompletionItem[] {
let res = new Array<CompletionItem>();
for (let i = 0; i < this._items.length;) {
if (!except.has(this._items[i].support)) {
if (!except.has(this._items[i].provider)) {
res.push(this._items[i]);
// unordered removed
@@ -155,7 +149,7 @@ export class CompletionModel {
// incrementally filter less
const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems;
const target: typeof source = [];
const target: StrictCompletionItem[] = [];
// picks a score function based on the number of
// items that we have to score/filter and based on the
@@ -165,21 +159,17 @@ export class CompletionModel {
for (let i = 0; i < source.length; i++) {
const item = source[i];
const { suggestion, container } = item;
// make sure _labelLow, _filterTextLow, _sortTextLow exist
ensureLowerCaseVariants(suggestion);
// collect those supports that signaled having
// an incomplete result
if (container.incomplete) {
this._isIncomplete.add(item.support);
if (item.container.incomplete) {
this._isIncomplete.add(item.provider);
}
// 'word' is that remainder of the current line that we
// filter and score against. In theory each suggestion uses a
// different word, but in practice not - that's why we cache
const overwriteBefore = item.position.column - suggestion.range.startColumn;
const overwriteBefore = item.position.column - item.completion.range.startColumn;
const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
if (word.length !== wordLen) {
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
@@ -196,8 +186,7 @@ export class CompletionModel {
// the fallback-sort using the initial sort order.
// use a score of `-100` because that is out of the
// bound of values `fuzzyScore` will return
item.score = -100;
item.matches = undefined;
item.score = FuzzyScore.Default;
} else {
// skip word characters that are whitespace until
@@ -215,40 +204,37 @@ export class CompletionModel {
if (wordPos >= wordLen) {
// the wordPos at which scoring starts is the whole word
// and therefore the same rules as not having a word apply
item.score = -100;
item.matches = [];
item.score = FuzzyScore.Default;
} else if (typeof suggestion.filterText === 'string') {
} else if (typeof item.completion.filterText === 'string') {
// when there is a `filterText` it must match the `word`.
// if it matches we check with the label to compute highlights
// and if that doesn't yield a result we have no highlights,
// despite having the match
let match = scoreFn(word, wordLow, wordPos, suggestion.filterText, suggestion._filterTextLow, 0, false);
let match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, false);
if (!match) {
continue;
continue; // NO match
}
item.score = match[0];
item.matches = (fuzzyScore(word, wordLow, 0, suggestion.label, suggestion._labelLow, 0, true) || anyScore(word, suggestion.label))[1];
item.score = anyScore(word, wordLow, 0, item.completion.label, item.labelLow, 0);
item.score[0] = match[0]; // use score from filterText
} else {
// by default match `word` against the `label`
let match = scoreFn(word, wordLow, wordPos, suggestion.label, suggestion._labelLow, 0, false);
if (match) {
item.score = match[0];
item.matches = match[1];
} else {
continue;
let match = scoreFn(word, wordLow, wordPos, item.completion.label, item.labelLow, 0, false);
if (!match) {
continue; // NO match
}
item.score = match;
}
}
item.idx = i;
item.distance = this._wordDistance.distance(item.position, suggestion);
target.push(item);
item.distance = this._wordDistance.distance(item.position, item.completion);
target.push(item as StrictCompletionItem);
// update stats
this._stats.suggestionCount++;
switch (suggestion.kind) {
switch (item.completion.kind) {
case CompletionItemKind.Snippet: this._stats.snippetCount++; break;
case CompletionItemKind.Text: this._stats.textCount++; break;
}
@@ -258,10 +244,10 @@ export class CompletionModel {
this._refilterKind = Refilter.Nothing;
}
private static _compareCompletionItems(a: ICompletionItem, b: ICompletionItem): number {
if (a.score > b.score) {
private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number {
if (a.score[0] > b.score[0]) {
return -1;
} else if (a.score < b.score) {
} else if (a.score[0] < b.score[0]) {
return 1;
} else if (a.distance < b.distance) {
return -1;
@@ -276,22 +262,22 @@ export class CompletionModel {
}
}
private static _compareCompletionItemsSnippetsDown(a: ICompletionItem, b: ICompletionItem): number {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
private static _compareCompletionItemsSnippetsDown(a: StrictCompletionItem, b: StrictCompletionItem): number {
if (a.completion.kind !== b.completion.kind) {
if (a.completion.kind === CompletionItemKind.Snippet) {
return 1;
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
} else if (b.completion.kind === CompletionItemKind.Snippet) {
return -1;
}
}
return CompletionModel._compareCompletionItems(a, b);
}
private static _compareCompletionItemsSnippetsUp(a: ICompletionItem, b: ICompletionItem): number {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
private static _compareCompletionItemsSnippetsUp(a: StrictCompletionItem, b: StrictCompletionItem): number {
if (a.completion.kind !== b.completion.kind) {
if (a.completion.kind === CompletionItemKind.Snippet) {
return -1;
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
} else if (b.completion.kind === CompletionItemKind.Snippet) {
return 1;
}
}

View File

@@ -56,7 +56,14 @@
height: 100%;
}
.monaco-editor .suggest-widget .monaco-list {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: -moz-none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
/** Styles for each row in the list element **/
@@ -68,6 +75,8 @@
background-repeat: no-repeat;
background-position: 2px 2px;
white-space: nowrap;
cursor: pointer;
touch-action: none;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents {
@@ -173,7 +182,7 @@
background-image: url('Misc_16x.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-size: 75%;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method::before,

View File

@@ -10,12 +10,13 @@ import { onUnexpectedExternalError, canceled, isPromiseCanceledError } from 'vs/
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions';
import { CompletionList, CompletionItemProvider, CompletionItem, CompletionProviderRegistry, CompletionContext, CompletionTriggerKind, CompletionItemKind } from 'vs/editor/common/modes';
import * as modes from 'vs/editor/common/modes';
import { Position, IPosition } from 'vs/editor/common/core/position';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Range } from 'vs/editor/common/core/range';
import { FuzzyScore } from 'vs/base/common/filters';
export const Context = {
Visible: new RawContextKey<boolean>('suggestWidgetVisible', false),
@@ -24,23 +25,77 @@ export const Context = {
AcceptSuggestionsOnEnter: new RawContextKey<boolean>('acceptSuggestionOnEnter', true)
};
export interface ISuggestionItem {
position: IPosition;
suggestion: CompletionItem;
container: CompletionList;
support: CompletionItemProvider;
resolve(token: CancellationToken): Thenable<void>;
export class CompletionItem {
_brand: 'ISuggestionItem';
readonly resolve: (token: CancellationToken) => Promise<void>;
// perf
readonly labelLow: string;
readonly sortTextLow?: string;
readonly filterTextLow?: string;
// sorting, filtering
score: FuzzyScore = FuzzyScore.Default;
distance: number = 0;
idx?: number;
word?: string;
constructor(
readonly position: IPosition,
readonly completion: modes.CompletionItem,
readonly container: modes.CompletionList,
readonly provider: modes.CompletionItemProvider,
model: ITextModel
) {
// ensure lower-variants (perf)
this.labelLow = completion.label.toLowerCase();
this.sortTextLow = completion.sortText && completion.sortText.toLowerCase();
this.filterTextLow = completion.filterText && completion.filterText.toLowerCase();
// create the suggestion resolver
const { resolveCompletionItem } = provider;
if (typeof resolveCompletionItem !== 'function') {
this.resolve = () => Promise.resolve();
} else {
let cached: Promise<void> | undefined;
this.resolve = (token) => {
if (!cached) {
let isDone = false;
cached = Promise.resolve(resolveCompletionItem.call(provider, model, position, completion, token)).then(value => {
assign(completion, value);
isDone = true;
}, err => {
if (isPromiseCanceledError(err)) {
// the IPC queue will reject the request with the
// cancellation error -> reset cached
cached = undefined;
}
});
token.onCancellationRequested(() => {
if (!isDone) {
// cancellation after the request has been
// dispatched -> reset cache
cached = undefined;
}
});
}
return cached;
};
}
}
}
export type SnippetConfig = 'top' | 'bottom' | 'inline' | 'none';
let _snippetSuggestSupport: CompletionItemProvider;
let _snippetSuggestSupport: modes.CompletionItemProvider;
export function getSnippetSuggestSupport(): CompletionItemProvider {
export function getSnippetSuggestSupport(): modes.CompletionItemProvider {
return _snippetSuggestSupport;
}
export function setSnippetSuggestSupport(support: CompletionItemProvider): CompletionItemProvider {
export function setSnippetSuggestSupport(support: modes.CompletionItemProvider): modes.CompletionItemProvider {
const old = _snippetSuggestSupport;
_snippetSuggestSupport = support;
return old;
@@ -50,12 +105,12 @@ export function provideSuggestionItems(
model: ITextModel,
position: Position,
snippetConfig: SnippetConfig = 'bottom',
onlyFrom?: CompletionItemProvider[],
context?: CompletionContext,
onlyFrom?: modes.CompletionItemProvider[],
context?: modes.CompletionContext,
token: CancellationToken = CancellationToken.None
): Promise<ISuggestionItem[]> {
): Promise<CompletionItem[]> {
const allSuggestions: ISuggestionItem[] = [];
const allSuggestions: CompletionItem[] = [];
const acceptSuggestion = createSuggesionFilter(snippetConfig);
const wordUntil = model.getWordUntilPosition(position);
@@ -64,27 +119,27 @@ export function provideSuggestionItems(
position = position.clone();
// get provider groups, always add snippet suggestion provider
const supports = CompletionProviderRegistry.orderedGroups(model);
const supports = modes.CompletionProviderRegistry.orderedGroups(model);
// add snippets provider unless turned off
if (snippetConfig !== 'none' && _snippetSuggestSupport) {
supports.unshift([_snippetSuggestSupport]);
}
const suggestConext = context || { triggerKind: CompletionTriggerKind.Invoke };
const suggestConext = context || { triggerKind: modes.CompletionTriggerKind.Invoke };
// add suggestions from contributed providers - providers are ordered in groups of
// equal score and once a group produces a result the process stops
let hasResult = false;
const factory = supports.map(supports => () => {
// for each support in the group ask for suggestions
return Promise.all(supports.map(support => {
return Promise.all(supports.map(provider => {
if (isNonEmptyArray(onlyFrom) && onlyFrom.indexOf(support) < 0) {
if (isNonEmptyArray(onlyFrom) && onlyFrom.indexOf(provider) < 0) {
return undefined;
}
return Promise.resolve(support.provideCompletionItems(model, position, suggestConext, token)).then(container => {
return Promise.resolve(provider.provideCompletionItems(model, position, suggestConext, token)).then(container => {
const len = allSuggestions.length;
@@ -97,21 +152,12 @@ export function provideSuggestionItems(
suggestion.range = defaultRange;
}
// fill in lower-case text
ensureLowerCaseVariants(suggestion);
allSuggestions.push({
position,
container,
suggestion,
support,
resolve: createSuggestionResolver(support, suggestion, model, position)
});
allSuggestions.push(new CompletionItem(position, suggestion, container, provider, model));
}
}
}
if (len !== allSuggestions.length && support !== _snippetSuggestSupport) {
if (len !== allSuggestions.length && provider !== _snippetSuggestSupport) {
hasResult = true;
}
@@ -139,101 +185,55 @@ export function provideSuggestionItems(
return result;
}
export function ensureLowerCaseVariants(suggestion: CompletionItem) {
if (!suggestion._labelLow) {
suggestion._labelLow = suggestion.label.toLowerCase();
}
if (suggestion.sortText && !suggestion._sortTextLow) {
suggestion._sortTextLow = suggestion.sortText.toLowerCase();
}
if (suggestion.filterText && !suggestion._filterTextLow) {
suggestion._filterTextLow = suggestion.filterText.toLowerCase();
}
}
function createSuggestionResolver(provider: CompletionItemProvider, suggestion: CompletionItem, model: ITextModel, position: Position): (token: CancellationToken) => Promise<void> {
const { resolveCompletionItem } = provider;
if (typeof resolveCompletionItem !== 'function') {
return () => Promise.resolve();
}
let cached: Promise<void> | undefined;
return (token) => {
if (!cached) {
let isDone = false;
cached = Promise.resolve(provider.resolveCompletionItem!(model, position, suggestion, token)).then(value => {
assign(suggestion, value);
isDone = true;
}, err => {
if (isPromiseCanceledError(err)) {
// the IPC queue will reject the request with the
// cancellation error -> reset cached
cached = undefined;
}
});
token.onCancellationRequested(() => {
if (!isDone) {
// cancellation after the request has been
// dispatched -> reset cache
cached = undefined;
}
});
}
return cached;
};
}
function createSuggesionFilter(snippetConfig: SnippetConfig): (candidate: CompletionItem) => boolean {
function createSuggesionFilter(snippetConfig: SnippetConfig): (candidate: modes.CompletionItem) => boolean {
if (snippetConfig === 'none') {
return suggestion => suggestion.kind !== CompletionItemKind.Snippet;
return suggestion => suggestion.kind !== modes.CompletionItemKind.Snippet;
} else {
return () => true;
}
}
function defaultComparator(a: ISuggestionItem, b: ISuggestionItem): number {
function defaultComparator(a: CompletionItem, b: CompletionItem): number {
// check with 'sortText'
if (a.suggestion._sortTextLow && b.suggestion._sortTextLow) {
if (a.suggestion._sortTextLow < b.suggestion._sortTextLow) {
if (a.sortTextLow && b.sortTextLow) {
if (a.sortTextLow < b.sortTextLow) {
return -1;
} else if (a.suggestion._sortTextLow > b.suggestion._sortTextLow) {
} else if (a.sortTextLow > b.sortTextLow) {
return 1;
}
}
// check with 'label'
if (a.suggestion.label < b.suggestion.label) {
if (a.completion.label < b.completion.label) {
return -1;
} else if (a.suggestion.label > b.suggestion.label) {
} else if (a.completion.label > b.completion.label) {
return 1;
}
// check with 'type'
return a.suggestion.kind - b.suggestion.kind;
return a.completion.kind - b.completion.kind;
}
function snippetUpComparator(a: ISuggestionItem, b: ISuggestionItem): number {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
function snippetUpComparator(a: CompletionItem, b: CompletionItem): number {
if (a.completion.kind !== b.completion.kind) {
if (a.completion.kind === modes.CompletionItemKind.Snippet) {
return -1;
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
} else if (b.completion.kind === modes.CompletionItemKind.Snippet) {
return 1;
}
}
return defaultComparator(a, b);
}
function snippetDownComparator(a: ISuggestionItem, b: ISuggestionItem): number {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
function snippetDownComparator(a: CompletionItem, b: CompletionItem): number {
if (a.completion.kind !== b.completion.kind) {
if (a.completion.kind === modes.CompletionItemKind.Snippet) {
return 1;
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
} else if (b.completion.kind === modes.CompletionItemKind.Snippet) {
return -1;
}
}
return defaultComparator(a, b);
}
export function getSuggestionComparator(snippetConfig: SnippetConfig): (a: ISuggestionItem, b: ISuggestionItem) => number {
export function getSuggestionComparator(snippetConfig: SnippetConfig): (a: CompletionItem, b: CompletionItem) => number {
if (snippetConfig === 'top') {
return snippetUpComparator;
} else if (snippetConfig === 'bottom') {
@@ -245,12 +245,12 @@ export function getSuggestionComparator(snippetConfig: SnippetConfig): (a: ISugg
registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, position, args) => {
const result: CompletionList = {
const result: modes.CompletionList = {
incomplete: false,
suggestions: []
};
let resolving: Thenable<any>[] = [];
let resolving: Promise<any>[] = [];
let maxItemsToResolve = args['maxItemsToResolve'] || 0;
return provideSuggestionItems(model, position).then(items => {
@@ -259,7 +259,7 @@ registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, positio
resolving.push(item.resolve(CancellationToken.None));
}
result.incomplete = result.incomplete || item.container.incomplete;
result.suggestions.push(item.suggestion);
result.suggestions.push(item.completion);
}
}).then(() => {
return Promise.all(resolving);
@@ -269,15 +269,15 @@ registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, positio
});
interface SuggestController extends IEditorContribution {
triggerSuggest(onlyFrom?: CompletionItemProvider[]): void;
triggerSuggest(onlyFrom?: modes.CompletionItemProvider[]): void;
}
let _provider = new class implements CompletionItemProvider {
let _provider = new class implements modes.CompletionItemProvider {
onlyOnceSuggestions: CompletionItem[] = [];
onlyOnceSuggestions: modes.CompletionItem[] = [];
provideCompletionItems(): CompletionList {
provideCompletionItems(): modes.CompletionList {
let suggestions = this.onlyOnceSuggestions.slice(0);
let result = { suggestions };
this.onlyOnceSuggestions.length = 0;
@@ -285,9 +285,9 @@ let _provider = new class implements CompletionItemProvider {
}
};
CompletionProviderRegistry.register('*', _provider);
modes.CompletionProviderRegistry.register('*', _provider);
export function showSimpleSuggestions(editor: ICodeEditor, suggestions: CompletionItem[]) {
export function showSimpleSuggestions(editor: ICodeEditor, suggestions: modes.CompletionItem[]) {
setTimeout(() => {
_provider.onlyOnceSuggestions.push(...suggestions);
editor.getContribution<SuggestController>('editor.contrib.suggestController').triggerSuggest([_provider]);

View File

@@ -16,8 +16,8 @@ export class SuggestAlternatives {
private readonly _ckOtherSuggestions: IContextKey<boolean>;
private _index: number;
private _model: CompletionModel;
private _acceptNext: (selected: ISelectedSuggestion) => any;
private _model: CompletionModel | undefined;
private _acceptNext: ((selected: ISelectedSuggestion) => any) | undefined;
private _listener: IDisposable;
private _ignore: boolean;
@@ -73,7 +73,7 @@ export class SuggestAlternatives {
if (newIndex === index) {
break;
}
if (!model.items[newIndex].suggestion.additionalTextEdits) {
if (!model.items[newIndex].completion.additionalTextEdits) {
break;
}
}
@@ -96,7 +96,7 @@ export class SuggestAlternatives {
try {
this._ignore = true;
this._index = SuggestAlternatives._moveIndex(fwd, this._model, this._index);
this._acceptNext({ index: this._index, item: this._model.items[this._index], model: this._model });
this._acceptNext!({ index: this._index, item: this._model.items[this._index], model: this._model });
} finally {
this._ignore = false;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { alert } from 'vs/base/browser/ui/aria/aria';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { onUnexpectedError } from 'vs/base/common/errors';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
@@ -12,33 +12,36 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution, ScrollType, Handler } from 'vs/editor/common/editorCommon';
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { CompletionItemProvider, CompletionItemInsertTextRule } from 'vs/editor/common/modes';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { SuggestMemories } from 'vs/editor/contrib/suggest/suggestMemory';
import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory';
import * as nls from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ICompletionItem } from './completionModel';
import { Context as SuggestContext, ISuggestionItem } from './suggest';
import { Context as SuggestContext, CompletionItem } from './suggest';
import { SuggestAlternatives } from './suggestAlternatives';
import { State, SuggestModel } from './suggestModel';
import { ISelectedSuggestion, SuggestWidget } from './suggestWidget';
import { WordContextKey } from 'vs/editor/contrib/suggest/wordContextKey';
import { once, anyEvent } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IdleValue } from 'vs/base/common/async';
import { CharacterSet } from 'vs/editor/common/core/characterClassifier';
import { isObject } from 'vs/base/common/types';
class AcceptOnCharacterOracle {
private _disposables: IDisposable[] = [];
private _activeAcceptCharacters = new Set<string>();
private _activeItem: ISelectedSuggestion;
private _active?: {
readonly acceptCharacters: CharacterSet;
readonly item: ISelectedSuggestion;
};
constructor(editor: ICodeEditor, widget: SuggestWidget, accept: (selected: ISelectedSuggestion) => any) {
@@ -47,31 +50,32 @@ class AcceptOnCharacterOracle {
this._disposables.push(widget.onDidHide(this.reset, this));
this._disposables.push(editor.onWillType(text => {
if (this._activeItem) {
const ch = text[text.length - 1];
if (this._activeAcceptCharacters.has(ch) && editor.getConfiguration().contribInfo.acceptSuggestionOnCommitCharacter) {
accept(this._activeItem);
if (this._active) {
const ch = text.charCodeAt(text.length - 1);
if (this._active.acceptCharacters.has(ch) && editor.getConfiguration().contribInfo.acceptSuggestionOnCommitCharacter) {
accept(this._active.item);
}
}
}));
}
private _onItem(selected: ISelectedSuggestion): void {
if (!selected || isFalsyOrEmpty(selected.item.suggestion.commitCharacters)) {
private _onItem(selected: ISelectedSuggestion | undefined): void {
if (!selected || !isNonEmptyArray(selected.item.completion.commitCharacters)) {
this.reset();
return;
}
this._activeItem = selected;
this._activeAcceptCharacters.clear();
for (const ch of selected.item.suggestion.commitCharacters) {
const acceptCharacters = new CharacterSet();
for (const ch of selected.item.completion.commitCharacters) {
if (ch.length > 0) {
this._activeAcceptCharacters.add(ch[0]);
acceptCharacters.add(ch.charCodeAt(0));
}
}
this._active = { acceptCharacters, item: selected };
}
reset(): void {
this._activeItem = undefined;
this._active = undefined;
}
dispose() {
@@ -87,9 +91,8 @@ export class SuggestController implements IEditorContribution {
return editor.getContribution<SuggestController>(SuggestController.ID);
}
private _model: SuggestModel;
private _widget: SuggestWidget;
private readonly _memory: IdleValue<SuggestMemories>;
private readonly _model: SuggestModel;
private readonly _widget: IdleValue<SuggestWidget>;
private readonly _alternatives: IdleValue<SuggestAlternatives>;
private _toDispose: IDisposable[] = [];
@@ -98,15 +101,62 @@ export class SuggestController implements IEditorContribution {
constructor(
private _editor: ICodeEditor,
@IEditorWorkerService editorWorker: IEditorWorkerService,
@ISuggestMemoryService private readonly _memoryService: ISuggestMemoryService,
@ICommandService private readonly _commandService: ICommandService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
this._model = new SuggestModel(this._editor, editorWorker);
this._memory = new IdleValue(() => {
let res = _instantiationService.createInstance(SuggestMemories, this._editor);
this._toDispose.push(res);
return res;
this._widget = new IdleValue(() => {
const widget = this._instantiationService.createInstance(SuggestWidget, this._editor);
this._toDispose.push(widget);
this._toDispose.push(widget.onDidSelect(item => this._onDidSelectItem(item, false, true), this));
// Wire up logic to accept a suggestion on certain characters
const autoAcceptOracle = new AcceptOnCharacterOracle(this._editor, widget, item => this._onDidSelectItem(item, false, true));
this._toDispose.push(
autoAcceptOracle,
this._model.onDidSuggest(e => {
if (e.completionModel.items.length === 0) {
autoAcceptOracle.reset();
}
})
);
// Wire up makes text edit context key
let makesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService);
this._toDispose.push(widget.onDidFocus(({ item }) => {
const position = this._editor.getPosition()!;
const startColumn = item.completion.range.startColumn;
const endColumn = position.column;
let value = true;
if (
this._editor.getConfiguration().contribInfo.acceptSuggestionOnEnter === 'smart'
&& this._model.state === State.Auto
&& !item.completion.command
&& !item.completion.additionalTextEdits
&& !(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)
&& endColumn - startColumn === item.completion.insertText.length
) {
const oldText = this._editor.getModel()!.getValueInRange({
startLineNumber: position.lineNumber,
startColumn,
endLineNumber: position.lineNumber,
endColumn
});
value = oldText !== item.completion.insertText;
}
makesTextEdit.set(value);
}));
this._toDispose.push({
dispose() { makesTextEdit.reset(); }
});
return widget;
});
this._alternatives = new IdleValue(() => {
@@ -118,20 +168,17 @@ export class SuggestController implements IEditorContribution {
this._toDispose.push(_instantiationService.createInstance(WordContextKey, _editor));
this._toDispose.push(this._model.onDidTrigger(e => {
if (!this._widget) {
this._createSuggestWidget();
}
this._widget.showTriggered(e.auto, e.shy ? 250 : 50);
this._widget.getValue().showTriggered(e.auto, e.shy ? 250 : 50);
}));
this._toDispose.push(this._model.onDidSuggest(e => {
if (!e.shy) {
let index = this._memory.getValue().select(this._editor.getModel(), this._editor.getPosition(), e.completionModel.items);
this._widget.showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
let index = this._memoryService.select(this._editor.getModel()!, this._editor.getPosition()!, e.completionModel.items);
this._widget.getValue().showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
}
}));
this._toDispose.push(this._model.onDidCancel(e => {
if (this._widget && !e.retrigger) {
this._widget.hideWidget();
this._widget.getValue().hideWidget();
}
}));
this._toDispose.push(this._editor.onDidBlurEditorText(() => {
@@ -150,51 +197,6 @@ export class SuggestController implements IEditorContribution {
updateFromConfig();
}
private _createSuggestWidget(): void {
this._widget = this._instantiationService.createInstance(SuggestWidget, this._editor);
this._toDispose.push(this._widget.onDidSelect(item => this._onDidSelectItem(item, false, true), this));
// Wire up logic to accept a suggestion on certain characters
const autoAcceptOracle = new AcceptOnCharacterOracle(this._editor, this._widget, item => this._onDidSelectItem(item, false, true));
this._toDispose.push(
autoAcceptOracle,
this._model.onDidSuggest(e => {
if (e.completionModel.items.length === 0) {
autoAcceptOracle.reset();
}
})
);
let makesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService);
this._toDispose.push(this._widget.onDidFocus(({ item }) => {
const position = this._editor.getPosition();
const startColumn = item.suggestion.range.startColumn;
const endColumn = position.column;
let value = true;
if (
this._editor.getConfiguration().contribInfo.acceptSuggestionOnEnter === 'smart'
&& this._model.state === State.Auto
&& !item.suggestion.command
&& !item.suggestion.additionalTextEdits
&& !(item.suggestion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet)
&& endColumn - startColumn === item.suggestion.insertText.length
) {
const oldText = this._editor.getModel().getValueInRange({
startLineNumber: position.lineNumber,
startColumn,
endLineNumber: position.lineNumber,
endColumn
});
value = oldText !== item.suggestion.insertText;
}
makesTextEdit.set(value);
}));
this._toDispose.push({
dispose() { makesTextEdit.reset(); }
});
}
getId(): string {
return SuggestController.ID;
@@ -202,26 +204,25 @@ export class SuggestController implements IEditorContribution {
dispose(): void {
this._toDispose = dispose(this._toDispose);
if (this._widget) {
this._widget.dispose();
this._widget = null;
}
this._widget.dispose();
if (this._model) {
this._model.dispose();
this._model = null;
}
}
protected _onDidSelectItem(event: ISelectedSuggestion, keepAlternativeSuggestions: boolean, undoStops: boolean): void {
protected _onDidSelectItem(event: ISelectedSuggestion | undefined, keepAlternativeSuggestions: boolean, undoStops: boolean): void {
if (!event || !event.item) {
this._alternatives.getValue().reset();
this._model.cancel();
return;
}
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const modelVersionNow = model.getAlternativeVersionId();
const { suggestion, position } = event.item;
const { completion: suggestion, position } = event.item;
const editorColumn = this._editor.getPosition().column;
const columnDelta = editorColumn - position.column;
@@ -236,10 +237,10 @@ export class SuggestController implements IEditorContribution {
}
// keep item in memory
this._memory.getValue().memorize(model, this._editor.getPosition(), event.item);
this._memoryService.memorize(model, this._editor.getPosition(), event.item);
let { insertText } = suggestion;
if (!(suggestion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet)) {
if (!(suggestion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) {
insertText = SnippetParser.escape(insertText);
}
@@ -251,7 +252,7 @@ export class SuggestController implements IEditorContribution {
overwriteBefore + columnDelta,
overwriteAfter,
false, false,
!(suggestion.insertTextRules & CompletionItemInsertTextRule.KeepWhitespace)
!(suggestion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace)
);
if (undoStops) {
@@ -264,11 +265,11 @@ export class SuggestController implements IEditorContribution {
} else if (suggestion.command.id === TriggerSuggestAction.id) {
// retigger
this._model.trigger({ auto: true }, true);
this._model.trigger({ auto: true, shy: false }, true);
} else {
// exec command, done
this._commandService.executeCommand(suggestion.command.id, ...suggestion.command.arguments).then(undefined, onUnexpectedError);
this._commandService.executeCommand(suggestion.command.id, ...(suggestion.command.arguments ? [...suggestion.command.arguments] : [])).catch(onUnexpectedError);
this._model.cancel();
}
@@ -290,54 +291,59 @@ export class SuggestController implements IEditorContribution {
this._alertCompletionItem(event.item);
}
private _alertCompletionItem({ suggestion }: ICompletionItem): void {
private _alertCompletionItem({ completion: suggestion }: CompletionItem): void {
let msg = nls.localize('arai.alert.snippet', "Accepting '{0}' did insert the following text: {1}", suggestion.label, suggestion.insertText);
alert(msg);
}
triggerSuggest(onlyFrom?: CompletionItemProvider[]): void {
this._model.trigger({ auto: false }, false, onlyFrom);
this._editor.revealLine(this._editor.getPosition().lineNumber, ScrollType.Smooth);
this._editor.focus();
if (this._editor.hasModel()) {
this._model.trigger({ auto: false, shy: false }, false, onlyFrom);
this._editor.revealLine(this._editor.getPosition().lineNumber, ScrollType.Smooth);
this._editor.focus();
}
}
triggerSuggestAndAcceptBest(defaultTypeText: string): void {
triggerSuggestAndAcceptBest(arg: { fallback: string }): void {
if (!this._editor.hasModel()) {
return;
}
const positionNow = this._editor.getPosition();
const fallback = () => {
if (positionNow.equals(this._editor.getPosition())) {
this._editor.trigger('suggest', Handler.Type, { text: defaultTypeText });
if (positionNow.equals(this._editor.getPosition()!)) {
this._commandService.executeCommand(arg.fallback);
}
};
const makesTextEdit = (item: ISuggestionItem): boolean => {
if (item.suggestion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet || item.suggestion.additionalTextEdits) {
const makesTextEdit = (item: CompletionItem): boolean => {
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet || item.completion.additionalTextEdits) {
// snippet, other editor -> makes edit
return true;
}
const position = this._editor.getPosition();
const startColumn = item.suggestion.range.startColumn;
const position = this._editor.getPosition()!;
const startColumn = item.completion.range.startColumn;
const endColumn = position.column;
if (endColumn - startColumn !== item.suggestion.insertText.length) {
if (endColumn - startColumn !== item.completion.insertText.length) {
// unequal lengths -> makes edit
return true;
}
const textNow = this._editor.getModel().getValueInRange({
const textNow = this._editor.getModel()!.getValueInRange({
startLineNumber: position.lineNumber,
startColumn,
endLineNumber: position.lineNumber,
endColumn
});
// unequal text -> makes edit
return textNow !== item.suggestion.insertText;
return textNow !== item.completion.insertText;
};
once(this._model.onDidTrigger)(_ => {
Event.once(this._model.onDidTrigger)(_ => {
// wait for trigger because only then the cancel-event is trustworthy
let listener: IDisposable[] = [];
anyEvent<any>(this._model.onDidTrigger, this._model.onDidCancel)(() => {
Event.any<any>(this._model.onDidTrigger, this._model.onDidCancel)(() => {
// retrigger or cancel -> try to type default text
dispose(listener);
fallback();
@@ -349,7 +355,7 @@ export class SuggestController implements IEditorContribution {
fallback();
return;
}
const index = this._memory.getValue().select(this._editor.getModel(), this._editor.getPosition(), completionModel.items);
const index = this._memoryService.select(this._editor.getModel()!, this._editor.getPosition()!, completionModel.items);
const item = completionModel.items[index];
if (!makesTextEdit(item)) {
fallback();
@@ -368,8 +374,8 @@ export class SuggestController implements IEditorContribution {
acceptSelectedSuggestion(keepAlternativeSuggestions?: boolean): void {
if (this._widget) {
const item = this._widget.getFocusedItem();
this._onDidSelectItem(item, keepAlternativeSuggestions, true);
const item = this._widget.getValue().getFocusedItem();
this._onDidSelectItem(item, !!keepAlternativeSuggestions, true);
}
}
@@ -384,55 +390,55 @@ export class SuggestController implements IEditorContribution {
cancelSuggestWidget(): void {
if (this._widget) {
this._model.cancel();
this._widget.hideWidget();
this._widget.getValue().hideWidget();
}
}
selectNextSuggestion(): void {
if (this._widget) {
this._widget.selectNext();
this._widget.getValue().selectNext();
}
}
selectNextPageSuggestion(): void {
if (this._widget) {
this._widget.selectNextPage();
this._widget.getValue().selectNextPage();
}
}
selectLastSuggestion(): void {
if (this._widget) {
this._widget.selectLast();
this._widget.getValue().selectLast();
}
}
selectPrevSuggestion(): void {
if (this._widget) {
this._widget.selectPrevious();
this._widget.getValue().selectPrevious();
}
}
selectPrevPageSuggestion(): void {
if (this._widget) {
this._widget.selectPreviousPage();
this._widget.getValue().selectPreviousPage();
}
}
selectFirstSuggestion(): void {
if (this._widget) {
this._widget.selectFirst();
this._widget.getValue().selectFirst();
}
}
toggleSuggestionDetails(): void {
if (this._widget) {
this._widget.toggleDetails();
this._widget.getValue().toggleDetails();
}
}
toggleSuggestionFocus(): void {
if (this._widget) {
this._widget.toggleDetailsFocus();
this._widget.getValue().toggleDetailsFocus();
}
}
}
@@ -606,7 +612,10 @@ registerEditorCommand(new SuggestCommand({
SuggestAlternatives.OtherSuggestions.toNegated(),
SnippetController2.InSnippetMode.toNegated()
),
handler: x => x.triggerSuggestAndAcceptBest('\t'),//todo@joh fallback/default configurable?
handler: (x, arg) => {
x.triggerSuggestAndAcceptBest(isObject(arg) ? { fallback: 'tab', ...arg } : { fallback: 'tab' });
},
kbOpts: {
weight,
primary: KeyCode.Tab

View File

@@ -3,25 +3,28 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICompletionItem } from 'vs/editor/contrib/suggest/completionModel';
import { LRUCache, TernarySearchTree } from 'vs/base/common/map';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ITextModel } from 'vs/editor/common/model';
import { IPosition } from 'vs/editor/common/core/position';
import { CompletionItemKind, completionKindFromLegacyString } from 'vs/editor/common/modes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Disposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
export abstract class Memory {
select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number {
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
if (items.length === 0) {
return 0;
}
let topScore = items[0].score;
for (let i = 1; i < items.length; i++) {
const { score, suggestion } = items[i];
const { score, completion: suggestion } = items[i];
if (score !== topScore) {
// stop when leaving the group of top matches
break;
@@ -34,16 +37,16 @@ export abstract class Memory {
return 0;
}
abstract memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void;
abstract memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
abstract toJSON(): object;
abstract toJSON(): object | undefined;
abstract fromJSON(data: object): void;
}
export class NoMemory extends Memory {
memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void {
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
// no-op
}
@@ -64,20 +67,20 @@ export interface MemItem {
export class LRUMemory extends Memory {
private _cache = new LRUCache<string, MemItem>(300, .66);
private _cache = new LRUCache<string, MemItem>(300, 0.66);
private _seq = 0;
memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void {
const { label } = item.suggestion;
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
const { label } = item.completion;
const key = `${model.getLanguageIdentifier().language}/${label}`;
this._cache.set(key, {
touch: this._seq++,
type: item.suggestion.kind,
insertText: item.suggestion.insertText
type: item.completion.kind,
insertText: item.completion.insertText
});
}
select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number {
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
// in order of completions, select the first
// that has been used in the past
let { word } = model.getWordUntilPosition(pos);
@@ -93,7 +96,7 @@ export class LRUMemory extends Memory {
let res = -1;
let seq = -1;
for (let i = 0; i < items.length; i++) {
const { suggestion } = items[i];
const { completion: suggestion } = items[i];
const key = `${model.getLanguageIdentifier().language}/${suggestion.label}`;
const item = this._cache.get(key);
if (item && item.touch > seq && item.type === suggestion.kind && item.insertText === suggestion.insertText) {
@@ -134,17 +137,17 @@ export class PrefixMemory extends Memory {
private _trie = TernarySearchTree.forStrings<MemItem>();
private _seq = 0;
memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void {
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
const { word } = model.getWordUntilPosition(pos);
const key = `${model.getLanguageIdentifier().language}/${word}`;
this._trie.set(key, {
type: item.suggestion.kind,
insertText: item.suggestion.insertText,
type: item.completion.kind,
insertText: item.completion.insertText,
touch: this._seq++
});
}
select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number {
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
let { word } = model.getWordUntilPosition(pos);
if (!word) {
return super.select(model, pos, items);
@@ -156,7 +159,7 @@ export class PrefixMemory extends Memory {
}
if (item) {
for (let i = 0; i < items.length; i++) {
let { kind, insertText } = items[i].suggestion;
let { kind, insertText } = items[i].completion;
if (kind === item.type && insertText === item.insertText) {
return i;
}
@@ -194,35 +197,60 @@ export class PrefixMemory extends Memory {
export type MemMode = 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix';
export class SuggestMemories extends Disposable {
export class SuggestMemoryService extends Disposable implements ISuggestMemoryService {
readonly _serviceBrand: any;
private readonly _storagePrefix = 'suggest/memories';
private _mode: MemMode;
private _strategy: Memory;
private readonly _persistSoon: RunOnceScheduler;
private _mode: MemMode;
private _shareMem: boolean;
private _strategy: Memory;
constructor(
editor: ICodeEditor,
@IStorageService private readonly _storageService: IStorageService,
@IConfigurationService private readonly _configService: IConfigurationService,
) {
super();
this._persistSoon = this._register(new RunOnceScheduler(() => this._saveState(editor.getConfiguration().contribInfo.suggest.shareSuggestSelections), 3000));
this._setMode(editor.getConfiguration().contribInfo.suggestSelection, editor.getConfiguration().contribInfo.suggest.shareSuggestSelections);
this._register(editor.onDidChangeConfiguration(e => e.contribInfo && this._setMode(editor.getConfiguration().contribInfo.suggestSelection, editor.getConfiguration().contribInfo.suggest.shareSuggestSelections)));
this._register(_storageService.onWillSaveState(() => this._saveState(editor.getConfiguration().contribInfo.suggest.shareSuggestSelections)));
const update = () => {
const mode = this._configService.getValue<MemMode>('editor.suggestSelection');
const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');
this._update(mode, share, false);
};
this._persistSoon = this._register(new RunOnceScheduler(() => this._saveState(), 500));
this._register(_storageService.onWillSaveState(() => this._saveState()));
this._register(this._configService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.suggestSelection') || e.affectsConfiguration('editor.suggest.shareSuggestSelections')) {
update();
}
}));
this._register(this._storageService.onDidChangeStorage(e => {
if (e.scope === StorageScope.GLOBAL && e.key.indexOf(this._storagePrefix) === 0) {
if (!document.hasFocus()) {
// windows that aren't focused have to drop their current
// storage value and accept what's stored now
this._update(this._mode, this._shareMem, true);
}
}
}));
update();
}
private _setMode(mode: MemMode, useGlobalStorageForSuggestions: boolean): void {
if (this._mode === mode) {
private _update(mode: MemMode, shareMem: boolean, force: boolean): void {
if (!force && this._mode === mode && this._shareMem === shareMem) {
return;
}
this._shareMem = shareMem;
this._mode = mode;
this._strategy = mode === 'recentlyUsedByPrefix' ? new PrefixMemory() : mode === 'recentlyUsed' ? new LRUMemory() : new NoMemory();
try {
const raw = useGlobalStorageForSuggestions ? this._storageService.get(`${this._storagePrefix}/${this._mode}`, StorageScope.GLOBAL) : this._storageService.get(`${this._storagePrefix}/${this._mode}`, StorageScope.WORKSPACE);
const scope = shareMem ? StorageScope.GLOBAL : StorageScope.WORKSPACE;
const raw = this._storageService.get(`${this._storagePrefix}/${this._mode}`, scope);
if (raw) {
this._strategy.fromJSON(JSON.parse(raw));
}
@@ -231,17 +259,29 @@ export class SuggestMemories extends Disposable {
}
}
memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void {
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
this._strategy.memorize(model, pos, item);
this._persistSoon.schedule();
}
select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number {
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
return this._strategy.select(model, pos, items);
}
private _saveState(useGlobalStorageForSuggestions: boolean) {
private _saveState() {
const raw = JSON.stringify(this._strategy);
this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, useGlobalStorageForSuggestions ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
const scope = this._shareMem ? StorageScope.GLOBAL : StorageScope.WORKSPACE;
this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, scope);
}
}
export const ISuggestMemoryService = createDecorator<ISuggestMemoryService>('ISuggestMemories');
export interface ISuggestMemoryService {
_serviceBrand: any;
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number;
}
registerSingleton(ISuggestMemoryService, SuggestMemoryService, true);

View File

@@ -16,7 +16,7 @@ import { Selection } from 'vs/editor/common/core/selection';
import { ITextModel, IWordAtPosition } from 'vs/editor/common/model';
import { CompletionItemProvider, StandardTokenType, CompletionContext, CompletionProviderRegistry, CompletionTriggerKind } from 'vs/editor/common/modes';
import { CompletionModel } from './completionModel';
import { ISuggestionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport } from './suggest';
import { CompletionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport } from './suggest';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
@@ -40,17 +40,17 @@ export interface ISuggestEvent {
export interface SuggestTriggerContext {
readonly auto: boolean;
readonly shy?: boolean;
readonly shy: boolean;
readonly triggerCharacter?: string;
}
export class LineContext {
static shouldAutoTrigger(editor: ICodeEditor): boolean {
const model = editor.getModel();
if (!model) {
if (!editor.hasModel()) {
return false;
}
const model = editor.getModel();
const pos = editor.getPosition();
model.tokenizeIfCheap(pos.lineNumber);
@@ -99,14 +99,14 @@ export class SuggestModel implements IDisposable {
private readonly _triggerRefilter = new TimeoutTimer();
private _state: State = State.Idle;
private _requestToken: CancellationTokenSource;
private _context: LineContext;
private _requestToken?: CancellationTokenSource;
private _context?: LineContext;
private _currentSelection: Selection;
private _completionModel: CompletionModel;
private readonly _onDidCancel: Emitter<ICancelEvent> = new Emitter<ICancelEvent>();
private readonly _onDidTrigger: Emitter<ITriggerEvent> = new Emitter<ITriggerEvent>();
private readonly _onDidSuggest: Emitter<ISuggestEvent> = new Emitter<ISuggestEvent>();
private _completionModel?: CompletionModel;
private readonly _onDidCancel = new Emitter<ICancelEvent>();
private readonly _onDidTrigger = new Emitter<ITriggerEvent>();
private readonly _onDidSuggest = new Emitter<ISuggestEvent>();
readonly onDidCancel: Event<ICancelEvent> = this._onDidCancel.event;
readonly onDidTrigger: Event<ITriggerEvent> = this._onDidTrigger.event;
@@ -116,8 +116,6 @@ export class SuggestModel implements IDisposable {
private readonly _editor: ICodeEditor,
private readonly _editorWorker: IEditorWorkerService
) {
this._completionModel = null;
this._context = null;
this._currentSelection = this._editor.getSelection() || new Selection(1, 1, 1, 1);
// wire up various listeners
@@ -185,7 +183,7 @@ export class SuggestModel implements IDisposable {
dispose(this._triggerCharacterListener);
if (this._editor.getConfiguration().readOnly
|| !this._editor.getModel()
|| !this._editor.hasModel()
|| !this._editor.getConfiguration().contribInfo.suggestOnTriggerCharacters) {
return;
@@ -210,8 +208,8 @@ export class SuggestModel implements IDisposable {
if (supports) {
// keep existing items that where not computed by the
// supports/providers that want to trigger now
const items: ISuggestionItem[] = this._completionModel ? this._completionModel.adopt(supports) : undefined;
this.trigger({ auto: true, triggerCharacter: lastChar }, Boolean(this._completionModel), values(supports), items);
const items: CompletionItem[] | undefined = this._completionModel ? this._completionModel.adopt(supports) : undefined;
this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), values(supports), items);
}
});
}
@@ -223,37 +221,38 @@ export class SuggestModel implements IDisposable {
}
cancel(retrigger: boolean = false): void {
this._triggerRefilter.cancel();
if (this._triggerQuickSuggest) {
if (this._state !== State.Idle) {
this._triggerRefilter.cancel();
this._triggerQuickSuggest.cancel();
if (this._requestToken) {
this._requestToken.cancel();
this._requestToken = undefined;
}
this._state = State.Idle;
dispose(this._completionModel);
this._completionModel = undefined;
this._context = undefined;
this._onDidCancel.fire({ retrigger });
}
if (this._requestToken) {
this._requestToken.cancel();
}
this._state = State.Idle;
dispose(this._completionModel);
this._completionModel = null;
this._context = null;
this._onDidCancel.fire({ retrigger });
}
private _updateActiveSuggestSession(): void {
if (this._state !== State.Idle) {
if (!CompletionProviderRegistry.has(this._editor.getModel())) {
if (!this._editor.hasModel() || !CompletionProviderRegistry.has(this._editor.getModel())) {
this.cancel();
} else {
this.trigger({ auto: this._state === State.Auto }, true);
this.trigger({ auto: this._state === State.Auto, shy: false }, true);
}
}
}
private _onCursorChange(e: ICursorSelectionChangedEvent): void {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const prevSelection = this._currentSelection;
this._currentSelection = this._editor.getSelection();
@@ -269,12 +268,7 @@ export class SuggestModel implements IDisposable {
return;
}
if (!CompletionProviderRegistry.has(this._editor.getModel())) {
return;
}
const model = this._editor.getModel();
if (!model) {
if (!CompletionProviderRegistry.has(model)) {
return;
}
@@ -301,12 +295,11 @@ export class SuggestModel implements IDisposable {
if (!LineContext.shouldAutoTrigger(this._editor)) {
return;
}
const model = this._editor.getModel();
const pos = this._editor.getPosition();
if (!model) {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const pos = this._editor.getPosition();
// validate enabled now
const { quickSuggestions } = this._editor.getConfiguration().contribInfo;
if (quickSuggestions === false) {
@@ -328,7 +321,7 @@ export class SuggestModel implements IDisposable {
}
// we made it till here -> trigger now
this.trigger({ auto: true });
this.trigger({ auto: true, shy: false });
}, this._quickSuggestDelay);
@@ -339,24 +332,27 @@ export class SuggestModel implements IDisposable {
if (this._state === State.Idle) {
return;
}
const model = this._editor.getModel();
if (model) {
// refine active suggestion
this._triggerRefilter.cancelAndSet(() => {
const position = this._editor.getPosition();
const ctx = new LineContext(model, position, this._state === State.Auto, false);
this._onNewContext(ctx);
}, 25);
}
}
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: CompletionItemProvider[], existingItems?: ISuggestionItem[]): void {
const model = this._editor.getModel();
if (!model) {
if (!this._editor.hasModel()) {
return;
}
// refine active suggestion
this._triggerRefilter.cancelAndSet(() => {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const position = this._editor.getPosition();
const ctx = new LineContext(model, position, this._state === State.Auto, false);
this._onNewContext(ctx);
}, 25);
}
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: CompletionItemProvider[], existingItems?: CompletionItem[]): void {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const auto = context.auto;
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
@@ -383,8 +379,6 @@ export class SuggestModel implements IDisposable {
this._requestToken = new CancellationTokenSource();
// TODO: Remove this workaround - https://github.com/Microsoft/vscode/issues/61917
// let wordDistance = Promise.resolve().then(() => WordDistance.create(this._editorWorker, this._editor));
let wordDistance = WordDistance.create(this._editorWorker, this._editor);
let items = provideSuggestionItems(
@@ -398,16 +392,18 @@ export class SuggestModel implements IDisposable {
Promise.all([items, wordDistance]).then(([items, wordDistance]) => {
this._requestToken.dispose();
dispose(this._requestToken);
if (this._state === State.Idle) {
return;
}
const model = this._editor.getModel();
if (!model) {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
if (isNonEmptyArray(existingItems)) {
const cmpFn = getSuggestionComparator(this._editor.getConfiguration().contribInfo.suggest.snippets);
items = items.concat(existingItems).sort(cmpFn);
@@ -415,9 +411,9 @@ export class SuggestModel implements IDisposable {
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
dispose(this._completionModel);
this._completionModel = new CompletionModel(items, this._context.column, {
this._completionModel = new CompletionModel(items, this._context!.column, {
leadingLineContent: ctx.leadingLineContent,
characterCountDelta: this._context ? ctx.column - this._context.column : 0
characterCountDelta: ctx.column - this._context!.column
},
wordDistance,
this._editor.getConfiguration().contribInfo.suggest
@@ -449,7 +445,7 @@ export class SuggestModel implements IDisposable {
if (ctx.column < this._context.column) {
// typed -> moved cursor LEFT -> retrigger if still on a word
if (ctx.leadingWord.word) {
this.trigger({ auto: this._context.auto }, true);
this.trigger({ auto: this._context.auto, shy: false }, true);
} else {
this.cancel();
}
@@ -465,7 +461,7 @@ export class SuggestModel implements IDisposable {
// typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger
const { incomplete } = this._completionModel;
const adopted = this._completionModel.adopt(incomplete);
this.trigger({ auto: this._state === State.Auto }, true, values(incomplete), adopted);
this.trigger({ auto: this._state === State.Auto, shy: false }, true, values(incomplete), adopted);
} else {
// typed -> moved cursor RIGHT -> update UI
@@ -481,7 +477,7 @@ export class SuggestModel implements IDisposable {
if (LineContext.shouldAutoTrigger(this._editor) && this._context.leadingWord.endColumn < ctx.leadingWord.startColumn) {
// retrigger when heading into a new word
this.trigger({ auto: this._context.auto }, true);
this.trigger({ auto: this._context.auto, shy: false }, true);
return;
}

View File

@@ -7,19 +7,19 @@ import 'vs/css!./media/suggest';
import * as nls from 'vs/nls';
import { createMatches } from 'vs/base/common/filters';
import * as strings from 'vs/base/common/strings';
import { Event, Emitter, chain } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom';
import { IListVirtualDelegate, IListEvent, IListRenderer } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IListEvent, IListRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { Context as SuggestContext } from './suggest';
import { ICompletionItem, CompletionModel } from './completionModel';
import { Context as SuggestContext, CompletionItem } from './suggest';
import { CompletionModel } from './completionModel';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { attachListStyler } from 'vs/platform/theme/common/styler';
@@ -63,22 +63,22 @@ export const editorSuggestWidgetHighlightForeground = registerColor('editorSugge
const colorRegExp = /^(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))$/i;
function matchesColor(text: string) {
function matchesColor(text: string): string | null {
return text && text.match(colorRegExp) ? text : null;
}
function canExpandCompletionItem(item: ICompletionItem) {
function canExpandCompletionItem(item: CompletionItem | null) {
if (!item) {
return false;
}
const suggestion = item.suggestion;
const suggestion = item.completion;
if (suggestion.documentation) {
return true;
}
return (suggestion.detail && suggestion.detail !== suggestion.label);
}
class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData> {
class Renderer implements IListRenderer<CompletionItem, ISuggestionTemplateData> {
constructor(
private widget: SuggestWidget,
@@ -136,16 +136,16 @@ class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData
configureFont();
chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
Event.chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
.filter(e => e.fontInfo || e.contribInfo)
.on(configureFont, null, data.disposables);
return data;
}
renderElement(element: ICompletionItem, _index: number, templateData: ISuggestionTemplateData): void {
renderElement(element: CompletionItem, _index: number, templateData: ISuggestionTemplateData): void {
const data = <ISuggestionTemplateData>templateData;
const suggestion = (<ICompletionItem>element).suggestion;
const suggestion = (<CompletionItem>element).completion;
data.icon.className = 'icon ' + completionKindToCssClass(suggestion.kind);
data.colorspan.style.backgroundColor = '';
@@ -153,11 +153,11 @@ class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData
const labelOptions: IIconLabelValueOptions = {
labelEscapeNewLines: true,
matches: createMatches(element.matches)
matches: createMatches(element.score)
};
let color: string;
if (suggestion.kind === CompletionItemKind.Color && (color = matchesColor(suggestion.label) || typeof suggestion.documentation === 'string' && matchesColor(suggestion.documentation))) {
let color: string | null = null;
if (suggestion.kind === CompletionItemKind.Color && ((color = matchesColor(suggestion.label) || typeof suggestion.documentation === 'string' ? matchesColor(suggestion.documentation as any) : null))) {
// special logic for 'color' completion items
data.icon.className = 'icon customcolor';
data.colorspan.style.backgroundColor = color;
@@ -165,7 +165,7 @@ class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData
} else if (suggestion.kind === CompletionItemKind.File && this._themeService.getIconTheme().hasFileIcons) {
// special logic for 'file' completion items
data.icon.className = 'icon hide';
labelOptions.extraClasses = [].concat(
labelOptions.extraClasses = ([] as string[]).concat(
getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FILE),
getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FILE)
);
@@ -173,7 +173,7 @@ class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData
} else if (suggestion.kind === CompletionItemKind.Folder && this._themeService.getIconTheme().hasFolderIcons) {
// special logic for 'folder' completion items
data.icon.className = 'icon hide';
labelOptions.extraClasses = [].concat(
labelOptions.extraClasses = ([] as string[]).concat(
getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FOLDER),
getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FOLDER)
);
@@ -185,7 +185,7 @@ class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData
];
}
data.iconLabel.setValue(suggestion.label, undefined, labelOptions);
data.iconLabel.setLabel(suggestion.label, undefined, labelOptions);
data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, '');
if (canExpandCompletionItem(element)) {
@@ -206,10 +206,6 @@ class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData
}
}
disposeElement(): void {
// noop
}
disposeTemplate(templateData: ISuggestionTemplateData): void {
// {{SQL CARBON EDIT}}
if (templateData.iconLabel) {
@@ -240,7 +236,7 @@ class SuggestionDetails {
private header: HTMLElement;
private type: HTMLElement;
private docs: HTMLElement;
private ariaLabel: string;
private ariaLabel: string | null;
private disposables: IDisposable[];
private renderDisposeable: IDisposable;
private borderWidth: number = 1;
@@ -273,7 +269,7 @@ class SuggestionDetails {
this.configureFont();
chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
Event.chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
.filter(e => e.fontInfo)
.on(this.configureFont, this, this.disposables);
@@ -284,7 +280,7 @@ class SuggestionDetails {
return this.el;
}
render(item: ICompletionItem): void {
render(item: CompletionItem): void {
this.renderDisposeable = dispose(this.renderDisposeable);
if (!item || !canExpandCompletionItem(item)) {
@@ -295,19 +291,19 @@ class SuggestionDetails {
return;
}
removeClass(this.el, 'no-docs');
if (typeof item.suggestion.documentation === 'string') {
if (typeof item.completion.documentation === 'string') {
removeClass(this.docs, 'markdown-docs');
this.docs.textContent = item.suggestion.documentation;
this.docs.textContent = item.completion.documentation;
} else {
addClass(this.docs, 'markdown-docs');
this.docs.innerHTML = '';
const renderedContents = this.markdownRenderer.render(item.suggestion.documentation);
const renderedContents = this.markdownRenderer.render(item.completion.documentation);
this.renderDisposeable = renderedContents;
this.docs.appendChild(renderedContents.element);
}
if (item.suggestion.detail) {
this.type.innerText = item.suggestion.detail;
if (item.completion.detail) {
this.type.innerText = item.completion.detail;
show(this.type);
} else {
this.type.innerText = '';
@@ -331,11 +327,11 @@ class SuggestionDetails {
this.ariaLabel = strings.format(
'{0}{1}',
item.suggestion.detail || '',
item.suggestion.documentation ? (typeof item.suggestion.documentation === 'string' ? item.suggestion.documentation : item.suggestion.documentation.value) : '');
item.completion.detail || '',
item.completion.documentation ? (typeof item.completion.documentation === 'string' ? item.completion.documentation : item.completion.documentation.value) : '');
}
getAriaLabel(): string {
getAriaLabel() {
return this.ariaLabel;
}
@@ -390,12 +386,12 @@ class SuggestionDetails {
}
export interface ISelectedSuggestion {
item: ICompletionItem;
item: CompletionItem;
index: number;
model: CompletionModel;
}
export class SuggestWidget implements IContentWidget, IListVirtualDelegate<ICompletionItem>, IDisposable {
export class SuggestWidget implements IContentWidget, IListVirtualDelegate<CompletionItem>, IDisposable {
private static readonly ID: string = 'editor.widget.suggestWidget';
@@ -405,19 +401,19 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
// Editor.IContentWidget.allowEditorOverflow
readonly allowEditorOverflow = true;
private state: State;
private state: State | null;
private isAuto: boolean;
private loadingTimeout: any;
private currentSuggestionDetails: CancelablePromise<void>;
private focusedItem: ICompletionItem;
private currentSuggestionDetails: CancelablePromise<void> | null;
private focusedItem: CompletionItem | null;
private ignoreFocusEvents = false;
private completionModel: CompletionModel;
private completionModel: CompletionModel | null;
private element: HTMLElement;
private messageElement: HTMLElement;
private listElement: HTMLElement;
private details: SuggestionDetails;
private list: List<ICompletionItem>;
private list: List<CompletionItem>;
private listHeight: number;
private suggestWidgetVisible: IContextKey<boolean>;
@@ -447,11 +443,11 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
private firstFocusInCurrentList: boolean = false;
private preferDocPositionTop: boolean = false;
private docsPositionPreviousWidgetY: number;
private docsPositionPreviousWidgetY: number | null;
constructor(
private editor: ICodeEditor,
@ITelemetryService private telemetryService: ITelemetryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@@ -481,9 +477,8 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
this.list = new List(this.listElement, this, [renderer], {
useShadows: false,
selectOnMouseDown: true,
focusOnMouseDown: false,
openController: { shouldOpen: () => false }
openController: { shouldOpen: () => false },
mouseSupport: false
});
this.toDispose = [
@@ -493,6 +488,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
}),
themeService.onThemeChange(t => this.onThemeChange(t)),
editor.onDidLayoutChange(() => this.onEditorLayoutChange()),
this.list.onMouseDown(e => this.onListMouseDown(e)),
this.list.onSelectionChange(e => this.onListSelection(e)),
this.list.onFocusChange(e => this.onListFocus(e)),
this.editor.onDidChangeCursorSelection(() => this.onCursorSelectionChanged())
@@ -521,37 +517,53 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
}
}
private onListSelection(e: IListEvent<ICompletionItem>): void {
private onListMouseDown(e: IListMouseEvent<CompletionItem>): void {
if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {
return;
}
this.select(e.element, e.index);
}
private onListSelection(e: IListEvent<CompletionItem>): void {
if (!e.elements.length) {
return;
}
const item = e.elements[0];
const index = e.indexes[0];
this.select(e.elements[0], e.indexes[0]);
}
private select(item: CompletionItem, index: number): void {
const completionModel = this.completionModel;
if (!completionModel) {
return;
}
item.resolve(CancellationToken.None).then(() => {
this.onDidSelectEmitter.fire({ item, index, model: this.completionModel });
alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.suggestion.label));
this.onDidSelectEmitter.fire({ item, index, model: completionModel });
alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.completion.label));
this.editor.focus();
});
}
private _getSuggestionAriaAlertLabel(item: ICompletionItem): string {
const isSnippet = item.suggestion.kind === CompletionItemKind.Snippet;
private _getSuggestionAriaAlertLabel(item: CompletionItem): string {
const isSnippet = item.completion.kind === CompletionItemKind.Snippet;
if (!canExpandCompletionItem(item)) {
return isSnippet ? nls.localize('ariaCurrentSnippetSuggestion', "{0}, snippet suggestion", item.suggestion.label)
: nls.localize('ariaCurrentSuggestion', "{0}, suggestion", item.suggestion.label);
return isSnippet ? nls.localize('ariaCurrentSnippetSuggestion', "{0}, snippet suggestion", item.completion.label)
: nls.localize('ariaCurrentSuggestion', "{0}, suggestion", item.completion.label);
} else if (this.expandDocsSettingFromStorage()) {
return isSnippet ? nls.localize('ariaCurrentSnippeSuggestionReadDetails', "{0}, snippet suggestion. Reading details. {1}", item.suggestion.label, this.details.getAriaLabel())
: nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, suggestion. Reading details. {1}", item.suggestion.label, this.details.getAriaLabel());
return isSnippet ? nls.localize('ariaCurrentSnippeSuggestionReadDetails', "{0}, snippet suggestion. Reading details. {1}", item.completion.label, this.details.getAriaLabel())
: nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, suggestion. Reading details. {1}", item.completion.label, this.details.getAriaLabel());
} else {
return isSnippet ? nls.localize('ariaCurrentSnippetSuggestionWithDetails', "{0}, snippet suggestion, has details", item.suggestion.label)
: nls.localize('ariaCurrentSuggestionWithDetails', "{0}, suggestion, has details", item.suggestion.label);
return isSnippet ? nls.localize('ariaCurrentSnippetSuggestionWithDetails', "{0}, snippet suggestion, has details", item.completion.label)
: nls.localize('ariaCurrentSuggestionWithDetails', "{0}, suggestion, has details", item.completion.label);
}
}
private _lastAriaAlertLabel: string;
private _ariaAlert(newAriaAlertLabel: string): void {
private _lastAriaAlertLabel: string | null;
private _ariaAlert(newAriaAlertLabel: string | null): void {
if (this._lastAriaAlertLabel === newAriaAlertLabel) {
return;
}
@@ -582,7 +594,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
this.details.setBorderWidth(theme.type === 'hc' ? 2 : 1);
}
private onListFocus(e: IListEvent<ICompletionItem>): void {
private onListFocus(e: IListEvent<CompletionItem>): void {
if (this.ignoreFocusEvents) {
return;
}
@@ -598,6 +610,10 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
return;
}
if (!this.completionModel) {
return;
}
const item = e.elements[0];
const index = e.indexes[0];
@@ -872,10 +888,12 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
}
}
getFocusedItem(): ISelectedSuggestion {
getFocusedItem(): ISelectedSuggestion | undefined {
if (this.state !== State.Hidden
&& this.state !== State.Empty
&& this.state !== State.Loading) {
&& this.state !== State.Loading
&& this.completionModel
) {
return {
item: this.list.getFocusedElements()[0],
@@ -944,7 +962,6 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
*/
this.telemetryService.publicLog('suggestWidget:expandDetails', this.editor.getTelemetryData());
}
}
showDetails(): void {
@@ -992,7 +1009,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
this.onDidHideEmitter.fire(this);
}
getPosition(): IContentWidgetPosition {
getPosition(): IContentWidgetPosition | null {
if (this.state === State.Hidden) {
return null;
}
@@ -1036,6 +1053,10 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
* Adds the propert classes, margins when positioning the docs to the side
*/
private adjustDocsPosition() {
if (!this.editor.hasModel()) {
return;
}
const lineHeight = this.editor.getConfiguration().fontInfo.lineHeight;
const cursorCoords = this.editor.getScrolledVisiblePosition(this.editor.getPosition());
const editorCoords = getDomNodePagePosition(this.editor.getDomNode());
@@ -1086,7 +1107,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
return;
}
let matches = this.element.style.maxWidth.match(/(\d+)px/);
let matches = this.element.style.maxWidth!.match(/(\d+)px/);
if (!matches || Number(matches[1]) < this.maxWidgetWidth) {
addClass(this.element, 'docs-below');
removeClass(this.element, 'docs-side');
@@ -1109,11 +1130,11 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
// IDelegate
getHeight(element: ICompletionItem): number {
getHeight(element: CompletionItem): number {
return this.unfocusedHeight;
}
getTemplateId(element: ICompletionItem): string {
getTemplateId(element: CompletionItem): string {
return 'suggestion';
}
@@ -1129,13 +1150,13 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<IComp
this.state = null;
this.currentSuggestionDetails = null;
this.focusedItem = null;
this.element = null;
this.messageElement = null;
this.listElement = null;
this.element = null!; // StrictNullOverride: nulling out ok in dispose
this.messageElement = null!; // StrictNullOverride: nulling out ok in dispose
this.listElement = null!; // StrictNullOverride: nulling out ok in dispose
this.details.dispose();
this.details = null;
this.details = null!; // StrictNullOverride: nulling out ok in dispose
this.list.dispose();
this.list = null;
this.list = null!; // StrictNullOverride: nulling out ok in dispose
this.toDispose = dispose(this.toDispose);
if (this.loadingTimeout) {
clearTimeout(this.loadingTimeout);

View File

@@ -4,39 +4,31 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IPosition } from 'vs/editor/common/core/position';
import { CompletionList, CompletionItemProvider, CompletionItem, CompletionItemKind } from 'vs/editor/common/modes';
import * as modes from 'vs/editor/common/modes';
import { CompletionModel } from 'vs/editor/contrib/suggest/completionModel';
import { ISuggestionItem, getSuggestionComparator, ensureLowerCaseVariants } from 'vs/editor/contrib/suggest/suggest';
import { CompletionItem, getSuggestionComparator } from 'vs/editor/contrib/suggest/suggest';
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
export function createSuggestItem(label: string, overwriteBefore: number, kind = CompletionItemKind.Property, incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }): ISuggestionItem {
return new class implements ISuggestionItem {
position = position;
suggestion: CompletionItem = {
label,
range: { startLineNumber: position.lineNumber, startColumn: position.column - overwriteBefore, endLineNumber: position.lineNumber, endColumn: position.column },
insertText: label,
kind
};
container: CompletionList = {
incomplete,
suggestions: [this.suggestion]
};
support: CompletionItemProvider = {
provideCompletionItems(): any {
return;
}
};
resolve(): Promise<void> {
return null;
export function createSuggestItem(label: string, overwriteBefore: number, kind = modes.CompletionItemKind.Property, incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }, sortText?: string, filterText?: string): CompletionItem {
const suggestion: modes.CompletionItem = {
label,
sortText,
filterText,
range: { startLineNumber: position.lineNumber, startColumn: position.column - overwriteBefore, endLineNumber: position.lineNumber, endColumn: position.column },
insertText: label,
kind
};
const container: modes.CompletionList = {
incomplete,
suggestions: [suggestion]
};
const provider: modes.CompletionItemProvider = {
provideCompletionItems(): any {
return;
}
};
return new CompletionItem(position, suggestion, container, provider, undefined!);
}
suite('CompletionModel', function () {
@@ -100,7 +92,7 @@ suite('CompletionModel', function () {
const complete = model.adopt(incomplete);
assert.equal(incomplete.size, 1);
assert.ok(incomplete.has(incompleteItem.support));
assert.ok(incomplete.has(incompleteItem.provider));
assert.equal(complete.length, 1);
assert.ok(complete[0] === completeItem);
});
@@ -130,7 +122,7 @@ suite('CompletionModel', function () {
const complete = model.adopt(incomplete);
assert.equal(incomplete.size, 1);
assert.ok(incomplete.has(incompleteItem1.support));
assert.ok(incomplete.has(incompleteItem1.provider));
assert.equal(complete.length, 5);
});
@@ -150,18 +142,18 @@ suite('CompletionModel', function () {
assert.equal(model.items.length, 4);
const [a, b, c, d] = model.items;
assert.equal(a.suggestion.label, ' </div');
assert.equal(b.suggestion.label, ' </tag');
assert.equal(c.suggestion.label, 'a');
assert.equal(d.suggestion.label, 'p');
assert.equal(a.completion.label, ' </div');
assert.equal(b.completion.label, ' </tag');
assert.equal(c.completion.label, 'a');
assert.equal(d.completion.label, 'p');
});
test('keep snippet sorting with prefix: top, #25495', function () {
model = new CompletionModel([
createSuggestItem('Snippet1', 1, CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, CompletionItemKind.Snippet),
createSuggestItem('semver', 1, CompletionItemKind.Property),
createSuggestItem('Snippet1', 1, modes.CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, modes.CompletionItemKind.Snippet),
createSuggestItem('semver', 1, modes.CompletionItemKind.Property),
], 1, {
leadingLineContent: 's',
characterCountDelta: 0
@@ -169,8 +161,8 @@ suite('CompletionModel', function () {
assert.equal(model.items.length, 2);
const [a, b] = model.items;
assert.equal(a.suggestion.label, 'Snippet1');
assert.equal(b.suggestion.label, 'semver');
assert.equal(a.completion.label, 'Snippet1');
assert.equal(b.completion.label, 'semver');
assert.ok(a.score < b.score); // snippet really promoted
});
@@ -178,9 +170,9 @@ suite('CompletionModel', function () {
test('keep snippet sorting with prefix: bottom, #25495', function () {
model = new CompletionModel([
createSuggestItem('snippet1', 1, CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, CompletionItemKind.Snippet),
createSuggestItem('Semver', 1, CompletionItemKind.Property),
createSuggestItem('snippet1', 1, modes.CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, modes.CompletionItemKind.Snippet),
createSuggestItem('Semver', 1, modes.CompletionItemKind.Property),
], 1, {
leadingLineContent: 's',
characterCountDelta: 0
@@ -188,16 +180,16 @@ suite('CompletionModel', function () {
assert.equal(model.items.length, 2);
const [a, b] = model.items;
assert.equal(a.suggestion.label, 'Semver');
assert.equal(b.suggestion.label, 'snippet1');
assert.equal(a.completion.label, 'Semver');
assert.equal(b.completion.label, 'snippet1');
assert.ok(a.score < b.score); // snippet really demoted
});
test('keep snippet sorting with prefix: inline, #25495', function () {
model = new CompletionModel([
createSuggestItem('snippet1', 1, CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, CompletionItemKind.Snippet),
createSuggestItem('snippet1', 1, modes.CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, modes.CompletionItemKind.Snippet),
createSuggestItem('Semver', 1),
], 1, {
leadingLineContent: 's',
@@ -206,15 +198,14 @@ suite('CompletionModel', function () {
assert.equal(model.items.length, 2);
const [a, b] = model.items;
assert.equal(a.suggestion.label, 'snippet1');
assert.equal(b.suggestion.label, 'Semver');
assert.equal(a.completion.label, 'snippet1');
assert.equal(b.completion.label, 'Semver');
assert.ok(a.score > b.score); // snippet really demoted
});
test('filterText seems ignored in autocompletion, #26874', function () {
const item1 = createSuggestItem('Map - java.util', 1);
item1.suggestion.filterText = 'Map';
const item1 = createSuggestItem('Map - java.util', 1, undefined, undefined, undefined, undefined, 'Map');
const item2 = createSuggestItem('Map - java.util', 1);
model = new CompletionModel([item1, item2], 1, {
@@ -233,17 +224,8 @@ suite('CompletionModel', function () {
test('Vscode 1.12 no longer obeys \'sortText\' in completion items (from language server), #26096', function () {
const item1 = createSuggestItem('<- groups', 2, CompletionItemKind.Property, false, { lineNumber: 1, column: 3 });
item1.suggestion.filterText = ' groups';
item1.suggestion.sortText = '00002';
const item2 = createSuggestItem('source', 0, CompletionItemKind.Property, false, { lineNumber: 1, column: 3 });
item2.suggestion.filterText = 'source';
item2.suggestion.sortText = '00001';
ensureLowerCaseVariants(item1.suggestion);
ensureLowerCaseVariants(item2.suggestion);
const item1 = createSuggestItem('<- groups', 2, modes.CompletionItemKind.Property, false, { lineNumber: 1, column: 3 }, '00002', ' groups');
const item2 = createSuggestItem('source', 0, modes.CompletionItemKind.Property, false, { lineNumber: 1, column: 3 }, '00001', 'source');
const items = [item1, item2].sort(getSuggestionComparator('inline'));
model = new CompletionModel(items, 3, {
@@ -254,8 +236,8 @@ suite('CompletionModel', function () {
assert.equal(model.items.length, 2);
const [first, second] = model.items;
assert.equal(first.suggestion.label, 'source');
assert.equal(second.suggestion.label, '<- groups');
assert.equal(first.completion.label, 'source');
assert.equal(second.completion.label, '<- groups');
});
test('Score only filtered items when typing more, score all when typing less', function () {
@@ -302,9 +284,9 @@ suite('CompletionModel', function () {
assert.equal(model.items.length, 3);
const [first, second, third] = model.items;
assert.equal(first.suggestion.label, 'result'); // best with `rult`
assert.equal(second.suggestion.label, 'replyToUser'); // best with `rltu`
assert.equal(third.suggestion.label, 'randomLolut'); // best with `rlut`
assert.equal(first.completion.label, 'result'); // best with `rult`
assert.equal(second.completion.label, 'replyToUser'); // best with `rltu`
assert.equal(third.completion.label, 'randomLolut'); // best with `rlut`
});
test('Emmet suggestion not appearing at the top of the list in jsx files, #39518', function () {
@@ -322,8 +304,8 @@ suite('CompletionModel', function () {
model.lineContext = { leadingLineContent: 'form', characterCountDelta: 4 };
assert.equal(model.items.length, 5);
const [first, second, third] = model.items;
assert.equal(first.suggestion.label, 'form'); // best with `form`
assert.equal(second.suggestion.label, 'form:get'); // best with `form`
assert.equal(third.suggestion.label, 'from'); // best with `from`
assert.equal(first.completion.label, 'form'); // best with `form`
assert.equal(second.completion.label, 'form:get'); // best with `form`
assert.equal(third.completion.label, 'from'); // best with `from`
});
});

View File

@@ -9,6 +9,7 @@ import { CompletionProviderRegistry, CompletionItemKind } from 'vs/editor/common
import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest';
import { Position } from 'vs/editor/common/core/position';
import { TextModel } from 'vs/editor/common/model/textModel';
import { Range } from 'vs/editor/common/core/range';
suite('Suggest', function () {
@@ -20,21 +21,24 @@ suite('Suggest', function () {
model = TextModel.createFromString('FOO\nbar\BAR\nfoo', undefined, undefined, URI.parse('foo:bar/path'));
registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, {
provideCompletionItems() {
provideCompletionItems(_doc, pos) {
return {
incomplete: false,
suggestions: [{
label: 'aaa',
kind: CompletionItemKind.Snippet,
insertText: 'aaa'
insertText: 'aaa',
range: Range.fromPositions(pos)
}, {
label: 'zzz',
kind: CompletionItemKind.Snippet,
insertText: 'zzz'
insertText: 'zzz',
range: Range.fromPositions(pos)
}, {
label: 'fff',
kind: CompletionItemKind.Property,
insertText: 'fff'
insertText: 'fff',
range: Range.fromPositions(pos)
}]
};
}
@@ -49,31 +53,31 @@ suite('Suggest', function () {
test('sort - snippet inline', async function () {
const items = await provideSuggestionItems(model, new Position(1, 1), 'inline');
assert.equal(items.length, 3);
assert.equal(items[0].suggestion.label, 'aaa');
assert.equal(items[1].suggestion.label, 'fff');
assert.equal(items[2].suggestion.label, 'zzz');
assert.equal(items[0].completion.label, 'aaa');
assert.equal(items[1].completion.label, 'fff');
assert.equal(items[2].completion.label, 'zzz');
});
test('sort - snippet top', async function () {
const items = await provideSuggestionItems(model, new Position(1, 1), 'top');
assert.equal(items.length, 3);
assert.equal(items[0].suggestion.label, 'aaa');
assert.equal(items[1].suggestion.label, 'zzz');
assert.equal(items[2].suggestion.label, 'fff');
assert.equal(items[0].completion.label, 'aaa');
assert.equal(items[1].completion.label, 'zzz');
assert.equal(items[2].completion.label, 'fff');
});
test('sort - snippet bottom', async function () {
const items = await provideSuggestionItems(model, new Position(1, 1), 'bottom');
assert.equal(items.length, 3);
assert.equal(items[0].suggestion.label, 'fff');
assert.equal(items[1].suggestion.label, 'aaa');
assert.equal(items[2].suggestion.label, 'zzz');
assert.equal(items[0].completion.label, 'fff');
assert.equal(items[1].completion.label, 'aaa');
assert.equal(items[2].completion.label, 'zzz');
});
test('sort - snippet none', async function () {
const items = await provideSuggestionItems(model, new Position(1, 1), 'none');
assert.equal(items.length, 1);
assert.equal(items[0].suggestion.label, 'fff');
assert.equal(items[0].completion.label, 'fff');
});
test('only from', function () {
@@ -98,7 +102,7 @@ suite('Suggest', function () {
registration.dispose();
assert.equal(items.length, 1);
assert.ok(items[0].support === foo);
assert.ok(items[0].provider === foo);
});
});
});

View File

@@ -7,15 +7,15 @@ import * as assert from 'assert';
import { LRUMemory, NoMemory, PrefixMemory, Memory } from 'vs/editor/contrib/suggest/suggestMemory';
import { ITextModel } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { ICompletionItem } from 'vs/editor/contrib/suggest/completionModel';
import { createSuggestItem } from 'vs/editor/contrib/suggest/test/completionModel.test';
import { IPosition } from 'vs/editor/common/core/position';
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
suite('SuggestMemories', function () {
let pos: IPosition;
let buffer: ITextModel;
let items: ICompletionItem[];
let items: CompletionItem[];
setup(function () {
pos = { lineNumber: 1, column: 1 };
@@ -29,7 +29,7 @@ suite('SuggestMemories', function () {
test('AbstractMemory, select', function () {
const mem = new class extends Memory {
memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void {
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
throw new Error('Method not implemented.');
} toJSON(): object {
throw new Error('Method not implemented.');
@@ -43,9 +43,9 @@ suite('SuggestMemories', function () {
let item2 = createSuggestItem('bazz', 0);
let item3 = createSuggestItem('bazz', 0);
let item4 = createSuggestItem('bazz', 0);
item1.suggestion.preselect = false;
item2.suggestion.preselect = true;
item3.suggestion.preselect = true;
item1.completion.preselect = false;
item2.completion.preselect = true;
item3.completion.preselect = true;
assert.equal(mem.select(buffer, pos, [item1, item2, item3, item4]), 1);
});
@@ -55,9 +55,9 @@ suite('SuggestMemories', function () {
let item2 = createSuggestItem('bazz', 0);
let item3 = createSuggestItem('bazz', 0);
let item4 = createSuggestItem('bazz', 0);
item1.suggestion.preselect = false;
item2.suggestion.preselect = true;
item3.suggestion.preselect = true;
item1.completion.preselect = false;
item2.completion.preselect = true;
item3.completion.preselect = true;
let items = [item1, item2, item3, item4];
@@ -74,7 +74,7 @@ suite('SuggestMemories', function () {
assert.equal(mem.select(buffer, pos, []), 0);
mem.memorize(buffer, pos, items[0]);
mem.memorize(buffer, pos, null);
mem.memorize(buffer, pos, null!);
});
test('LRUMemory', () => {

View File

@@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { TokenizationResult2 } from 'vs/editor/common/core/token';
import { Handler } from 'vs/editor/common/editorCommon';
@@ -27,6 +28,8 @@ import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/com
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory';
import { ITextModel } from 'vs/editor/common/model';
export interface Ctor<T> {
new(): T;
@@ -42,7 +45,15 @@ function createMockEditor(model: TextModel): TestCodeEditor {
model: model,
serviceCollection: new ServiceCollection(
[ITelemetryService, NullTelemetryService],
[IStorageService, new InMemoryStorageService()]
[IStorageService, new InMemoryStorageService()],
[ISuggestMemoryService, new class implements ISuggestMemoryService {
_serviceBrand: any;
memorize(): void {
}
select(): number {
return -1;
}
}],
),
});
editor.registerAndInstantiateContribution(SnippetController2);
@@ -60,7 +71,7 @@ suite('SuggestModel - Context', function () {
this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, {
getInitialState: (): IState => NULL_STATE,
tokenize: undefined,
tokenize: undefined!,
tokenize2: (line: string, state: IState): TokenizationResult2 => {
const tokensArr: number[] = [];
let prevLanguageId: LanguageIdentifier | undefined = undefined;
@@ -140,6 +151,11 @@ suite('SuggestModel - Context', function () {
suite('SuggestModel - TriggerAndCancelOracle', function () {
function getDefaultSuggestRange(model: ITextModel, position: Position) {
const wordUntil = model.getWordUntilPosition(position);
return new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
}
const alwaysEmptySupport: CompletionItemProvider = {
provideCompletionItems(doc, pos): CompletionList {
return {
@@ -156,7 +172,8 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
suggestions: [{
label: doc.getWordUntilPosition(pos).word,
kind: CompletionItemKind.Property,
insertText: 'foofoo'
insertText: 'foofoo',
range: getDefaultSuggestRange(doc, pos)
}]
};
}
@@ -213,39 +230,27 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle(model => {
return Promise.all([
assertEvent(model.onDidCancel, function () {
model.cancel();
}, function (event) {
assert.equal(event.retrigger, false);
}),
assertEvent(model.onDidCancel, function () {
model.cancel(true);
assertEvent(model.onDidTrigger, function () {
model.trigger({ auto: true, shy: false });
}, function (event) {
assert.equal(event.retrigger, true);
}),
assert.equal(event.auto, true);
// cancel on trigger
assertEvent(model.onDidCancel, function () {
model.trigger({ auto: false });
}, function (event) {
assert.equal(event.retrigger, false);
}),
assertEvent(model.onDidCancel, function () {
model.trigger({ auto: false }, true);
}, function (event) {
assert.equal(event.retrigger, true);
return assertEvent(model.onDidCancel, function () {
model.cancel();
}, function (event) {
assert.equal(event.retrigger, false);
});
}),
assertEvent(model.onDidTrigger, function () {
model.trigger({ auto: true });
model.trigger({ auto: true, shy: false });
}, function (event) {
assert.equal(event.auto, true);
}),
assertEvent(model.onDidTrigger, function () {
model.trigger({ auto: false });
model.trigger({ auto: false, shy: false });
}, function (event) {
assert.equal(event.auto, false);
})
@@ -261,12 +266,12 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle(model => {
return Promise.all([
assertEvent(model.onDidCancel, function () {
model.trigger({ auto: true });
model.trigger({ auto: true, shy: false });
}, function (event) {
assert.equal(event.retrigger, false);
}),
assertEvent(model.onDidSuggest, function () {
model.trigger({ auto: false });
model.trigger({ auto: false, shy: false });
}, function (event) {
assert.equal(event.auto, false);
assert.equal(event.isFrozen, false);
@@ -290,7 +295,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.support, alwaysSomethingSupport);
assert.equal(first.provider, alwaysSomethingSupport);
});
});
});
@@ -304,7 +309,8 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
suggestions: [{
label: 'My Table',
kind: CompletionItemKind.Property,
insertText: 'My Table'
insertText: 'My Table',
range: getDefaultSuggestRange(doc, pos)
}]
};
}
@@ -316,7 +322,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return assertEvent(model.onDidSuggest, () => {
// make sure completionModel starts here!
model.trigger({ auto: true });
model.trigger({ auto: true, shy: false });
}, event => {
return assertEvent(model.onDidSuggest, () => {
@@ -327,7 +333,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.auto, true);
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.suggestion.label, 'My Table');
assert.equal(first.completion.label, 'My Table');
return assertEvent(model.onDidSuggest, () => {
editor.setPosition({ lineNumber: 1, column: 3 });
@@ -337,7 +343,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.auto, true);
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.suggestion.label, 'My Table');
assert.equal(first.completion.label, 'My Table');
});
});
});
@@ -390,7 +396,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.auto, true);
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.suggestion.label, 'foo.bar');
assert.equal(first.completion.label, 'foo.bar');
return assertEvent(model.onDidSuggest, () => {
editor.trigger('keyboard', Handler.Type, { text: '.' });
@@ -399,8 +405,8 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.auto, true);
assert.equal(event.completionModel.items.length, 2);
const [first, second] = event.completionModel.items;
assert.equal(first.suggestion.label, 'foo.bar');
assert.equal(second.suggestion.label, 'boom');
assert.equal(first.completion.label, 'foo.bar');
assert.equal(second.completion.label, 'boom');
});
});
});
@@ -412,11 +418,11 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle((model, editor) => {
editor.getModel().setValue('fo');
editor.getModel()!.setValue('fo');
editor.setPosition({ lineNumber: 1, column: 3 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false });
model.trigger({ auto: false, shy: false });
}, event => {
assert.equal(event.auto, false);
assert.equal(event.isFrozen, false);
@@ -437,11 +443,11 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle((model, editor) => {
editor.getModel().setValue('fo');
editor.getModel()!.setValue('fo');
editor.setPosition({ lineNumber: 1, column: 3 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false });
model.trigger({ auto: false, shy: false });
}, event => {
assert.equal(event.auto, false);
assert.equal(event.isFrozen, false);
@@ -474,11 +480,11 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle((model, editor) => {
editor.getModel().setValue('foo');
editor.getModel()!.setValue('foo');
editor.setPosition({ lineNumber: 1, column: 4 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false });
model.trigger({ auto: false, shy: false });
}, event => {
assert.equal(event.auto, false);
assert.equal(event.completionModel.incomplete.size, 1);
@@ -511,11 +517,11 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle((model, editor) => {
editor.getModel().setValue('foo');
editor.getModel()!.setValue('foo');
editor.setPosition({ lineNumber: 1, column: 4 });
return assertEvent(model.onDidSuggest, () => {
model.trigger({ auto: false });
model.trigger({ auto: false, shy: false });
}, event => {
assert.equal(event.auto, false);
assert.equal(event.completionModel.incomplete.size, 1);
@@ -542,7 +548,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
triggerCharacters: ['.'],
provideCompletionItems(doc, pos, context): CompletionList {
assert.equal(context.triggerKind, CompletionTriggerKind.TriggerCharacter);
triggerCharacter = context.triggerCharacter;
triggerCharacter = context.triggerCharacter!;
return {
incomplete: false,
suggestions: [
@@ -598,7 +604,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
editor.trigger('keyboard', Handler.Type, { text: 'a' });
}, event => {
assert.equal(event.completionModel.items.length, 1);
assert.equal(event.completionModel.items[0].suggestion.label, 'abc');
assert.equal(event.completionModel.items[0].completion.label, 'abc');
return assertEvent(model.onDidSuggest, () => {
editor.executeEdits('test', [EditOperation.replace(new Range(1, 1, 1, 2), 'ä')]);
@@ -606,7 +612,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
}, event => {
// suggest model changed to äbc
assert.equal(event.completionModel.items.length, 1);
assert.equal(event.completionModel.items[0].suggestion.label, 'äbc');
assert.equal(event.completionModel.items[0].completion.label, 'äbc');
});
});
@@ -626,7 +632,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.support, alwaysSomethingSupport);
assert.equal(first.provider, alwaysSomethingSupport);
});
await assertEvent(model.onDidSuggest, () => {
@@ -637,7 +643,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.support, alwaysSomethingSupport);
assert.equal(first.provider, alwaysSomethingSupport);
});
});
});
@@ -674,12 +680,12 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
await assertEvent(sugget.onDidSuggest, () => {
editor.setPosition({ lineNumber: 1, column: 3 });
sugget.trigger({ auto: false });
sugget.trigger({ auto: false, shy: false });
}, event => {
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.suggestion.label, 'bar');
assert.equal(first.completion.label, 'bar');
ctrl._onDidSelectItem({ item: first, index: 0, model: event.completionModel });
});
@@ -706,7 +712,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
assert.equal(event.completionModel.items.length, 1);
const [first] = event.completionModel.items;
assert.equal(first.support, alwaysSomethingSupport);
assert.equal(first.provider, alwaysSomethingSupport);
});
});
});
@@ -721,7 +727,13 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
provideCompletionItems(doc, pos) {
return {
incomplete: true,
suggestions: [{ kind: CompletionItemKind.Folder, label: 'CompleteNot', insertText: 'Incomplete', sortText: 'a', overwriteBefore: pos.column - 1 }],
suggestions: [{
kind: CompletionItemKind.Folder,
label: 'CompleteNot',
insertText: 'Incomplete',
sortText: 'a',
range: getDefaultSuggestRange(doc, pos)
}],
dispose() { disposeA += 1; }
};
}
@@ -730,7 +742,13 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
provideCompletionItems(doc, pos) {
return {
incomplete: false,
suggestions: [{ kind: CompletionItemKind.Folder, label: 'Complete', insertText: 'Complete', sortText: 'z', overwriteBefore: pos.column - 1 }],
suggestions: [{
kind: CompletionItemKind.Folder,
label: 'Complete',
insertText: 'Complete',
sortText: 'z',
range: getDefaultSuggestRange(doc, pos)
}],
dispose() { disposeB += 1; }
};
},

View File

@@ -8,9 +8,8 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IPosition } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import * as tokenTree from 'vs/editor/contrib/smartSelect/tokenTree';
import { CompletionItem, CompletionItemKind } from 'vs/editor/common/modes';
import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections';
export abstract class WordDistance {
@@ -18,7 +17,7 @@ export abstract class WordDistance {
distance() { return 0; }
};
static create(service: IEditorWorkerService, editor: ICodeEditor): Thenable<WordDistance> {
static create(service: IEditorWorkerService, editor: ICodeEditor): Promise<WordDistance> {
if (!editor.getConfiguration().contribInfo.suggest.localityBonus) {
return Promise.resolve(WordDistance.None);
@@ -35,51 +34,37 @@ export abstract class WordDistance {
return Promise.resolve(WordDistance.None);
}
// use token tree ranges
let node = tokenTree.find(tokenTree.build(model), position);
let ranges: Range[] = [];
while (node) {
if (!node.range.isEmpty()) {
ranges.push(node.range);
return new BracketSelectionRangeProvider().provideSelectionRanges(model, position).then(ranges => {
if (!ranges || ranges.length === 0) {
return WordDistance.None;
}
if (node.end.lineNumber - node.start.lineNumber >= 100) {
break;
}
node = node.parent;
}
ranges.reverse();
if (ranges.length === 0) {
return Promise.resolve(WordDistance.None);
}
return service.computeWordRanges(model.uri, ranges[0]).then(wordRanges => {
return new class extends WordDistance {
distance(anchor: IPosition, suggestion: CompletionItem) {
if (!wordRanges || !position.equals(editor.getPosition())) {
return 0;
}
if (suggestion.kind === CompletionItemKind.Keyword) {
return 2 << 20;
}
let word = suggestion.label;
let wordLines = wordRanges[word];
if (isFalsyOrEmpty(wordLines)) {
return 2 << 20;
}
let idx = binarySearch(wordLines, Range.fromPositions(anchor), Range.compareRangesUsingStarts);
let bestWordRange = idx >= 0 ? wordLines[idx] : wordLines[Math.max(0, ~idx - 1)];
let blockDistance = ranges.length;
for (const range of ranges) {
if (!Range.containsRange(range, bestWordRange)) {
break;
return service.computeWordRanges(model.uri, ranges[0].range).then(wordRanges => {
return new class extends WordDistance {
distance(anchor: IPosition, suggestion: CompletionItem) {
if (!wordRanges || !position.equals(editor.getPosition())) {
return 0;
}
blockDistance -= 1;
if (suggestion.kind === CompletionItemKind.Keyword) {
return 2 << 20;
}
let word = suggestion.label;
let wordLines = wordRanges[word];
if (isFalsyOrEmpty(wordLines)) {
return 2 << 20;
}
let idx = binarySearch(wordLines, Range.fromPositions(anchor), Range.compareRangesUsingStarts);
let bestWordRange = idx >= 0 ? wordLines[idx] : wordLines[Math.max(0, ~idx - 1)];
let blockDistance = ranges.length;
for (const range of ranges) {
if (!Range.containsRange(range.range, bestWordRange)) {
break;
}
blockDistance -= 1;
}
return blockDistance;
}
return blockDistance;
}
};
};
});
});
}