Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View File

@@ -3,22 +3,22 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore } from 'vs/base/common/filters';
import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore, FuzzyScorer } from 'vs/base/common/filters';
import { isDisposable } from 'vs/base/common/lifecycle';
import { ISuggestResult, ISuggestSupport } from 'vs/editor/common/modes';
import { ISuggestionItem } from './suggest';
import { CompletionList, CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes';
import { ISuggestionItem, ensureLowerCaseVariants } 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;
}
/* __GDPR__FRAGMENT__
"ICompletionStats" : {
"suggestionCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
@@ -49,18 +49,26 @@ export class CompletionModel {
private readonly _items: ICompletionItem[];
private readonly _column: number;
private readonly _wordDistance: WordDistance;
private readonly _options: InternalSuggestOptions;
private readonly _snippetCompareFn = CompletionModel._compareCompletionItems;
private _lineContext: LineContext;
private _refilterKind: Refilter;
private _filteredItems: ICompletionItem[];
private _isIncomplete: Set<ISuggestSupport>;
private _isIncomplete: Set<CompletionItemProvider>;
private _stats: ICompletionStats;
constructor(items: ISuggestionItem[], column: number, lineContext: LineContext, options: InternalSuggestOptions = EDITOR_DEFAULTS.contribInfo.suggest) {
constructor(
items: ISuggestionItem[],
column: number,
lineContext: LineContext,
wordDistance: WordDistance,
options: InternalSuggestOptions = EDITOR_DEFAULTS.contribInfo.suggest
) {
this._items = items;
this._column = column;
this._wordDistance = wordDistance;
this._options = options;
this._refilterKind = Refilter.All;
this._lineContext = lineContext;
@@ -73,7 +81,7 @@ export class CompletionModel {
}
dispose(): void {
const seen = new Set<ISuggestResult>();
const seen = new Set<CompletionList>();
for (const { container } of this._items) {
if (!seen.has(container)) {
seen.add(container);
@@ -102,12 +110,12 @@ export class CompletionModel {
return this._filteredItems;
}
get incomplete(): Set<ISuggestSupport> {
get incomplete(): Set<CompletionItemProvider> {
this._ensureCachedState();
return this._isIncomplete;
}
adopt(except: Set<ISuggestSupport>): ISuggestionItem[] {
adopt(except: Set<CompletionItemProvider>): ISuggestionItem[] {
let res = new Array<ISuggestionItem>();
for (let i = 0; i < this._items.length;) {
if (!except.has(this._items[i].support)) {
@@ -143,6 +151,7 @@ export class CompletionModel {
const { leadingLineContent, characterCountDelta } = this._lineContext;
let word = '';
let wordLow = '';
// incrementally filter less
const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems;
@@ -151,13 +160,16 @@ export class CompletionModel {
// picks a score function based on the number of
// items that we have to score/filter and based on the
// user-configuration
const scoreFn = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;
const scoreFn: FuzzyScorer = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;
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) {
@@ -167,9 +179,11 @@ export class CompletionModel {
// '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 wordLen = suggestion.overwriteBefore + characterCountDelta - (item.position.column - this._column);
const overwriteBefore = item.position.column - suggestion.range.startColumn;
const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
if (word.length !== wordLen) {
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
wordLow = word.toLowerCase();
}
// remember the word against which this item was
@@ -185,38 +199,58 @@ export class CompletionModel {
item.score = -100;
item.matches = undefined;
} else if (typeof suggestion.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, suggestion.filterText, suggestion.overwriteBefore);
if (!match) {
continue;
}
item.score = match[0];
item.matches = (fuzzyScore(word, suggestion.label) || anyScore(word, suggestion.label))[1];
} else {
// by default match `word` against the `label`
let match = scoreFn(word, suggestion.label, suggestion.overwriteBefore);
if (match) {
// skip word characters that are whitespace until
// we have hit the replace range (overwriteBefore)
let wordPos = 0;
while (wordPos < overwriteBefore) {
const ch = word.charCodeAt(wordPos);
if (ch === CharCode.Space || ch === CharCode.Tab) {
wordPos += 1;
} else {
break;
}
}
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 = [];
} else if (typeof suggestion.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);
if (!match) {
continue;
}
item.score = match[0];
item.matches = match[1];
item.matches = (fuzzyScore(word, wordLow, 0, suggestion.label, suggestion._labelLow, 0, true) || anyScore(word, suggestion.label))[1];
} else {
continue;
// 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;
}
}
}
item.idx = i;
item.distance = this._wordDistance.distance(item.position, suggestion);
target.push(item);
// update stats
this._stats.suggestionCount++;
switch (suggestion.type) {
case 'snippet': this._stats.snippetCount++; break;
case 'text': this._stats.textCount++; break;
switch (suggestion.kind) {
case CompletionItemKind.Snippet: this._stats.snippetCount++; break;
case CompletionItemKind.Text: this._stats.textCount++; break;
}
}
@@ -229,6 +263,10 @@ export class CompletionModel {
return -1;
} else if (a.score < b.score) {
return 1;
} else if (a.distance < b.distance) {
return -1;
} else if (a.distance > b.distance) {
return 1;
} else if (a.idx < b.idx) {
return -1;
} else if (a.idx > b.idx) {
@@ -239,10 +277,10 @@ export class CompletionModel {
}
private static _compareCompletionItemsSnippetsDown(a: ICompletionItem, b: ICompletionItem): number {
if (a.suggestion.type !== b.suggestion.type) {
if (a.suggestion.type === 'snippet') {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
return 1;
} else if (b.suggestion.type === 'snippet') {
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
return -1;
}
}
@@ -250,10 +288,10 @@ export class CompletionModel {
}
private static _compareCompletionItemsSnippetsUp(a: ICompletionItem, b: ICompletionItem): number {
if (a.suggestion.type !== b.suggestion.type) {
if (a.suggestion.type === 'snippet') {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
return -1;
} else if (b.suggestion.type === 'snippet') {
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
return 1;
}
}

View File

@@ -123,6 +123,7 @@
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.7;
white-space: nowrap;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .type-label > .monaco-tokenized-source {
@@ -143,43 +144,64 @@
}
/** Styles for each row in the list **/
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label::before {
height: 100%;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon {
display: block;
height: 16px;
width: 16px;
margin-left: 2px;
background-repeat: no-repeat;
background-size: 80%;
background-position: center;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon { background-image: url('Misc_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.method,
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.function,
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.constructor { background-image: url('Method_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.field { background-image: url('Field_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.event { background-image: url('Event_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.operator { background-image: url('Operator_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.variable { background-image: url('LocalVariable_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.class { background-image: url('Class_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.interface { background-image: url('Interface_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.struct { background-image: url('Structure_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.type-parameter { background-image: url('Template_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.module { background-image: url('Namespace_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.property { background-image: url('Property_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.unit { background-image: url('Ruler_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.constant { background-image: url('Constant_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.value,
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.enum { background-image: url('Enumerator_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.enum-member { background-image: url('EnumItem_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.keyword { background-image: url('IntelliSenseKeyword_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.text { background-image: url('String_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.color { background-image: url('ColorPalette_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.file { background-image: url('Document_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.reference { background-image: url('ImportFile_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.snippet { background-image: url('Snippet_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.customcolor { background-image: none; }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.folder { background-image: url('Folder_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.hide {
display: none;
}
.monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .icon,
.monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .monaco-icon-label.suggest-icon::before {
display: none;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.suggest-icon::before {
content: ' ';
background-image: url('Misc_16x.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method::before,
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.function::before,
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constructor::before { background-image: url('Method_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.field::before { background-image: url('Field_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.event::before { background-image: url('Event_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.operator::before { background-image: url('Operator_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.variable::before { background-image: url('LocalVariable_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.class::before { background-image: url('Class_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.interface::before { background-image: url('Interface_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.struct::before { background-image: url('Structure_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.type-parameter::before { background-image: url('Template_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.module::before { background-image: url('Namespace_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.property::before { background-image: url('Property_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.unit::before { background-image: url('Ruler_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constant::before { background-image: url('Constant_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.value::before,
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum::before { background-image: url('Enumerator_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum-member::before { background-image: url('EnumItem_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.keyword::before { background-image: url('IntelliSenseKeyword_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.text::before { background-image: url('String_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.color::before { background-image: url('ColorPalette_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.file::before { background-image: url('Document_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.reference::before { background-image: url('ImportFile_16x_vscode.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.snippet::before { background-image: url('Snippet_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.customcolor::before { background-image: none; }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon.folder::before { background-image: url('Folder_16x.svg'); }
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.customcolor .colorspan {
margin: 0 0 0 0.3em;
@@ -201,7 +223,7 @@
}
.monaco-editor .suggest-widget.docs-below .details {
border-top-width: 0px;
border-top-width: 0;
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element {
@@ -222,7 +244,7 @@
opacity: 0.7;
word-break: break-all;
margin: 0;
padding: 4px 0 4px 5px;
padding: 4px 0 12px 5px;
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs {
@@ -232,9 +254,23 @@
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs {
padding: 0;
white-space: initial;
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs > div,
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs > span:not(:empty) {
padding: 4px 5px;
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs > div > p:first-child {
margin-top: 0;
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs > div > p:last-child {
margin-bottom: 0;
}
.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs .code {
white-space: pre-wrap;
word-wrap: break-word;
@@ -256,80 +292,80 @@
background-image: url('./close-dark.svg');
}
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon { background-image: url('Misc_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon::before { background-image: url('Misc_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.method,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.method,
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.function,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.function,
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.constructor,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.constructor { background-image: url('Method_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.method::before,
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.function::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.function::before,
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constructor::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constructor::before { background-image: url('Method_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.field,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.field { background-image: url('Field_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.field::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.field::before { background-image: url('Field_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.event,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.event { background-image: url('Event_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.event::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.event::before { background-image: url('Event_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.operator,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.operator { background-image: url('Operator_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.operator::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.operator::before { background-image: url('Operator_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.variable,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.variable { background-image: url('LocalVariable_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.variable::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.variable::before { background-image: url('LocalVariable_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.class,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.class { background-image: url('Class_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.class::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.class::before { background-image: url('Class_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.interface,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.interface { background-image: url('Interface_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.interface::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.interface::before { background-image: url('Interface_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.struct,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.struct { background-image: url('Structure_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.struct::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.struct::before { background-image: url('Structure_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.type-parameter,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.type-parameter { background-image: url('Template_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.type-parameter::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.type-parameter::before { background-image: url('Template_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.module,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.module { background-image: url('Namespace_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.module::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.module::before { background-image: url('Namespace_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.property,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.property { background-image: url('Property_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.property::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.property::before { background-image: url('Property_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.unit,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.unit { background-image: url('Ruler_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.unit::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.unit::before { background-image: url('Ruler_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.constant,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.constant { background-image: url('Constant_16x_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constant::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.constant::before { background-image: url('Constant_16x_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.value,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.value,
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.enum,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.enum { background-image: url('Enumerator_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.value::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.value::before,
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum::before { background-image: url('Enumerator_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.enum-member,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.enum-member { background-image: url('EnumItem_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum-member::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.enum-member::before { background-image: url('EnumItem_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.keyword,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.keyword { background-image: url('IntelliSenseKeyword_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.keyword::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.keyword::before { background-image: url('IntelliSenseKeyword_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.text,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.text { background-image: url('String_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.text::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.text::before { background-image: url('String_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.color,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.color { background-image: url('ColorPalette_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.color::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.color::before { background-image: url('ColorPalette_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.file,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.file { background-image: url('Document_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.file::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.file::before { background-image: url('Document_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.reference,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.reference { background-image: url('ImportFile_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.reference::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.reference::before { background-image: url('ImportFile_16x_vscode_inverse.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.snippet,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.snippet { background-image: url('Snippet_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.snippet::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.snippet::before { background-image: url('Snippet_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.customcolor,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.customcolor { background-image: none; }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.customcolor::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.customcolor::before { background-image: none; }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon.folder,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon.folder { background-image: url('Folder_inverse_16x.svg'); }
.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .suggest-icon.folder::before,
.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .suggest-icon.folder::before { background-image: url('Folder_inverse_16x.svg'); }

View File

@@ -3,45 +3,44 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { first2 } from 'vs/base/common/async';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { compareIgnoreCase } from 'vs/base/common/strings';
import { first } from 'vs/base/common/async';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { assign } from 'vs/base/common/objects';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { onUnexpectedExternalError, canceled, isPromiseCanceledError } from 'vs/base/common/errors';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions';
import { ISuggestResult, ISuggestSupport, ISuggestion, SuggestRegistry, SuggestContext, SuggestTriggerKind } from 'vs/editor/common/modes';
import { CompletionList, CompletionItemProvider, CompletionItem, CompletionProviderRegistry, CompletionContext, CompletionTriggerKind, CompletionItemKind } 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';
export const Context = {
Visible: new RawContextKey<boolean>('suggestWidgetVisible', false),
MultipleSuggestions: new RawContextKey<boolean>('suggestWidgetMultipleSuggestions', false),
MakesTextEdit: new RawContextKey('suggestionMakesTextEdit', true),
AcceptOnKey: new RawContextKey<boolean>('suggestionSupportsAcceptOnKey', true),
AcceptSuggestionsOnEnter: new RawContextKey<boolean>('acceptSuggestionOnEnter', true)
};
export interface ISuggestionItem {
position: IPosition;
suggestion: ISuggestion;
container: ISuggestResult;
support: ISuggestSupport;
suggestion: CompletionItem;
container: CompletionList;
support: CompletionItemProvider;
resolve(token: CancellationToken): Thenable<void>;
}
export type SnippetConfig = 'top' | 'bottom' | 'inline' | 'none';
let _snippetSuggestSupport: ISuggestSupport;
let _snippetSuggestSupport: CompletionItemProvider;
export function getSnippetSuggestSupport(): ISuggestSupport {
export function getSnippetSuggestSupport(): CompletionItemProvider {
return _snippetSuggestSupport;
}
export function setSnippetSuggestSupport(support: ISuggestSupport): ISuggestSupport {
export function setSnippetSuggestSupport(support: CompletionItemProvider): CompletionItemProvider {
const old = _snippetSuggestSupport;
_snippetSuggestSupport = support;
return old;
@@ -51,25 +50,28 @@ export function provideSuggestionItems(
model: ITextModel,
position: Position,
snippetConfig: SnippetConfig = 'bottom',
onlyFrom?: ISuggestSupport[],
context?: SuggestContext,
onlyFrom?: CompletionItemProvider[],
context?: CompletionContext,
token: CancellationToken = CancellationToken.None
): Promise<ISuggestionItem[]> {
const allSuggestions: ISuggestionItem[] = [];
const acceptSuggestion = createSuggesionFilter(snippetConfig);
const wordUntil = model.getWordUntilPosition(position);
const defaultRange = new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
position = position.clone();
// get provider groups, always add snippet suggestion provider
const supports = SuggestRegistry.orderedGroups(model);
const supports = CompletionProviderRegistry.orderedGroups(model);
// add snippets provider unless turned off
if (snippetConfig !== 'none' && _snippetSuggestSupport) {
supports.unshift([_snippetSuggestSupport]);
}
const suggestConext = context || { triggerKind: SuggestTriggerKind.Invoke };
const suggestConext = context || { triggerKind: 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
@@ -78,7 +80,7 @@ export function provideSuggestionItems(
// for each support in the group ask for suggestions
return Promise.all(supports.map(support => {
if (!isFalsyOrEmpty(onlyFrom) && onlyFrom.indexOf(support) < 0) {
if (isNonEmptyArray(onlyFrom) && onlyFrom.indexOf(support) < 0) {
return undefined;
}
@@ -86,11 +88,17 @@ export function provideSuggestionItems(
const len = allSuggestions.length;
if (container && !isFalsyOrEmpty(container.suggestions)) {
for (let suggestion of container.suggestions) {
if (container) {
for (let suggestion of container.suggestions || []) {
if (acceptSuggestion(suggestion)) {
fixOverwriteBeforeAfter(suggestion, container);
// fill in default range when missing
if (!suggestion.range) {
suggestion.range = defaultRange;
}
// fill in lower-case text
ensureLowerCaseVariants(suggestion);
allSuggestions.push({
position,
@@ -111,7 +119,15 @@ export function provideSuggestionItems(
}));
});
const result = first2(factory, () => hasResult).then(() => allSuggestions.sort(getSuggestionComparator(snippetConfig)));
const result = first(factory, () => {
// stop on result or cancellation
return hasResult || token.isCancellationRequested;
}).then(() => {
if (token.isCancellationRequested) {
return Promise.reject(canceled());
}
return allSuggestions.sort(getSuggestionComparator(snippetConfig));
});
// result.then(items => {
// console.log(model.getWordUntilPosition(position), items.map(item => `${item.suggestion.label}, type=${item.suggestion.type}, incomplete?${item.container.incomplete}, overwriteBefore=${item.suggestion.overwriteBefore}`));
@@ -123,63 +139,83 @@ export function provideSuggestionItems(
return result;
}
function fixOverwriteBeforeAfter(suggestion: ISuggestion, container: ISuggestResult): void {
if (typeof suggestion.overwriteBefore !== 'number') {
suggestion.overwriteBefore = 0;
export function ensureLowerCaseVariants(suggestion: CompletionItem) {
if (!suggestion._labelLow) {
suggestion._labelLow = suggestion.label.toLowerCase();
}
if (typeof suggestion.overwriteAfter !== 'number' || suggestion.overwriteAfter < 0) {
suggestion.overwriteAfter = 0;
if (suggestion.sortText && !suggestion._sortTextLow) {
suggestion._sortTextLow = suggestion.sortText.toLowerCase();
}
if (suggestion.filterText && !suggestion._filterTextLow) {
suggestion._filterTextLow = suggestion.filterText.toLowerCase();
}
}
function createSuggestionResolver(provider: ISuggestSupport, suggestion: ISuggestion, model: ITextModel, position: Position): (token: CancellationToken) => Promise<void> {
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 (typeof provider.resolveCompletionItem === 'function') {
return Promise.resolve(provider.resolveCompletionItem(model, position, suggestion, token)).then(value => { assign(suggestion, value); });
} else {
return Promise.resolve(void 0);
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: ISuggestion) => boolean {
function createSuggesionFilter(snippetConfig: SnippetConfig): (candidate: CompletionItem) => boolean {
if (snippetConfig === 'none') {
return suggestion => suggestion.type !== 'snippet';
return suggestion => suggestion.kind !== CompletionItemKind.Snippet;
} else {
return () => true;
}
}
function defaultComparator(a: ISuggestionItem, b: ISuggestionItem): number {
let ret = 0;
// check with 'sortText'
if (typeof a.suggestion.sortText === 'string' && typeof b.suggestion.sortText === 'string') {
ret = compareIgnoreCase(a.suggestion.sortText, b.suggestion.sortText);
}
// check with 'label'
if (ret === 0) {
ret = compareIgnoreCase(a.suggestion.label, b.suggestion.label);
}
// check with 'type' and lower snippets
if (ret === 0 && a.suggestion.type !== b.suggestion.type) {
if (a.suggestion.type === 'snippet') {
ret = 1;
} else if (b.suggestion.type === 'snippet') {
ret = -1;
if (a.suggestion._sortTextLow && b.suggestion._sortTextLow) {
if (a.suggestion._sortTextLow < b.suggestion._sortTextLow) {
return -1;
} else if (a.suggestion._sortTextLow > b.suggestion._sortTextLow) {
return 1;
}
}
return ret;
// check with 'label'
if (a.suggestion.label < b.suggestion.label) {
return -1;
} else if (a.suggestion.label > b.suggestion.label) {
return 1;
}
// check with 'type'
return a.suggestion.kind - b.suggestion.kind;
}
function snippetUpComparator(a: ISuggestionItem, b: ISuggestionItem): number {
if (a.suggestion.type !== b.suggestion.type) {
if (a.suggestion.type === 'snippet') {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
return -1;
} else if (b.suggestion.type === 'snippet') {
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
return 1;
}
}
@@ -187,10 +223,10 @@ function snippetUpComparator(a: ISuggestionItem, b: ISuggestionItem): number {
}
function snippetDownComparator(a: ISuggestionItem, b: ISuggestionItem): number {
if (a.suggestion.type !== b.suggestion.type) {
if (a.suggestion.type === 'snippet') {
if (a.suggestion.kind !== b.suggestion.kind) {
if (a.suggestion.kind === CompletionItemKind.Snippet) {
return 1;
} else if (b.suggestion.type === 'snippet') {
} else if (b.suggestion.kind === CompletionItemKind.Snippet) {
return -1;
}
}
@@ -209,7 +245,7 @@ export function getSuggestionComparator(snippetConfig: SnippetConfig): (a: ISugg
registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, position, args) => {
const result: ISuggestResult = {
const result: CompletionList = {
incomplete: false,
suggestions: []
};
@@ -233,15 +269,15 @@ registerDefaultLanguageCommand('_executeCompletionItemProvider', (model, positio
});
interface SuggestController extends IEditorContribution {
triggerSuggest(onlyFrom?: ISuggestSupport[]): void;
triggerSuggest(onlyFrom?: CompletionItemProvider[]): void;
}
let _provider = new class implements ISuggestSupport {
let _provider = new class implements CompletionItemProvider {
onlyOnceSuggestions: ISuggestion[] = [];
onlyOnceSuggestions: CompletionItem[] = [];
provideCompletionItems(): ISuggestResult {
provideCompletionItems(): CompletionList {
let suggestions = this.onlyOnceSuggestions.slice(0);
let result = { suggestions };
this.onlyOnceSuggestions.length = 0;
@@ -249,9 +285,9 @@ let _provider = new class implements ISuggestSupport {
}
};
SuggestRegistry.register('*', _provider);
CompletionProviderRegistry.register('*', _provider);
export function showSimpleSuggestions(editor: ICodeEditor, suggestions: ISuggestion[]) {
export function showSimpleSuggestions(editor: ICodeEditor, suggestions: CompletionItem[]) {
setTimeout(() => {
_provider.onlyOnceSuggestions.push(...suggestions);
editor.getContribution<SuggestController>('editor.contrib.suggestController').triggerSuggest([_provider]);

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { CompletionModel } from './completionModel';
import { ISelectedSuggestion } from './suggestWidget';
export class SuggestAlternatives {
static OtherSuggestions = new RawContextKey<boolean>('hasOtherSuggestions', false);
private readonly _ckOtherSuggestions: IContextKey<boolean>;
private _index: number;
private _model: CompletionModel;
private _acceptNext: (selected: ISelectedSuggestion) => any;
private _listener: IDisposable;
private _ignore: boolean;
constructor(
private readonly _editor: ICodeEditor,
@IContextKeyService contextKeyService: IContextKeyService
) {
this._ckOtherSuggestions = SuggestAlternatives.OtherSuggestions.bindTo(contextKeyService);
}
dispose(): void {
this.reset();
}
reset(): void {
this._ckOtherSuggestions.reset();
dispose(this._listener);
this._model = undefined;
this._acceptNext = undefined;
this._ignore = false;
}
set({ model, index }: ISelectedSuggestion, acceptNext: (selected: ISelectedSuggestion) => any): void {
// no suggestions -> nothing to do
if (model.items.length === 0) {
this.reset();
return;
}
// no alternative suggestions -> nothing to do
let nextIndex = SuggestAlternatives._moveIndex(true, model, index);
if (nextIndex === index) {
this.reset();
return;
}
this._acceptNext = acceptNext;
this._model = model;
this._index = index;
this._listener = this._editor.onDidChangeCursorPosition(() => {
if (!this._ignore) {
this.reset();
}
});
this._ckOtherSuggestions.set(true);
}
private static _moveIndex(fwd: boolean, model: CompletionModel, index: number): number {
let newIndex = index;
while (true) {
newIndex = (newIndex + model.items.length + (fwd ? +1 : -1)) % model.items.length;
if (newIndex === index) {
break;
}
if (!model.items[newIndex].suggestion.additionalTextEdits) {
break;
}
}
return newIndex;
}
next(): void {
this._move(true);
}
prev(): void {
this._move(false);
}
private _move(fwd: boolean): void {
if (!this._model) {
// nothing to reason about
return;
}
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 });
} finally {
this._ignore = false;
}
}
}

View File

@@ -2,32 +2,36 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { onUnexpectedError } from 'vs/base/common/errors';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { isFalsyOrEmpty } 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';
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 { ISuggestSupport } from 'vs/editor/common/modes';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { IEditorContribution, ScrollType, Handler } 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 { Context as SuggestContext } from './suggest';
import { SuggestModel, State } from './suggestModel';
import { ICompletionItem } from './completionModel';
import { SuggestWidget, ISelectedSuggestion } from './suggestWidget';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { SuggestMemories } 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 { 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 { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IdleValue } from 'vs/base/common/async';
class AcceptOnCharacterOracle {
@@ -85,40 +89,62 @@ export class SuggestController implements IEditorContribution {
private _model: SuggestModel;
private _widget: SuggestWidget;
private _memory: SuggestMemories;
private readonly _memory: IdleValue<SuggestMemories>;
private readonly _alternatives: IdleValue<SuggestAlternatives>;
private _toDispose: IDisposable[] = [];
private readonly _sticky = false; // for development purposes only
constructor(
private _editor: ICodeEditor,
@IEditorWorkerService editorWorker: IEditorWorkerService,
@ICommandService private readonly _commandService: ICommandService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
this._model = new SuggestModel(this._editor);
this._memory = _instantiationService.createInstance(SuggestMemories, this._editor.getConfiguration().contribInfo.suggestSelection);
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._alternatives = new IdleValue(() => {
let res = new SuggestAlternatives(this._editor, this._contextKeyService);
this._toDispose.push(res);
return res;
});
this._toDispose.push(_instantiationService.createInstance(WordContextKey, _editor));
this._toDispose.push(this._model.onDidTrigger(e => {
if (!this._widget) {
this._createSuggestWidget();
}
this._widget.showTriggered(e.auto);
this._widget.showTriggered(e.auto, e.shy ? 250 : 50);
}));
this._toDispose.push(this._model.onDidSuggest(e => {
let index = this._memory.select(this._editor.getModel(), this._editor.getPosition(), e.completionModel.items);
this._widget.showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
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);
}
}));
this._toDispose.push(this._model.onDidCancel(e => {
if (this._widget && !e.retrigger) {
this._widget.hideWidget();
}
}));
this._toDispose.push(this._editor.onDidBlurEditorText(() => {
if (!this._sticky) {
this._model.cancel();
}
}));
// Manage the acceptSuggestionsOnEnter context key
let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService);
let updateFromConfig = () => {
const { acceptSuggestionOnEnter, suggestSelection } = this._editor.getConfiguration().contribInfo;
const { acceptSuggestionOnEnter } = this._editor.getConfiguration().contribInfo;
acceptSuggestionsOnEnter.set(acceptSuggestionOnEnter === 'on' || acceptSuggestionOnEnter === 'smart');
this._memory.setMode(suggestSelection);
};
this._toDispose.push(this._editor.onDidChangeConfiguration((e) => updateFromConfig()));
updateFromConfig();
@@ -127,10 +153,10 @@ export class SuggestController implements IEditorContribution {
private _createSuggestWidget(): void {
this._widget = this._instantiationService.createInstance(SuggestWidget, this._editor);
this._toDispose.push(this._widget.onDidSelect(this._onDidSelectItem, this));
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));
const autoAcceptOracle = new AcceptOnCharacterOracle(this._editor, this._widget, item => this._onDidSelectItem(item, false, true));
this._toDispose.push(
autoAcceptOracle,
this._model.onDidSuggest(e => {
@@ -144,7 +170,7 @@ export class SuggestController implements IEditorContribution {
this._toDispose.push(this._widget.onDidFocus(({ item }) => {
const position = this._editor.getPosition();
const startColumn = item.position.column - item.suggestion.overwriteBefore;
const startColumn = item.suggestion.range.startColumn;
const endColumn = position.column;
let value = true;
if (
@@ -152,7 +178,7 @@ export class SuggestController implements IEditorContribution {
&& this._model.state === State.Auto
&& !item.suggestion.command
&& !item.suggestion.additionalTextEdits
&& item.suggestion.snippetType !== 'textmate'
&& !(item.suggestion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet)
&& endColumn - startColumn === item.suggestion.insertText.length
) {
const oldText = this._editor.getModel().getValueInRange({
@@ -186,40 +212,51 @@ export class SuggestController implements IEditorContribution {
}
}
protected _onDidSelectItem(event: ISelectedSuggestion): void {
protected _onDidSelectItem(event: ISelectedSuggestion, keepAlternativeSuggestions: boolean, undoStops: boolean): void {
if (!event || !event.item) {
this._alternatives.getValue().reset();
this._model.cancel();
return;
}
const model = this._editor.getModel();
const modelVersionNow = model.getAlternativeVersionId();
const { suggestion, position } = event.item;
const editorColumn = this._editor.getPosition().column;
const columnDelta = editorColumn - position.column;
// pushing undo stops *before* additional text edits and
// *after* the main edit
this._editor.pushUndoStop();
if (undoStops) {
this._editor.pushUndoStop();
}
if (Array.isArray(suggestion.additionalTextEdits)) {
this._editor.executeEdits('suggestController.additionalTextEdits', suggestion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
}
// keep item in memory
this._memory.memorize(this._editor.getModel(), this._editor.getPosition(), event.item);
this._memory.getValue().memorize(model, this._editor.getPosition(), event.item);
let { insertText } = suggestion;
if (suggestion.snippetType !== 'textmate') {
if (!(suggestion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet)) {
insertText = SnippetParser.escape(insertText);
}
const overwriteBefore = position.column - suggestion.range.startColumn;
const overwriteAfter = suggestion.range.endColumn - position.column;
SnippetController2.get(this._editor).insert(
insertText,
suggestion.overwriteBefore + columnDelta,
suggestion.overwriteAfter,
false, false
overwriteBefore + columnDelta,
overwriteAfter,
false, false,
!(suggestion.insertTextRules & CompletionItemInsertTextRule.KeepWhitespace)
);
this._editor.pushUndoStop();
if (undoStops) {
this._editor.pushUndoStop();
}
if (!suggestion.command) {
// done
@@ -231,10 +268,25 @@ export class SuggestController implements IEditorContribution {
} else {
// exec command, done
this._commandService.executeCommand(suggestion.command.id, ...suggestion.command.arguments).done(undefined, onUnexpectedError);
this._commandService.executeCommand(suggestion.command.id, ...suggestion.command.arguments).then(undefined, onUnexpectedError);
this._model.cancel();
}
if (keepAlternativeSuggestions) {
this._alternatives.getValue().set(event, next => {
// this is not so pretty. when inserting the 'next'
// suggestion we undo until we are at the state at
// which we were before inserting the previous suggestion...
while (model.canUndo()) {
if (modelVersionNow !== model.getAlternativeVersionId()) {
model.undo();
}
this._onDidSelectItem(next, false, false);
break;
}
});
}
this._alertCompletionItem(event.item);
}
@@ -243,19 +295,92 @@ export class SuggestController implements IEditorContribution {
alert(msg);
}
triggerSuggest(onlyFrom?: ISuggestSupport[]): void {
triggerSuggest(onlyFrom?: CompletionItemProvider[]): void {
this._model.trigger({ auto: false }, false, onlyFrom);
this._editor.revealLine(this._editor.getPosition().lineNumber, ScrollType.Smooth);
this._editor.focus();
}
acceptSelectedSuggestion(): void {
triggerSuggestAndAcceptBest(defaultTypeText: string): void {
const positionNow = this._editor.getPosition();
const fallback = () => {
if (positionNow.equals(this._editor.getPosition())) {
this._editor.trigger('suggest', Handler.Type, { text: defaultTypeText });
}
};
const makesTextEdit = (item: ISuggestionItem): boolean => {
if (item.suggestion.insertTextRules & CompletionItemInsertTextRule.InsertAsSnippet || item.suggestion.additionalTextEdits) {
// snippet, other editor -> makes edit
return true;
}
const position = this._editor.getPosition();
const startColumn = item.suggestion.range.startColumn;
const endColumn = position.column;
if (endColumn - startColumn !== item.suggestion.insertText.length) {
// unequal lengths -> makes edit
return true;
}
const textNow = this._editor.getModel().getValueInRange({
startLineNumber: position.lineNumber,
startColumn,
endLineNumber: position.lineNumber,
endColumn
});
// unequal text -> makes edit
return textNow !== item.suggestion.insertText;
};
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)(() => {
// retrigger or cancel -> try to type default text
dispose(listener);
fallback();
}, undefined, listener);
this._model.onDidSuggest(({ completionModel }) => {
dispose(listener);
if (completionModel.items.length === 0) {
fallback();
return;
}
const index = this._memory.getValue().select(this._editor.getModel(), this._editor.getPosition(), completionModel.items);
const item = completionModel.items[index];
if (!makesTextEdit(item)) {
fallback();
return;
}
this._editor.pushUndoStop();
this._onDidSelectItem({ index, item, model: completionModel }, true, false);
}, undefined, listener);
});
this._model.trigger({ auto: false, shy: true });
this._editor.revealLine(positionNow.lineNumber, ScrollType.Smooth);
this._editor.focus();
}
acceptSelectedSuggestion(keepAlternativeSuggestions?: boolean): void {
if (this._widget) {
const item = this._widget.getFocusedItem();
this._onDidSelectItem(item);
this._onDidSelectItem(item, keepAlternativeSuggestions, true);
}
}
acceptNextSuggestion() {
this._alternatives.getValue().next();
}
acceptPrevSuggestion() {
this._alternatives.getValue().prev();
}
cancelSuggestWidget(): void {
if (this._widget) {
this._model.cancel();
@@ -353,7 +478,7 @@ const SuggestCommand = EditorCommand.bindToContribution<SuggestController>(Sugge
registerEditorCommand(new SuggestCommand({
id: 'acceptSelectedSuggestion',
precondition: SuggestContext.Visible,
handler: x => x.acceptSelectedSuggestion(),
handler: x => x.acceptSelectedSuggestion(true),
kbOpts: {
weight: weight,
kbExpr: EditorContextKeys.textInputFocus,
@@ -364,7 +489,7 @@ registerEditorCommand(new SuggestCommand({
registerEditorCommand(new SuggestCommand({
id: 'acceptSelectedSuggestionOnEnter',
precondition: SuggestContext.Visible,
handler: x => x.acceptSelectedSuggestion(),
handler: x => x.acceptSelectedSuggestion(false),
kbOpts: {
weight: weight,
kbExpr: ContextKeyExpr.and(EditorContextKeys.textInputFocus, SuggestContext.AcceptSuggestionsOnEnter, SuggestContext.MakesTextEdit),
@@ -469,3 +594,53 @@ registerEditorCommand(new SuggestCommand({
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space }
}
}));
//#region tab completions
registerEditorCommand(new SuggestCommand({
id: 'insertBestCompletion',
precondition: ContextKeyExpr.and(
ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),
WordContextKey.AtEnd,
SuggestContext.Visible.toNegated(),
SuggestAlternatives.OtherSuggestions.toNegated(),
SnippetController2.InSnippetMode.toNegated()
),
handler: x => x.triggerSuggestAndAcceptBest('\t'),//todo@joh fallback/default configurable?
kbOpts: {
weight,
primary: KeyCode.Tab
}
}));
registerEditorCommand(new SuggestCommand({
id: 'insertNextSuggestion',
precondition: ContextKeyExpr.and(
ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),
SuggestAlternatives.OtherSuggestions,
SuggestContext.Visible.toNegated(),
SnippetController2.InSnippetMode.toNegated()
),
handler: x => x.acceptNextSuggestion(),
kbOpts: {
weight: weight,
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyCode.Tab
}
}));
registerEditorCommand(new SuggestCommand({
id: 'insertPrevSuggestion',
precondition: ContextKeyExpr.and(
ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),
SuggestAlternatives.OtherSuggestions,
SuggestContext.Visible.toNegated(),
SnippetController2.InSnippetMode.toNegated()
),
handler: x => x.acceptPrevSuggestion(),
kbOpts: {
weight: weight,
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyMod.Shift | KeyCode.Tab
}
}));

View File

@@ -2,13 +2,15 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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';
export abstract class Memory {
@@ -55,7 +57,7 @@ export class NoMemory extends Memory {
}
export interface MemItem {
type: string;
type: string | CompletionItemKind;
insertText: string;
touch: number;
}
@@ -70,7 +72,7 @@ export class LRUMemory extends Memory {
const key = `${model.getLanguageIdentifier().language}/${label}`;
this._cache.set(key, {
touch: this._seq++,
type: item.suggestion.type,
type: item.suggestion.kind,
insertText: item.suggestion.insertText
});
}
@@ -94,7 +96,7 @@ export class LRUMemory extends Memory {
const { suggestion } = items[i];
const key = `${model.getLanguageIdentifier().language}/${suggestion.label}`;
const item = this._cache.get(key);
if (item && item.touch > seq && item.type === suggestion.type && item.insertText === suggestion.insertText) {
if (item && item.touch > seq && item.type === suggestion.kind && item.insertText === suggestion.insertText) {
seq = item.touch;
res = i;
}
@@ -119,6 +121,7 @@ export class LRUMemory extends Memory {
let seq = 0;
for (const [key, value] of data) {
value.touch = seq;
value.type = typeof value.type === 'number' ? value.type : completionKindFromLegacyString(value.type);
this._cache.set(key, value);
}
this._seq = this._cache.size;
@@ -135,7 +138,7 @@ export class PrefixMemory extends Memory {
const { word } = model.getWordUntilPosition(pos);
const key = `${model.getLanguageIdentifier().language}/${word}`;
this._trie.set(key, {
type: item.suggestion.type,
type: item.suggestion.kind,
insertText: item.suggestion.insertText,
touch: this._seq++
});
@@ -153,8 +156,8 @@ export class PrefixMemory extends Memory {
}
if (item) {
for (let i = 0; i < items.length; i++) {
let { type, insertText } = items[i].suggestion;
if (type === item.type && insertText === item.insertText) {
let { kind, insertText } = items[i].suggestion;
if (kind === item.type && insertText === item.insertText) {
return i;
}
}
@@ -182,6 +185,7 @@ export class PrefixMemory extends Memory {
if (data.length > 0) {
this._seq = data[0][1].touch + 1;
for (const [key, value] of data) {
value.type = typeof value.type === 'number' ? value.type : completionKindFromLegacyString(value.type);
this._trie.set(key, value);
}
}
@@ -190,23 +194,27 @@ export class PrefixMemory extends Memory {
export type MemMode = 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix';
export class SuggestMemories {
export class SuggestMemories extends Disposable {
private readonly _storagePrefix = 'suggest/memories';
private _mode: MemMode;
private _strategy: Memory;
private _persistSoon: RunOnceScheduler;
private readonly _persistSoon: RunOnceScheduler;
constructor(
mode: MemMode,
@IStorageService private readonly _storageService: IStorageService
editor: ICodeEditor,
@IStorageService private readonly _storageService: IStorageService,
) {
this._persistSoon = new RunOnceScheduler(() => this._flush(), 3000);
this.setMode(mode);
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)));
}
setMode(mode: MemMode): void {
private _setMode(mode: MemMode, useGlobalStorageForSuggestions: boolean): void {
if (this._mode === mode) {
return;
}
@@ -214,7 +222,7 @@ export class SuggestMemories {
this._strategy = mode === 'recentlyUsedByPrefix' ? new PrefixMemory() : mode === 'recentlyUsed' ? new LRUMemory() : new NoMemory();
try {
const raw = this._storageService.get(`${this._storagePrefix}/${this._mode}`, StorageScope.WORKSPACE);
const raw = useGlobalStorageForSuggestions ? this._storageService.get(`${this._storagePrefix}/${this._mode}`, StorageScope.GLOBAL) : this._storageService.get(`${this._storagePrefix}/${this._mode}`, StorageScope.WORKSPACE);
if (raw) {
this._strategy.fromJSON(JSON.parse(raw));
}
@@ -232,8 +240,8 @@ export class SuggestMemories {
return this._strategy.select(model, pos, items);
}
private _flush() {
private _saveState(useGlobalStorageForSuggestions: boolean) {
const raw = JSON.stringify(this._strategy);
this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, StorageScope.WORKSPACE);
this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, useGlobalStorageForSuggestions ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
}
}

View File

@@ -2,10 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { TimeoutTimer } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
@@ -15,10 +14,13 @@ import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/comm
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { ITextModel, IWordAtPosition } from 'vs/editor/common/model';
import { ISuggestSupport, StandardTokenType, SuggestContext, SuggestRegistry, SuggestTriggerKind } from 'vs/editor/common/modes';
import { CompletionItemProvider, StandardTokenType, CompletionContext, CompletionProviderRegistry, CompletionTriggerKind } from 'vs/editor/common/modes';
import { CompletionModel } from './completionModel';
import { ISuggestionItem, 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';
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
export interface ICancelEvent {
readonly retrigger: boolean;
@@ -26,16 +28,19 @@ export interface ICancelEvent {
export interface ITriggerEvent {
readonly auto: boolean;
readonly shy: boolean;
}
export interface ISuggestEvent {
readonly completionModel: CompletionModel;
readonly isFrozen: boolean;
readonly auto: boolean;
readonly shy: boolean;
}
export interface SuggestTriggerContext {
readonly auto: boolean;
readonly shy?: boolean;
readonly triggerCharacter?: string;
}
@@ -67,13 +72,15 @@ export class LineContext {
readonly leadingLineContent: string;
readonly leadingWord: IWordAtPosition;
readonly auto: boolean;
readonly shy: boolean;
constructor(model: ITextModel, position: Position, auto: boolean) {
constructor(model: ITextModel, position: Position, auto: boolean, shy: boolean) {
this.leadingLineContent = model.getLineContent(position.lineNumber).substr(0, position.column - 1);
this.leadingWord = model.getWordUntilPosition(position);
this.lineNumber = position.lineNumber;
this.column = position.column;
this.auto = auto;
this.shy = shy;
}
}
@@ -85,15 +92,14 @@ export const enum State {
export class SuggestModel implements IDisposable {
private _editor: ICodeEditor;
private _toDispose: IDisposable[] = [];
private _quickSuggestDelay: number;
private _triggerCharacterListener: IDisposable;
private readonly _triggerQuickSuggest = new TimeoutTimer();
private readonly _triggerRefilter = new TimeoutTimer();
private _state: State;
private _state: State = State.Idle;
private _requestPromise: CancelablePromise<ISuggestionItem[]>;
private _requestToken: CancellationTokenSource;
private _context: LineContext;
private _currentSelection: Selection;
@@ -106,10 +112,10 @@ export class SuggestModel implements IDisposable {
readonly onDidTrigger: Event<ITriggerEvent> = this._onDidTrigger.event;
readonly onDidSuggest: Event<ISuggestEvent> = this._onDidSuggest.event;
constructor(editor: ICodeEditor) {
this._editor = editor;
this._state = State.Idle;
this._requestPromise = null;
constructor(
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);
@@ -127,16 +133,31 @@ export class SuggestModel implements IDisposable {
this._updateTriggerCharacters();
this._updateQuickSuggest();
}));
this._toDispose.push(SuggestRegistry.onDidChange(() => {
this._toDispose.push(CompletionProviderRegistry.onDidChange(() => {
this._updateTriggerCharacters();
this._updateActiveSuggestSession();
}));
this._toDispose.push(this._editor.onDidChangeCursorSelection(e => {
this._onCursorChange(e);
}));
this._toDispose.push(this._editor.onDidChangeModelContent(e => {
let editorIsComposing = false;
this._toDispose.push(this._editor.onCompositionStart(() => {
editorIsComposing = true;
}));
this._toDispose.push(this._editor.onCompositionEnd(() => {
// refilter when composition ends
editorIsComposing = false;
this._refilterCompletionItems();
}));
this._toDispose.push(this._editor.onDidChangeModelContent(() => {
// only filter completions when the editor isn't
// composing a character, e.g. ¨ + u makes ü but just
// ¨ cannot be used for filtering
if (!editorIsComposing) {
this._refilterCompletionItems();
}
}));
this._updateTriggerCharacters();
this._updateQuickSuggest();
@@ -170,12 +191,9 @@ export class SuggestModel implements IDisposable {
return;
}
const supportsByTriggerCharacter: { [ch: string]: Set<ISuggestSupport> } = Object.create(null);
for (const support of SuggestRegistry.all(this._editor.getModel())) {
if (isFalsyOrEmpty(support.triggerCharacters)) {
continue;
}
for (const ch of support.triggerCharacters) {
const supportsByTriggerCharacter: { [ch: string]: Set<CompletionItemProvider> } = Object.create(null);
for (const support of CompletionProviderRegistry.all(this._editor.getModel())) {
for (const ch of support.triggerCharacters || []) {
let set = supportsByTriggerCharacter[ch];
if (!set) {
set = supportsByTriggerCharacter[ch] = new Set();
@@ -210,12 +228,10 @@ export class SuggestModel implements IDisposable {
if (this._triggerQuickSuggest) {
this._triggerQuickSuggest.cancel();
}
if (this._requestPromise) {
this._requestPromise.cancel();
this._requestPromise = null;
if (this._requestToken) {
this._requestToken.cancel();
}
this._state = State.Idle;
@@ -228,7 +244,7 @@ export class SuggestModel implements IDisposable {
private _updateActiveSuggestSession(): void {
if (this._state !== State.Idle) {
if (!SuggestRegistry.has(this._editor.getModel())) {
if (!CompletionProviderRegistry.has(this._editor.getModel())) {
this.cancel();
} else {
this.trigger({ auto: this._state === State.Auto }, true);
@@ -253,7 +269,7 @@ export class SuggestModel implements IDisposable {
return;
}
if (!SuggestRegistry.has(this._editor.getModel())) {
if (!CompletionProviderRegistry.has(this._editor.getModel())) {
return;
}
@@ -328,13 +344,13 @@ export class SuggestModel implements IDisposable {
// refine active suggestion
this._triggerRefilter.cancelAndSet(() => {
const position = this._editor.getPosition();
const ctx = new LineContext(model, position, this._state === State.Auto);
const ctx = new LineContext(model, position, this._state === State.Auto, false);
this._onNewContext(ctx);
}, 25);
}
}
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: ISuggestSupport[], existingItems?: ISuggestionItem[]): void {
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: CompletionItemProvider[], existingItems?: ISuggestionItem[]): void {
const model = this._editor.getModel();
@@ -342,41 +358,48 @@ export class SuggestModel implements IDisposable {
return;
}
const auto = context.auto;
const ctx = new LineContext(model, this._editor.getPosition(), auto);
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
// Cancel previous requests, change state & update UI
this.cancel(retrigger);
this._state = auto ? State.Auto : State.Manual;
this._onDidTrigger.fire({ auto });
this._onDidTrigger.fire({ auto, shy: context.shy });
// Capture context when request was sent
this._context = ctx;
// Build context for request
let suggestCtx: SuggestContext;
let suggestCtx: CompletionContext;
if (context.triggerCharacter) {
suggestCtx = {
triggerKind: SuggestTriggerKind.TriggerCharacter,
triggerKind: CompletionTriggerKind.TriggerCharacter,
triggerCharacter: context.triggerCharacter
};
} else if (onlyFrom && onlyFrom.length) {
suggestCtx = { triggerKind: SuggestTriggerKind.TriggerForIncompleteCompletions };
suggestCtx = { triggerKind: CompletionTriggerKind.TriggerForIncompleteCompletions };
} else {
suggestCtx = { triggerKind: SuggestTriggerKind.Invoke };
suggestCtx = { triggerKind: CompletionTriggerKind.Invoke };
}
this._requestPromise = createCancelablePromise(token => provideSuggestionItems(
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(
model,
this._editor.getPosition(),
this._editor.getConfiguration().contribInfo.suggest.snippets,
onlyFrom,
suggestCtx,
token
));
this._requestToken.token
);
this._requestPromise.then(items => {
Promise.all([items, wordDistance]).then(([items, wordDistance]) => {
this._requestToken.dispose();
this._requestPromise = null;
if (this._state === State.Idle) {
return;
}
@@ -385,17 +408,18 @@ export class SuggestModel implements IDisposable {
return;
}
if (!isFalsyOrEmpty(existingItems)) {
if (isNonEmptyArray(existingItems)) {
const cmpFn = getSuggestionComparator(this._editor.getConfiguration().contribInfo.suggest.snippets);
items = items.concat(existingItems).sort(cmpFn);
}
const ctx = new LineContext(model, this._editor.getPosition(), auto);
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
dispose(this._completionModel);
this._completionModel = new CompletionModel(items, this._context.column, {
leadingLineContent: ctx.leadingLineContent,
characterCountDelta: this._context ? ctx.column - this._context.column : 0
},
wordDistance,
this._editor.getConfiguration().contribInfo.suggest
);
this._onNewContext(ctx);
@@ -483,6 +507,7 @@ export class SuggestModel implements IDisposable {
this._onDidSuggest.fire({
completionModel: this._completionModel,
auto: this._context.auto,
shy: this._context.shy,
isFrozen,
});
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/suggest';
import * as nls from 'vs/nls';
import { createMatches } from 'vs/base/common/filters';
@@ -13,8 +11,7 @@ import { Event, Emitter, chain } 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 { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IVirtualDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IListEvent, IListRenderer } 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';
@@ -34,8 +31,14 @@ import { IModeService } from 'vs/editor/common/services/modeService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { CompletionItemKind, completionKindToCssClass } from 'vs/editor/common/modes';
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { IModelService } from 'vs/editor/common/services/modelService';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FileKind } from 'vs/platform/files/common/files';
const sticky = false; // for development purposes
const expandSuggestionDocsByDefault = false;
const maxSuggestionsToShow = 12;
@@ -43,7 +46,7 @@ interface ISuggestionTemplateData {
root: HTMLElement;
icon: HTMLElement;
colorspan: HTMLElement;
highlightedLabel: HighlightedLabel;
iconLabel: IconLabel;
typeLabel: HTMLElement;
readMore: HTMLElement;
disposables: IDisposable[];
@@ -75,12 +78,15 @@ function canExpandCompletionItem(item: ICompletionItem) {
return (suggestion.detail && suggestion.detail !== suggestion.label);
}
class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData> {
constructor(
private widget: SuggestWidget,
private editor: ICodeEditor,
private triggerKeybindingLabel: string
private triggerKeybindingLabel: string,
@IModelService private readonly _modelService: IModelService,
@IModeService private readonly _modeService: IModeService,
@IThemeService private readonly _themeService: IThemeService,
) {
}
@@ -93,14 +99,17 @@ class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
const data = <ISuggestionTemplateData>Object.create(null);
data.disposables = [];
data.root = container;
addClass(data.root, 'show-file-icons');
data.icon = append(container, $('.icon'));
data.colorspan = append(data.icon, $('span.colorspan'));
const text = append(container, $('.contents'));
const main = append(text, $('.main'));
data.highlightedLabel = new HighlightedLabel(main);
data.disposables.push(data.highlightedLabel);
data.iconLabel = new IconLabel(main, { supportHighlights: true });
data.disposables.push(data.iconLabel);
data.typeLabel = append(main, $('span.type-label'));
data.readMore = append(main, $('span.readMore'));
@@ -111,10 +120,12 @@ class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
const fontFamily = configuration.fontInfo.fontFamily;
const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize;
const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
const fontWeight = configuration.fontInfo.fontWeight;
const fontSizePx = `${fontSize}px`;
const lineHeightPx = `${lineHeight}px`;
data.root.style.fontSize = fontSizePx;
data.root.style.fontWeight = fontWeight;
main.style.fontFamily = fontFamily;
main.style.lineHeight = lineHeightPx;
data.icon.style.height = lineHeightPx;
@@ -132,29 +143,49 @@ class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
return data;
}
renderElement(element: ICompletionItem, index: number, templateData: ISuggestionTemplateData): void {
renderElement(element: ICompletionItem, _index: number, templateData: ISuggestionTemplateData): void {
const data = <ISuggestionTemplateData>templateData;
const suggestion = (<ICompletionItem>element).suggestion;
if (canExpandCompletionItem(element)) {
data.root.setAttribute('aria-label', nls.localize('suggestionWithDetailsAriaLabel', "{0}, suggestion, has details", suggestion.label));
} else {
data.root.setAttribute('aria-label', nls.localize('suggestionAriaLabel', "{0}, suggestion", suggestion.label));
}
data.icon.className = 'icon ' + suggestion.type;
data.icon.className = 'icon ' + completionKindToCssClass(suggestion.kind);
data.colorspan.style.backgroundColor = '';
if (suggestion.type === 'color') {
let color = matchesColor(suggestion.label) || typeof suggestion.documentation === 'string' && matchesColor(suggestion.documentation);
if (color) {
data.icon.className = 'icon customcolor';
data.colorspan.style.backgroundColor = color;
}
const labelOptions: IIconLabelValueOptions = {
labelEscapeNewLines: true,
matches: createMatches(element.matches)
};
let color: string;
if (suggestion.kind === CompletionItemKind.Color && (color = matchesColor(suggestion.label) || typeof suggestion.documentation === 'string' && matchesColor(suggestion.documentation))) {
// special logic for 'color' completion items
data.icon.className = 'icon customcolor';
data.colorspan.style.backgroundColor = color;
} else if (suggestion.kind === CompletionItemKind.File && this._themeService.getIconTheme().hasFileIcons) {
// special logic for 'file' completion items
data.icon.className = 'icon hide';
labelOptions.extraClasses = [].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)
);
} else if (suggestion.kind === CompletionItemKind.Folder && this._themeService.getIconTheme().hasFolderIcons) {
// special logic for 'folder' completion items
data.icon.className = 'icon hide';
labelOptions.extraClasses = [].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)
);
} else {
// normal icon
data.icon.className = 'icon hide';
labelOptions.extraClasses = [
`suggest-icon ${completionKindToCssClass(suggestion.kind)}`
];
}
data.highlightedLabel.set(suggestion.label, createMatches(element.matches), '', true);
// data.highlightedLabel.set(`${suggestion.label} <${element.score}=score(${element.word}, ${suggestion.filterText || suggestion.label})>`, createMatches(element.matches));
data.iconLabel.setValue(suggestion.label, undefined, labelOptions);
data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, '');
if (canExpandCompletionItem(element)) {
@@ -181,8 +212,8 @@ class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
disposeTemplate(templateData: ISuggestionTemplateData): void {
// {{SQL CARBON EDIT}}
if (templateData.highlightedLabel) {
templateData.highlightedLabel.dispose();
if (templateData.iconLabel) {
templateData.iconLabel.dispose();
}
// {{SQL CARBON EDIT}}
if (templateData.disposables) {
@@ -298,7 +329,10 @@ class SuggestionDetails {
this.body.scrollTop = 0;
this.scrollbar.scanDomNode();
this.ariaLabel = strings.format('{0}\n{1}\n{2}', item.suggestion.label || '', item.suggestion.detail || '', item.suggestion.documentation || '');
this.ariaLabel = strings.format(
'{0}{1}',
item.suggestion.detail || '',
item.suggestion.documentation ? (typeof item.suggestion.documentation === 'string' ? item.suggestion.documentation : item.suggestion.documentation.value) : '');
}
getAriaLabel(): string {
@@ -338,10 +372,12 @@ class SuggestionDetails {
const fontFamily = configuration.fontInfo.fontFamily;
const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize;
const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
const fontWeight = configuration.fontInfo.fontWeight;
const fontSizePx = `${fontSize}px`;
const lineHeightPx = `${lineHeight}px`;
this.el.style.fontSize = fontSizePx;
this.el.style.fontWeight = fontWeight;
this.type.style.fontFamily = fontFamily;
this.close.style.height = lineHeightPx;
this.close.style.width = lineHeightPx;
@@ -359,7 +395,7 @@ export interface ISelectedSuggestion {
model: CompletionModel;
}
export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompletionItem>, IDisposable {
export class SuggestWidget implements IContentWidget, IListVirtualDelegate<ICompletionItem>, IDisposable {
private static readonly ID: string = 'editor.widget.suggestWidget';
@@ -371,7 +407,7 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
private state: State;
private isAuto: boolean;
private loadingTimeout: number;
private loadingTimeout: any;
private currentSuggestionDetails: CancelablePromise<void>;
private focusedItem: ICompletionItem;
private ignoreFocusEvents = false;
@@ -386,7 +422,6 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
private suggestWidgetVisible: IContextKey<boolean>;
private suggestWidgetMultipleSuggestions: IContextKey<boolean>;
private suggestionSupportsAutoAccept: IContextKey<boolean>;
private readonly editorBlurTimeout = new TimeoutTimer();
private readonly showTimeout = new TimeoutTimer();
@@ -409,11 +444,11 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
private detailsFocusBorderColor: string;
private detailsBorderColor: string;
private storageServiceAvailable: boolean = true;
private expandSuggestionDocs: boolean = false;
private firstFocusInCurrentList: boolean = false;
private preferDocPositionTop: boolean = false;
private docsPositionPreviousWidgetY: number;
constructor(
private editor: ICodeEditor,
@ITelemetryService private telemetryService: ITelemetryService,
@@ -422,7 +457,8 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
@IStorageService storageService: IStorageService,
@IKeybindingService keybindingService: IKeybindingService,
@IModeService modeService: IModeService,
@IOpenerService openerService: IOpenerService
@IOpenerService openerService: IOpenerService,
@IInstantiationService instantiationService: IInstantiationService,
) {
const kb = keybindingService.lookupKeybinding('editor.action.triggerSuggest');
const triggerKeybindingLabel = !kb ? '' : ` (${kb.getLabel()})`;
@@ -432,13 +468,6 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.focusedItem = null;
this.storageService = storageService;
if (this.expandDocsSettingFromStorage() === undefined) {
this.storageService.store('expandSuggestionDocs', expandSuggestionDocsByDefault, StorageScope.GLOBAL);
if (this.expandDocsSettingFromStorage() === undefined) {
this.storageServiceAvailable = false;
}
}
this.element = $('.editor-widget.suggest-widget');
if (!this.editor.getConfiguration().contribInfo.iconsInSuggestions) {
addClass(this.element, 'no-icons');
@@ -448,7 +477,7 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.listElement = append(this.element, $('.tree'));
this.details = new SuggestionDetails(this.element, this, this.editor, markdownRenderer, triggerKeybindingLabel);
let renderer = new Renderer(this, this.editor, triggerKeybindingLabel);
let renderer = instantiationService.createInstance(Renderer, this, this.editor, triggerKeybindingLabel);
this.list = new List(this.listElement, this, [renderer], {
useShadows: false,
@@ -463,7 +492,6 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
listInactiveFocusOutline: activeContrastBorder
}),
themeService.onThemeChange(t => this.onThemeChange(t)),
editor.onDidBlurEditorText(() => this.onEditorBlur()),
editor.onDidLayoutChange(() => this.onEditorLayoutChange()),
this.list.onSelectionChange(e => this.onListSelection(e)),
this.list.onFocusChange(e => this.onListFocus(e)),
@@ -472,7 +500,6 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.suggestWidgetVisible = SuggestContext.Visible.bindTo(contextKeyService);
this.suggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(contextKeyService);
this.suggestionSupportsAutoAccept = SuggestContext.AcceptOnKey.bindTo(contextKeyService);
this.editor.addContentWidget(this);
this.setState(State.Hidden);
@@ -488,18 +515,6 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.editor.layoutContentWidget(this);
}
private onEditorBlur(): void {
if (sticky) {
return;
}
this.editorBlurTimeout.cancelAndSet(() => {
if (!this.editor.hasTextFocus()) {
this.setState(State.Hidden);
}
}, 150);
}
private onEditorLayoutChange(): void {
if ((this.state === State.Open || this.state === State.Details) && this.expandDocsSettingFromStorage()) {
this.expandSideOrBelow();
@@ -521,10 +536,17 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
}
private _getSuggestionAriaAlertLabel(item: ICompletionItem): string {
if (canExpandCompletionItem(item)) {
return nls.localize('ariaCurrentSuggestionWithDetails', "{0}, suggestion, has details", item.suggestion.label);
const isSnippet = item.suggestion.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);
} 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());
} else {
return nls.localize('ariaCurrentSuggestion', "{0}, suggestion", item.suggestion.label);
return isSnippet ? nls.localize('ariaCurrentSnippetSuggestionWithDetails', "{0}, snippet suggestion, has details", item.suggestion.label)
: nls.localize('ariaCurrentSuggestionWithDetails', "{0}, suggestion, has details", item.suggestion.label);
}
}
@@ -540,20 +562,20 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
}
private onThemeChange(theme: ITheme) {
let backgroundColor = theme.getColor(editorSuggestWidgetBackground);
const backgroundColor = theme.getColor(editorSuggestWidgetBackground);
if (backgroundColor) {
this.listElement.style.backgroundColor = backgroundColor.toString();
this.details.element.style.backgroundColor = backgroundColor.toString();
this.messageElement.style.backgroundColor = backgroundColor.toString();
}
let borderColor = theme.getColor(editorSuggestWidgetBorder);
const borderColor = theme.getColor(editorSuggestWidgetBorder);
if (borderColor) {
this.listElement.style.borderColor = borderColor.toString();
this.details.element.style.borderColor = borderColor.toString();
this.messageElement.style.borderColor = borderColor.toString();
this.detailsBorderColor = borderColor.toString();
}
let focusBorderColor = theme.getColor(focusBorder);
const focusBorderColor = theme.getColor(focusBorder);
if (focusBorderColor) {
this.detailsFocusBorderColor = focusBorderColor.toString();
}
@@ -577,45 +599,47 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
}
const item = e.elements[0];
this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
this.firstFocusInCurrentList = !this.focusedItem;
if (item === this.focusedItem) {
return;
}
if (this.currentSuggestionDetails) {
this.currentSuggestionDetails.cancel();
this.currentSuggestionDetails = null;
}
const index = e.indexes[0];
this.suggestionSupportsAutoAccept.set(!item.suggestion.noAutoAccept);
this.firstFocusInCurrentList = !this.focusedItem;
if (item !== this.focusedItem) {
this.focusedItem = item;
this.list.reveal(index);
this.currentSuggestionDetails = createCancelablePromise(token => item.resolve(token));
this.currentSuggestionDetails.then(() => {
// item can have extra information, so re-render
this.ignoreFocusEvents = true;
this.list.splice(index, 1, [item]);
this.list.setFocus([index]);
this.ignoreFocusEvents = false;
if (this.expandDocsSettingFromStorage()) {
this.showDetails();
} else {
removeClass(this.element, 'docs-side');
}
}).catch(onUnexpectedError).then(() => {
if (this.focusedItem === item) {
if (this.currentSuggestionDetails) {
this.currentSuggestionDetails.cancel();
this.currentSuggestionDetails = null;
}
});
this.focusedItem = item;
this.list.reveal(index);
this.currentSuggestionDetails = createCancelablePromise(token => item.resolve(token));
this.currentSuggestionDetails.then(() => {
if (this.list.length < index) {
return;
}
// item can have extra information, so re-render
this.ignoreFocusEvents = true;
this.list.splice(index, 1, [item]);
this.list.setFocus([index]);
this.ignoreFocusEvents = false;
if (this.expandDocsSettingFromStorage()) {
this.showDetails();
} else {
removeClass(this.element, 'docs-side');
}
this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
}).catch(onUnexpectedError).then(() => {
if (this.focusedItem === item) {
this.currentSuggestionDetails = null;
}
});
}
// emit an event
this.onDidFocusEmitter.fire({ item, index, model: this.completionModel });
@@ -676,7 +700,7 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
}
}
showTriggered(auto: boolean) {
showTriggered(auto: boolean, delay: number) {
if (this.state !== State.Hidden) {
return;
}
@@ -687,16 +711,24 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.loadingTimeout = setTimeout(() => {
this.loadingTimeout = null;
this.setState(State.Loading);
}, 50);
}, delay);
}
}
showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean): void {
this.preferDocPositionTop = false;
this.docsPositionPreviousWidgetY = null;
if (this.loadingTimeout) {
clearTimeout(this.loadingTimeout);
this.loadingTimeout = null;
}
if (this.currentSuggestionDetails) {
this.currentSuggestionDetails.cancel();
this.currentSuggestionDetails = null;
}
if (this.completionModel !== completionModel) {
this.completionModel = completionModel;
}
@@ -721,19 +753,23 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.completionModel = null;
} else {
const { stats } = this.completionModel;
stats['wasAutomaticallyTriggered'] = !!isAuto;
/* __GDPR__
"suggestWidget" : {
"wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${ICompletionStats}",
"${EditorTelemetryData}"
]
}
*/
this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() });
if (this.state !== State.Open) {
const { stats } = this.completionModel;
stats['wasAutomaticallyTriggered'] = !!isAuto;
/* __GDPR__
"suggestWidget" : {
"wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${ICompletionStats}",
"${EditorTelemetryData}"
]
}
*/
this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() });
}
this.focusedItem = null;
this.list.splice(0, this.list.length, this.completionModel.items);
if (isFrozen) {
@@ -742,7 +778,7 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.setState(State.Open);
}
this.list.reveal(selectionIndex, selectionIndex);
this.list.reveal(selectionIndex, 0);
this.list.setFocus([selectionIndex]);
// Reset focus border
@@ -898,6 +934,7 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.updateExpandDocsSetting(true);
this.showDetails();
this._ariaAlert(this.details.getAriaLabel());
/* __GDPR__
"suggestWidget:expandDetails" : {
"${include}": [
@@ -926,8 +963,6 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
this.adjustDocsPosition();
this.editor.focus();
this._ariaAlert(this.details.getAriaLabel());
}
private show(): void {
@@ -962,9 +997,14 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
return null;
}
let preference = [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE];
if (this.preferDocPositionTop) {
preference = [ContentWidgetPositionPreference.ABOVE];
}
return {
position: this.editor.getPosition(),
preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]
preference: preference
};
}
@@ -992,6 +1032,9 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
return height;
}
/**
* Adds the propert classes, margins when positioning the docs to the side
*/
private adjustDocsPosition() {
const lineHeight = this.editor.getConfiguration().fontInfo.lineHeight;
const cursorCoords = this.editor.getScrolledVisiblePosition(this.editor.getPosition());
@@ -1002,6 +1045,17 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
const widgetX = widgetCoords.left;
const widgetY = widgetCoords.top;
// Fixes #27649
// Check if the Y changed to the top of the cursor and keep the widget flagged to prefer top
if (this.docsPositionPreviousWidgetY &&
this.docsPositionPreviousWidgetY < widgetY &&
!this.preferDocPositionTop) {
this.preferDocPositionTop = true;
this.adjustDocsPosition();
return;
}
this.docsPositionPreviousWidgetY = widgetY;
if (widgetX < cursorX - this.listWidth) {
// Widget is too far to the left of cursor, swap list and docs
addClass(this.element, 'list-right');
@@ -1022,6 +1076,9 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
}
}
/**
* Adds the proper classes for positioning the docs to the side or below
*/
private expandSideOrBelow() {
if (!canExpandCompletionItem(this.focusedItem) && this.firstFocusInCurrentList) {
removeClass(this.element, 'docs-side');
@@ -1060,27 +1117,16 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
return 'suggestion';
}
// Monaco Editor does not have a storage service
private expandDocsSettingFromStorage(): boolean {
if (this.storageServiceAvailable) {
return this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL);
} else {
return this.expandSuggestionDocs;
}
return this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault);
}
// Monaco Editor does not have a storage service
private updateExpandDocsSetting(value: boolean) {
if (this.storageServiceAvailable) {
this.storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL);
} else {
this.expandSuggestionDocs = value;
}
this.storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL);
}
dispose(): void {
this.state = null;
this.suggestionSupportsAutoAccept = null;
this.currentSuggestionDetails = null;
this.focusedItem = null;
this.element = null;
@@ -1102,11 +1148,11 @@ export class SuggestWidget implements IContentWidget, IVirtualDelegate<ICompleti
}
registerThemingParticipant((theme, collector) => {
let matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground);
const matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground);
if (matchHighlight) {
collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`);
}
let foreground = theme.getColor(editorSuggestWidgetForeground);
const foreground = theme.getColor(editorSuggestWidgetForeground);
if (foreground) {
collector.addRule(`.monaco-editor .suggest-widget { color: ${foreground}; }`);
}
@@ -1116,7 +1162,7 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(`.monaco-editor .suggest-widget a { color: ${link}; }`);
}
let codeBackground = theme.getColor(textCodeBlockBackground);
const codeBackground = theme.getColor(textCodeBlockBackground);
if (codeBackground) {
collector.addRule(`.monaco-editor .suggest-widget code { background-color: ${codeBackground}; }`);
}

View File

@@ -2,40 +2,38 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { TPromise } from 'vs/base/common/winjs.base';
import { IPosition } from 'vs/editor/common/core/position';
import { ISuggestResult, ISuggestSupport, ISuggestion, SuggestionType } from 'vs/editor/common/modes';
import { CompletionList, CompletionItemProvider, CompletionItem, CompletionItemKind } from 'vs/editor/common/modes';
import { CompletionModel } from 'vs/editor/contrib/suggest/completionModel';
import { ISuggestionItem, getSuggestionComparator } from 'vs/editor/contrib/suggest/suggest';
import { ISuggestionItem, getSuggestionComparator, ensureLowerCaseVariants } from 'vs/editor/contrib/suggest/suggest';
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
export function createSuggestItem(label: string, overwriteBefore: number, type: SuggestionType = 'property', incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }): ISuggestionItem {
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: ISuggestion = {
suggestion: CompletionItem = {
label,
overwriteBefore,
range: { startLineNumber: position.lineNumber, startColumn: position.column - overwriteBefore, endLineNumber: position.lineNumber, endColumn: position.column },
insertText: label,
type
kind
};
container: ISuggestResult = {
container: CompletionList = {
incomplete,
suggestions: [this.suggestion]
};
support: ISuggestSupport = {
support: CompletionItemProvider = {
provideCompletionItems(): any {
return;
}
};
resolve(): TPromise<void> {
resolve(): Promise<void> {
return null;
}
};
@@ -54,7 +52,7 @@ suite('CompletionModel', function () {
], 1, {
leadingLineContent: 'foo',
characterCountDelta: 0
});
}, WordDistance.None);
});
test('filtering - cached', function () {
@@ -75,7 +73,7 @@ suite('CompletionModel', function () {
});
test('complete/incomplete', function () {
test('complete/incomplete', () => {
assert.equal(model.incomplete.size, 0);
@@ -85,16 +83,16 @@ suite('CompletionModel', function () {
], 1, {
leadingLineContent: 'foo',
characterCountDelta: 0
});
}, WordDistance.None);
assert.equal(incompleteModel.incomplete.size, 1);
});
test('replaceIncomplete', function () {
test('replaceIncomplete', () => {
const completeItem = createSuggestItem('foobar', 1, undefined, false, { lineNumber: 1, column: 2 });
const incompleteItem = createSuggestItem('foofoo', 1, undefined, true, { lineNumber: 1, column: 2 });
const model = new CompletionModel([completeItem, incompleteItem], 2, { leadingLineContent: 'f', characterCountDelta: 0 });
const model = new CompletionModel([completeItem, incompleteItem], 2, { leadingLineContent: 'f', characterCountDelta: 0 }, WordDistance.None);
assert.equal(model.incomplete.size, 1);
assert.equal(model.items.length, 2);
@@ -123,7 +121,7 @@ suite('CompletionModel', function () {
completeItem4,
completeItem5,
incompleteItem1,
], 2, { leadingLineContent: 'f', characterCountDelta: 0 }
], 2, { leadingLineContent: 'f', characterCountDelta: 0 }, WordDistance.None
);
assert.equal(model.incomplete.size, 1);
assert.equal(model.items.length, 6);
@@ -147,7 +145,7 @@ suite('CompletionModel', function () {
], 1, {
leadingLineContent: ' <',
characterCountDelta: 0
});
}, WordDistance.None);
assert.equal(model.items.length, 4);
@@ -161,13 +159,13 @@ suite('CompletionModel', function () {
test('keep snippet sorting with prefix: top, #25495', function () {
model = new CompletionModel([
createSuggestItem('Snippet1', 1, 'snippet'),
createSuggestItem('tnippet2', 1, 'snippet'),
createSuggestItem('semver', 1, 'property'),
createSuggestItem('Snippet1', 1, CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, CompletionItemKind.Snippet),
createSuggestItem('semver', 1, CompletionItemKind.Property),
], 1, {
leadingLineContent: 's',
characterCountDelta: 0
}, { snippets: 'top', snippetsPreventQuickSuggestions: true, filterGraceful: true });
}, WordDistance.None, { snippets: 'top', snippetsPreventQuickSuggestions: true, filterGraceful: true, localityBonus: false, shareSuggestSelections: false });
assert.equal(model.items.length, 2);
const [a, b] = model.items;
@@ -180,13 +178,13 @@ suite('CompletionModel', function () {
test('keep snippet sorting with prefix: bottom, #25495', function () {
model = new CompletionModel([
createSuggestItem('snippet1', 1, 'snippet'),
createSuggestItem('tnippet2', 1, 'snippet'),
createSuggestItem('Semver', 1, 'property'),
createSuggestItem('snippet1', 1, CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, CompletionItemKind.Snippet),
createSuggestItem('Semver', 1, CompletionItemKind.Property),
], 1, {
leadingLineContent: 's',
characterCountDelta: 0
}, { snippets: 'bottom', snippetsPreventQuickSuggestions: true, filterGraceful: true });
}, WordDistance.None, { snippets: 'bottom', snippetsPreventQuickSuggestions: true, filterGraceful: true, localityBonus: false, shareSuggestSelections: false });
assert.equal(model.items.length, 2);
const [a, b] = model.items;
@@ -198,13 +196,13 @@ suite('CompletionModel', function () {
test('keep snippet sorting with prefix: inline, #25495', function () {
model = new CompletionModel([
createSuggestItem('snippet1', 1, 'snippet'),
createSuggestItem('tnippet2', 1, 'snippet'),
createSuggestItem('Semver', 1, 'property'),
createSuggestItem('snippet1', 1, CompletionItemKind.Snippet),
createSuggestItem('tnippet2', 1, CompletionItemKind.Snippet),
createSuggestItem('Semver', 1),
], 1, {
leadingLineContent: 's',
characterCountDelta: 0
}, { snippets: 'inline', snippetsPreventQuickSuggestions: true, filterGraceful: true });
}, WordDistance.None, { snippets: 'inline', snippetsPreventQuickSuggestions: true, filterGraceful: true, localityBonus: false, shareSuggestSelections: false });
assert.equal(model.items.length, 2);
const [a, b] = model.items;
@@ -215,14 +213,14 @@ suite('CompletionModel', function () {
test('filterText seems ignored in autocompletion, #26874', function () {
const item1 = createSuggestItem('Map - java.util', 1, 'property');
const item1 = createSuggestItem('Map - java.util', 1);
item1.suggestion.filterText = 'Map';
const item2 = createSuggestItem('Map - java.util', 1, 'property');
const item2 = createSuggestItem('Map - java.util', 1);
model = new CompletionModel([item1, item2], 1, {
leadingLineContent: 'M',
characterCountDelta: 0
});
}, WordDistance.None);
assert.equal(model.items.length, 2);
@@ -235,20 +233,23 @@ suite('CompletionModel', function () {
test('Vscode 1.12 no longer obeys \'sortText\' in completion items (from language server), #26096', function () {
const item1 = createSuggestItem('<- groups', 2, 'property', false, { lineNumber: 1, column: 3 });
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, 'property', false, { lineNumber: 1, column: 3 });
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 items = [item1, item2].sort(getSuggestionComparator('inline'));
model = new CompletionModel(items, 3, {
leadingLineContent: ' ',
characterCountDelta: 0
});
}, WordDistance.None);
assert.equal(model.items.length, 2);
@@ -259,15 +260,15 @@ suite('CompletionModel', function () {
test('Score only filtered items when typing more, score all when typing less', function () {
model = new CompletionModel([
createSuggestItem('console', 0, 'property'),
createSuggestItem('co_new', 0, 'property'),
createSuggestItem('bar', 0, 'property'),
createSuggestItem('car', 0, 'property'),
createSuggestItem('foo', 0, 'property'),
createSuggestItem('console', 0),
createSuggestItem('co_new', 0),
createSuggestItem('bar', 0),
createSuggestItem('car', 0),
createSuggestItem('foo', 0),
], 1, {
leadingLineContent: '',
characterCountDelta: 0
});
}, WordDistance.None);
assert.equal(model.items.length, 5);
@@ -286,15 +287,15 @@ suite('CompletionModel', function () {
test('Have more relaxed suggest matching algorithm #15419', function () {
model = new CompletionModel([
createSuggestItem('result', 0, 'property'),
createSuggestItem('replyToUser', 0, 'property'),
createSuggestItem('randomLolut', 0, 'property'),
createSuggestItem('car', 0, 'property'),
createSuggestItem('foo', 0, 'property'),
createSuggestItem('result', 0),
createSuggestItem('replyToUser', 0),
createSuggestItem('randomLolut', 0),
createSuggestItem('car', 0),
createSuggestItem('foo', 0),
], 1, {
leadingLineContent: '',
characterCountDelta: 0
});
}, WordDistance.None);
// query gets longer, narrow down the narrow-down'ed-set from before
model.lineContext = { leadingLineContent: 'rlut', characterCountDelta: 4 };
@@ -308,15 +309,15 @@ suite('CompletionModel', function () {
test('Emmet suggestion not appearing at the top of the list in jsx files, #39518', function () {
model = new CompletionModel([
createSuggestItem('from', 0, 'property'),
createSuggestItem('form', 0, 'property'),
createSuggestItem('form:get', 0, 'property'),
createSuggestItem('testForeignMeasure', 0, 'property'),
createSuggestItem('fooRoom', 0, 'property'),
createSuggestItem('from', 0),
createSuggestItem('form', 0),
createSuggestItem('form:get', 0),
createSuggestItem('testForeignMeasure', 0),
createSuggestItem('fooRoom', 0),
], 1, {
leadingLineContent: '',
characterCountDelta: 0
});
}, WordDistance.None);
model.lineContext = { leadingLineContent: 'form', characterCountDelta: 4 };
assert.equal(model.items.length, 5);

View File

@@ -2,12 +2,10 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
import { SuggestRegistry } from 'vs/editor/common/modes';
import { CompletionProviderRegistry, CompletionItemKind } from 'vs/editor/common/modes';
import { provideSuggestionItems } from 'vs/editor/contrib/suggest/suggest';
import { Position } from 'vs/editor/common/core/position';
import { TextModel } from 'vs/editor/common/model/textModel';
@@ -21,21 +19,21 @@ suite('Suggest', function () {
setup(function () {
model = TextModel.createFromString('FOO\nbar\BAR\nfoo', undefined, undefined, URI.parse('foo:bar/path'));
registration = SuggestRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, {
registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, {
provideCompletionItems() {
return {
incomplete: false,
suggestions: [{
label: 'aaa',
type: 'snippet',
kind: CompletionItemKind.Snippet,
insertText: 'aaa'
}, {
label: 'zzz',
type: 'snippet',
kind: CompletionItemKind.Snippet,
insertText: 'zzz'
}, {
label: 'fff',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'fff'
}]
};
@@ -48,38 +46,34 @@ suite('Suggest', function () {
model.dispose();
});
test('sort - snippet inline', function () {
return provideSuggestionItems(model, new Position(1, 1), 'inline').then(items => {
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');
});
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');
});
test('sort - snippet top', function () {
return provideSuggestionItems(model, new Position(1, 1), 'top').then(items => {
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');
});
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');
});
test('sort - snippet bottom', function () {
return provideSuggestionItems(model, new Position(1, 1), 'bottom').then(items => {
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');
});
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');
});
test('sort - snippet none', function () {
return provideSuggestionItems(model, new Position(1, 1), 'none').then(items => {
assert.equal(items.length, 1);
assert.equal(items[0].suggestion.label, 'fff');
});
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');
});
test('only from', function () {
@@ -98,7 +92,7 @@ suite('Suggest', function () {
};
}
};
const registration = SuggestRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, foo);
const registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, foo);
provideSuggestionItems(model, new Position(1, 1), undefined, [foo]).then(items => {
registration.dispose();

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { LRUMemory, NoMemory, PrefixMemory, Memory } from 'vs/editor/contrib/suggest/suggestMemory';
import { ITextModel } from 'vs/editor/common/model';
@@ -68,7 +66,7 @@ suite('SuggestMemories', function () {
assert.equal(new PrefixMemory().select(buffer, pos, items), 1);
});
test('NoMemory', function () {
test('NoMemory', () => {
const mem = new NoMemory();
@@ -79,7 +77,7 @@ suite('SuggestMemories', function () {
mem.memorize(buffer, pos, null);
});
test('LRUMemory', function () {
test('LRUMemory', () => {
pos = { lineNumber: 2, column: 6 };
@@ -116,7 +114,7 @@ suite('SuggestMemories', function () {
assert.equal(mem.select(buffer, { lineNumber: 3, column: 6 }, items), 1); // foo: ,|
});
test('PrefixMemory', function () {
test('PrefixMemory', () => {
const mem = new PrefixMemory();
buffer.setValue('constructor');

View File

@@ -2,12 +2,10 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Event } from 'vs/base/common/event';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
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';
@@ -15,7 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection';
import { TokenizationResult2 } from 'vs/editor/common/core/token';
import { Handler } from 'vs/editor/common/editorCommon';
import { TextModel } from 'vs/editor/common/model/textModel';
import { IState, ISuggestResult, ISuggestSupport, LanguageIdentifier, MetadataConsts, SuggestRegistry, SuggestTriggerKind, TokenizationRegistry } from 'vs/editor/common/modes';
import { IState, CompletionList, CompletionItemProvider, LanguageIdentifier, MetadataConsts, CompletionProviderRegistry, CompletionTriggerKind, TokenizationRegistry, CompletionItemKind } from 'vs/editor/common/modes';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { NULL_STATE } from 'vs/editor/common/modes/nullMode';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
@@ -25,16 +23,26 @@ import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget';
import { TestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IStorageService, NullStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
export interface Ctor<T> {
new(): T;
}
export function mock<T>(): Ctor<T> {
return function () { } as any;
}
function createMockEditor(model: TextModel): TestCodeEditor {
let editor = createTestCodeEditor({
model: model,
serviceCollection: new ServiceCollection(
[ITelemetryService, NullTelemetryService],
[IStorageService, NullStorageService]
[IStorageService, new InMemoryStorageService()]
),
});
editor.registerAndInstantiateContribution(SnippetController2);
@@ -55,7 +63,7 @@ suite('SuggestModel - Context', function () {
tokenize: undefined,
tokenize2: (line: string, state: IState): TokenizationResult2 => {
const tokensArr: number[] = [];
let prevLanguageId: LanguageIdentifier = undefined;
let prevLanguageId: LanguageIdentifier | undefined = undefined;
for (let i = 0; i < line.length; i++) {
const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);
if (prevLanguageId !== languageId) {
@@ -132,8 +140,8 @@ suite('SuggestModel - Context', function () {
suite('SuggestModel - TriggerAndCancelOracle', function () {
const alwaysEmptySupport: ISuggestSupport = {
provideCompletionItems(doc, pos): ISuggestResult {
const alwaysEmptySupport: CompletionItemProvider = {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: false,
suggestions: []
@@ -141,13 +149,13 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
}
};
const alwaysSomethingSupport: ISuggestSupport = {
provideCompletionItems(doc, pos): ISuggestResult {
const alwaysSomethingSupport: CompletionItemProvider = {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: false,
suggestions: [{
label: doc.getWordUntilPosition(pos).word,
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'foofoo'
}]
};
@@ -167,7 +175,12 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return new Promise((resolve, reject) => {
const editor = createMockEditor(model);
const oracle = new SuggestModel(editor);
const oracle = new SuggestModel(editor, new class extends mock<IEditorWorkerService>() {
computeWordRanges() {
return Promise.resolve({});
}
});
disposables.push(oracle, editor);
try {
@@ -243,7 +256,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('events - suggest/empty', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, alwaysEmptySupport));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysEmptySupport));
return withOracle(model => {
return Promise.all([
@@ -265,7 +278,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('trigger - on type', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
return withOracle((model, editor) => {
return assertEvent(model.onDidSuggest, () => {
@@ -284,13 +297,13 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('#17400: Keep filtering suggestModel.ts after space', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): ISuggestResult {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: false,
suggestions: [{
label: 'My Table',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'My Table'
}]
};
@@ -333,30 +346,33 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('#21484: Trigger character always force a new completion session', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): ISuggestResult {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: false,
suggestions: [{
label: 'foo.bar',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'foo.bar',
overwriteBefore: pos.column - 1
range: Range.fromPositions(pos.with(undefined, 1), pos)
}]
};
}
}));
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
triggerCharacters: ['.'],
provideCompletionItems(doc, pos): ISuggestResult {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: false,
suggestions: [{
label: 'boom',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'boom',
overwriteBefore: doc.getLineContent(pos.lineNumber)[pos.column - 2] === '.' ? 0 : pos.column - 1
range: Range.fromPositions(
pos.delta(0, doc.getLineContent(pos.lineNumber)[pos.column - 2] === '.' ? 0 : -1),
pos
)
}]
};
}
@@ -392,7 +408,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [1/2]', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
return withOracle((model, editor) => {
@@ -417,7 +433,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [2/2]', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
return withOracle((model, editor) => {
@@ -442,15 +458,15 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (1/2)', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): ISuggestResult {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: true,
suggestions: [{
label: 'foo',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'foo',
overwriteBefore: pos.column - 1
range: Range.fromPositions(pos.with(undefined, 1), pos)
}]
};
}
@@ -479,15 +495,15 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (2/2)', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): ISuggestResult {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: true,
suggestions: [{
label: 'foo;',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'foo',
overwriteBefore: pos.column - 1
range: Range.fromPositions(pos.with(undefined, 1), pos)
}]
};
}
@@ -522,19 +538,19 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('Trigger character is provided in suggest context', function () {
let triggerCharacter = '';
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
triggerCharacters: ['.'],
provideCompletionItems(doc, pos, context): ISuggestResult {
assert.equal(context.triggerKind, SuggestTriggerKind.TriggerCharacter);
provideCompletionItems(doc, pos, context): CompletionList {
assert.equal(context.triggerKind, CompletionTriggerKind.TriggerCharacter);
triggerCharacter = context.triggerCharacter;
return {
incomplete: false,
suggestions: [
{
label: 'foo.bar',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'foo.bar',
overwriteBefore: pos.column - 1
range: Range.fromPositions(pos.with(undefined, 1), pos)
}
]
};
@@ -555,20 +571,20 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
});
test('Mac press and hold accent character insertion does not update suggestions, #35269', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): ISuggestResult {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: true,
suggestions: [{
label: 'abc',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'abc',
overwriteBefore: pos.column - 1
range: Range.fromPositions(pos.with(undefined, 1), pos)
}, {
label: 'äbc',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'äbc',
overwriteBefore: pos.column - 1
range: Range.fromPositions(pos.with(undefined, 1), pos)
}]
};
}
@@ -598,7 +614,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
});
test('Backspace should not always cancel code completion, #36491', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
return withOracle(async (model, editor) => {
await assertEvent(model.onDidSuggest, () => {
@@ -627,15 +643,15 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
});
test('Text changes for completion CodeAction are affected by the completion #39893', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): ISuggestResult {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos): CompletionList {
return {
incomplete: true,
suggestions: [{
label: 'bar',
type: 'property',
kind: CompletionItemKind.Property,
insertText: 'bar',
overwriteBefore: 2,
range: Range.fromPositions(pos.delta(0, -2), pos),
additionalTextEdits: [{
text: ', bar',
range: { startLineNumber: 1, endLineNumber: 1, startColumn: 17, endColumn: 17 }
@@ -650,7 +666,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
return withOracle(async (sugget, editor) => {
class TestCtrl extends SuggestController {
_onDidSelectItem(item: ISelectedSuggestion) {
super._onDidSelectItem(item);
super._onDidSelectItem(item, false, true);
}
}
const ctrl = <TestCtrl>editor.registerAndInstantiateContribution(TestCtrl);
@@ -677,7 +693,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
test('Completion unexpectedly triggers on second keypress of an edit group in a snippet #43523', function () {
disposables.push(SuggestRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
return withOracle((model, editor) => {
return assertEvent(model.onDidSuggest, () => {
@@ -701,20 +717,20 @@ suite('SuggestModel - TriggerAndCancelOracle', function () {
let disposeA = 0;
let disposeB = 0;
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos) {
return {
incomplete: true,
suggestions: [{ type: 'folder', label: 'CompleteNot', insertText: 'Incomplete', sortText: 'a', overwriteBefore: pos.column - 1 }],
suggestions: [{ kind: CompletionItemKind.Folder, label: 'CompleteNot', insertText: 'Incomplete', sortText: 'a', overwriteBefore: pos.column - 1 }],
dispose() { disposeA += 1; }
};
}
}));
disposables.push(SuggestRegistry.register({ scheme: 'test' }, {
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
provideCompletionItems(doc, pos) {
return {
incomplete: false,
suggestions: [{ type: 'folder', label: 'Complete', insertText: 'Complete', sortText: 'z', overwriteBefore: pos.column - 1 }],
suggestions: [{ kind: CompletionItemKind.Folder, label: 'Complete', insertText: 'Complete', sortText: 'z', overwriteBefore: pos.column - 1 }],
dispose() { disposeB += 1; }
};
},

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
export class WordContextKey {
static readonly AtEnd = new RawContextKey<boolean>('atEndOfWord', false);
private readonly _ckAtEnd: IContextKey<boolean>;
private readonly _confListener: IDisposable;
private _enabled: boolean;
private _selectionListener?: IDisposable;
constructor(
private readonly _editor: ICodeEditor,
@IContextKeyService contextKeyService: IContextKeyService,
) {
this._ckAtEnd = WordContextKey.AtEnd.bindTo(contextKeyService);
this._confListener = this._editor.onDidChangeConfiguration(e => e.contribInfo && this._update());
this._update();
}
dispose(): void {
dispose(this._confListener, this._selectionListener);
this._ckAtEnd.reset();
}
private _update(): void {
// only update this when tab completions are enabled
const enabled = this._editor.getConfiguration().contribInfo.tabCompletion === 'on';
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
if (this._enabled) {
const checkForWordEnd = () => {
if (!this._editor.hasModel()) {
this._ckAtEnd.set(false);
return;
}
const model = this._editor.getModel();
const selection = this._editor.getSelection();
const word = model.getWordAtPosition(selection.getStartPosition());
if (!word) {
this._ckAtEnd.set(false);
return;
}
this._ckAtEnd.set(word.endColumn === selection.getStartPosition().column);
};
this._selectionListener = this._editor.onDidChangeCursorSelection(checkForWordEnd);
checkForWordEnd();
} else if (this._selectionListener) {
this._ckAtEnd.reset();
this._selectionListener.dispose();
this._selectionListener = undefined;
}
}
}

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { binarySearch, isFalsyOrEmpty } from 'vs/base/common/arrays';
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';
export abstract class WordDistance {
static readonly None = new class extends WordDistance {
distance() { return 0; }
};
static create(service: IEditorWorkerService, editor: ICodeEditor): Thenable<WordDistance> {
if (!editor.getConfiguration().contribInfo.suggest.localityBonus) {
return Promise.resolve(WordDistance.None);
}
if (!editor.hasModel()) {
return Promise.resolve(WordDistance.None);
}
const model = editor.getModel();
const position = editor.getPosition();
if (!service.canComputeWordRanges(model.uri)) {
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);
}
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;
}
blockDistance -= 1;
}
return blockDistance;
}
};
});
}
abstract distance(anchor: IPosition, suggestion: CompletionItem): number;
}