mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-26 06:40:30 -04:00
Merge from master
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'); }
|
||||
|
||||
@@ -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]);
|
||||
|
||||
104
src/vs/editor/contrib/suggest/suggestAlternatives.ts
Normal file
104
src/vs/editor/contrib/suggest/suggestAlternatives.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}; }`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
},
|
||||
|
||||
66
src/vs/editor/contrib/suggest/wordContextKey.ts
Normal file
66
src/vs/editor/contrib/suggest/wordContextKey.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/vs/editor/contrib/suggest/wordDistance.ts
Normal file
89
src/vs/editor/contrib/suggest/wordDistance.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user