mirror of
https://github.com/coder/code-server.git
synced 2026-05-09 22:07:26 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
294
lib/vscode/src/vs/editor/contrib/suggest/completionModel.ts
Normal file
294
lib/vscode/src/vs/editor/contrib/suggest/completionModel.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer, FuzzyScore, anyScore } from 'vs/base/common/filters';
|
||||
import { CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes';
|
||||
import { CompletionItem } from './suggest';
|
||||
import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { compareIgnoreCase } from 'vs/base/common/strings';
|
||||
import { quickSelect } from 'vs/base/common/arrays';
|
||||
|
||||
type StrictCompletionItem = Required<CompletionItem>;
|
||||
|
||||
export interface ICompletionStats {
|
||||
pLabelLen: number;
|
||||
}
|
||||
|
||||
export class LineContext {
|
||||
constructor(
|
||||
readonly leadingLineContent: string,
|
||||
readonly characterCountDelta: number,
|
||||
) { }
|
||||
}
|
||||
|
||||
const enum Refilter {
|
||||
Nothing = 0,
|
||||
All = 1,
|
||||
Incr = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted, filtered completion view model
|
||||
* */
|
||||
export class CompletionModel {
|
||||
|
||||
private readonly _items: CompletionItem[];
|
||||
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?: StrictCompletionItem[];
|
||||
private _providerInfo?: Map<CompletionItemProvider, boolean>;
|
||||
private _stats?: ICompletionStats;
|
||||
|
||||
constructor(
|
||||
items: CompletionItem[],
|
||||
column: number,
|
||||
lineContext: LineContext,
|
||||
wordDistance: WordDistance,
|
||||
options: InternalSuggestOptions,
|
||||
snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none',
|
||||
readonly clipboardText: string | undefined
|
||||
) {
|
||||
this._items = items;
|
||||
this._column = column;
|
||||
this._wordDistance = wordDistance;
|
||||
this._options = options;
|
||||
this._refilterKind = Refilter.All;
|
||||
this._lineContext = lineContext;
|
||||
|
||||
if (snippetSuggestions === 'top') {
|
||||
this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp;
|
||||
} else if (snippetSuggestions === 'bottom') {
|
||||
this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown;
|
||||
}
|
||||
}
|
||||
|
||||
get lineContext(): LineContext {
|
||||
return this._lineContext;
|
||||
}
|
||||
|
||||
set lineContext(value: LineContext) {
|
||||
if (this._lineContext.leadingLineContent !== value.leadingLineContent
|
||||
|| this._lineContext.characterCountDelta !== value.characterCountDelta
|
||||
) {
|
||||
this._refilterKind = this._lineContext.characterCountDelta < value.characterCountDelta && this._filteredItems ? Refilter.Incr : Refilter.All;
|
||||
this._lineContext = value;
|
||||
}
|
||||
}
|
||||
|
||||
get items(): CompletionItem[] {
|
||||
this._ensureCachedState();
|
||||
return this._filteredItems!;
|
||||
}
|
||||
|
||||
get allProvider(): IterableIterator<CompletionItemProvider> {
|
||||
this._ensureCachedState();
|
||||
return this._providerInfo!.keys();
|
||||
}
|
||||
|
||||
get incomplete(): Set<CompletionItemProvider> {
|
||||
this._ensureCachedState();
|
||||
const result = new Set<CompletionItemProvider>();
|
||||
for (let [provider, incomplete] of this._providerInfo!) {
|
||||
if (incomplete) {
|
||||
result.add(provider);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
adopt(except: Set<CompletionItemProvider>): CompletionItem[] {
|
||||
let res: CompletionItem[] = [];
|
||||
for (let i = 0; i < this._items.length;) {
|
||||
if (!except.has(this._items[i].provider)) {
|
||||
res.push(this._items[i]);
|
||||
|
||||
// unordered removed
|
||||
this._items[i] = this._items[this._items.length - 1];
|
||||
this._items.pop();
|
||||
} else {
|
||||
// continue with next item
|
||||
i++;
|
||||
}
|
||||
}
|
||||
this._refilterKind = Refilter.All;
|
||||
return res;
|
||||
}
|
||||
|
||||
get stats(): ICompletionStats {
|
||||
this._ensureCachedState();
|
||||
return this._stats!;
|
||||
}
|
||||
|
||||
private _ensureCachedState(): void {
|
||||
if (this._refilterKind !== Refilter.Nothing) {
|
||||
this._createCachedState();
|
||||
}
|
||||
}
|
||||
|
||||
private _createCachedState(): void {
|
||||
|
||||
this._providerInfo = new Map();
|
||||
|
||||
const labelLengths: number[] = [];
|
||||
|
||||
const { leadingLineContent, characterCountDelta } = this._lineContext;
|
||||
let word = '';
|
||||
let wordLow = '';
|
||||
|
||||
// incrementally filter less
|
||||
const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems!;
|
||||
const target: StrictCompletionItem[] = [];
|
||||
|
||||
// picks a score function based on the number of
|
||||
// items that we have to score/filter and based on the
|
||||
// user-configuration
|
||||
const scoreFn: FuzzyScorer = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
|
||||
const item = source[i];
|
||||
|
||||
if (item.isInvalid) {
|
||||
continue; // SKIP invalid items
|
||||
}
|
||||
|
||||
// collect all support, know if their result is incomplete
|
||||
this._providerInfo.set(item.provider, Boolean(item.container.incomplete));
|
||||
|
||||
// 'word' is that remainder of the current line that we
|
||||
// filter and score against. In theory each suggestion uses a
|
||||
// different word, but in practice not - that's why we cache
|
||||
const overwriteBefore = item.position.column - item.editStart.column;
|
||||
const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
|
||||
if (word.length !== wordLen) {
|
||||
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
|
||||
wordLow = word.toLowerCase();
|
||||
}
|
||||
|
||||
const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name;
|
||||
|
||||
// remember the word against which this item was
|
||||
// scored
|
||||
item.word = word;
|
||||
|
||||
if (wordLen === 0) {
|
||||
// when there is nothing to score against, don't
|
||||
// event try to do. Use a const rank and rely on
|
||||
// the fallback-sort using the initial sort order.
|
||||
// use a score of `-100` because that is out of the
|
||||
// bound of values `fuzzyScore` will return
|
||||
item.score = FuzzyScore.Default;
|
||||
|
||||
} else {
|
||||
// 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 = FuzzyScore.Default;
|
||||
|
||||
} else if (typeof item.completion.filterText === 'string') {
|
||||
// when there is a `filterText` it must match the `word`.
|
||||
// if it matches we check with the label to compute highlights
|
||||
// and if that doesn't yield a result we have no highlights,
|
||||
// despite having the match
|
||||
let match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, false);
|
||||
if (!match) {
|
||||
continue; // NO match
|
||||
}
|
||||
if (compareIgnoreCase(item.completion.filterText, textLabel) === 0) {
|
||||
// filterText and label are actually the same -> use good highlights
|
||||
item.score = match;
|
||||
} else {
|
||||
// re-run the scorer on the label in the hope of a result BUT use the rank
|
||||
// of the filterText-match
|
||||
item.score = anyScore(word, wordLow, wordPos, textLabel, item.labelLow, 0);
|
||||
item.score[0] = match[0]; // use score from filterText
|
||||
}
|
||||
|
||||
} else {
|
||||
// by default match `word` against the `label`
|
||||
let match = scoreFn(word, wordLow, wordPos, textLabel, item.labelLow, 0, false);
|
||||
if (!match) {
|
||||
continue; // NO match
|
||||
}
|
||||
item.score = match;
|
||||
}
|
||||
}
|
||||
|
||||
item.idx = i;
|
||||
item.distance = this._wordDistance.distance(item.position, item.completion);
|
||||
target.push(item as StrictCompletionItem);
|
||||
|
||||
// update stats
|
||||
labelLengths.push(textLabel.length);
|
||||
}
|
||||
|
||||
this._filteredItems = target.sort(this._snippetCompareFn);
|
||||
this._refilterKind = Refilter.Nothing;
|
||||
this._stats = {
|
||||
pLabelLen: labelLengths.length ?
|
||||
quickSelect(labelLengths.length - .85, labelLengths, (a, b) => a - b)
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number {
|
||||
if (a.score[0] > b.score[0]) {
|
||||
return -1;
|
||||
} else if (a.score[0] < b.score[0]) {
|
||||
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) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static _compareCompletionItemsSnippetsDown(a: StrictCompletionItem, b: StrictCompletionItem): number {
|
||||
if (a.completion.kind !== b.completion.kind) {
|
||||
if (a.completion.kind === CompletionItemKind.Snippet) {
|
||||
return 1;
|
||||
} else if (b.completion.kind === CompletionItemKind.Snippet) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return CompletionModel._compareCompletionItems(a, b);
|
||||
}
|
||||
|
||||
private static _compareCompletionItemsSnippetsUp(a: StrictCompletionItem, b: StrictCompletionItem): number {
|
||||
if (a.completion.kind !== b.completion.kind) {
|
||||
if (a.completion.kind === CompletionItemKind.Snippet) {
|
||||
return -1;
|
||||
} else if (b.completion.kind === CompletionItemKind.Snippet) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return CompletionModel._compareCompletionItems(a, b);
|
||||
}
|
||||
}
|
||||
420
lib/vscode/src/vs/editor/contrib/suggest/media/suggest.css
Normal file
420
lib/vscode/src/vs/editor/contrib/suggest/media/suggest.css
Normal file
@@ -0,0 +1,420 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Suggest widget*/
|
||||
|
||||
.monaco-editor .suggest-widget {
|
||||
width: 430px;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.message {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget,
|
||||
.monaco-editor .suggest-details {
|
||||
flex: 0 1 auto;
|
||||
width: 100%;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .suggest-widget,
|
||||
.monaco-editor.hc-black .suggest-details {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
/* Styles for status bar part */
|
||||
|
||||
|
||||
.monaco-editor .suggest-widget .suggest-status-bar {
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
font-size: 80%;
|
||||
padding: 0 4px 0 4px;
|
||||
border-top: 1px solid transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .suggest-status-bar .left {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-label {
|
||||
opacity: 0.5;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-item:not(:last-of-type) .action-label {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-item:not(:last-of-type) .action-label::after {
|
||||
content: ', ';
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row>.contents>.main>.right>.readMore,
|
||||
.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row.focused.string-label>.contents>.main>.right>.readMore {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.with-status-bar:not(.docs-side) .monaco-list .monaco-list-row:hover>.contents>.main>.right.can-expand-details>.details-label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Styles for Message element for when widget is loading or is empty */
|
||||
|
||||
.monaco-editor .suggest-widget>.message {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
/** Styles for the list element **/
|
||||
|
||||
.monaco-editor .suggest-widget>.tree {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/** Styles for each row in the list element **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row {
|
||||
display: flex;
|
||||
-mox-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding-right: 10px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 2px 2px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left, .monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget:not(.frozen) .monaco-highlighted-label .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/** ReadMore Icon styles **/
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close,
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore::before {
|
||||
color: inherit;
|
||||
opacity: 1;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.codicon-close:hover,
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/** signature, qualifier, type/details opacity **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left>.signature-label,
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left>.qualifier-label,
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left>.signature-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left>.qualifier-label {
|
||||
margin-left: 4px;
|
||||
opacity: 0.4;
|
||||
font-size: 90%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/** Type Info and icon next to the label in the focused completion item **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label {
|
||||
margin-left: 1.1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label>.monaco-tokenized-source {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/** Details: if using CompletionItem#details, show on focus **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.details-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget:not(.shows-details) .monaco-list .monaco-list-row.focused>.contents>.main>.right>.details-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/** Details: if using CompletionItemLabel#details, always show **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label)>.contents>.main>.right>.details-label,
|
||||
.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused:not(.string-label)>.contents>.main>.right>.details-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/** Ellipsis on hover **/
|
||||
|
||||
.monaco-editor .suggest-widget:not(.docs-side) .monaco-list .monaco-list-row:hover>.contents>.main>.right.can-expand-details>.details-label {
|
||||
width: calc(100% - 26px);
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.left>.monaco-icon-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label)>.contents>.main>.left>.monaco-icon-label {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label>.contents>.main>.left>.monaco-icon-label {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right {
|
||||
overflow: hidden;
|
||||
flex-shrink: 4;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row>.contents>.main>.right>.readMore {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/** Do NOT display ReadMore when docs is side/below **/
|
||||
|
||||
.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row>.contents>.main>.right>.readMore, .monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row>.contents>.main>.right>.readMore {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/** Do NOT display ReadMore when using plain CompletionItemLabel (details/documentation might not be resolved) **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label>.contents>.main>.right>.readMore {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Focused item can show ReadMore, but can't when docs is side/below **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused.string-label>.contents>.main>.right>.readMore {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row>.contents>.main>.right>.readMore,
|
||||
.monaco-editor .suggest-widget.docs-below .monaco-list .monaco-list-row>.contents>.main>.right>.readMore {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row:hover>.contents>.main>.right>.readMore {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/** Styles for each row in the list **/
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.deprecated {
|
||||
opacity: 0.66;
|
||||
text-decoration: unset;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.deprecated>.monaco-icon-label-container>.monaco-icon-name-container {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.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.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .icon, .monaco-editor .suggest-widget.no-icons .monaco-list .monaco-list-row .suggest-icon::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon.customcolor .colorspan {
|
||||
margin: 0 0 0 0.3em;
|
||||
border: 0.1em solid #000;
|
||||
width: 0.7em;
|
||||
height: 0.7em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/** Styles for the docs of the completion item in focus **/
|
||||
|
||||
.monaco-editor .suggest-details-container {
|
||||
z-index: 41;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details.no-docs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.type {
|
||||
flex: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.7;
|
||||
white-space: pre;
|
||||
margin: 0 24px 0 0;
|
||||
padding: 4px 0 12px 5px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.header>.type.auto-wrap {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs {
|
||||
margin: 0;
|
||||
padding: 4px 5px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details.no-type>.monaco-scrollable-element>.body>.docs {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs {
|
||||
padding: 0;
|
||||
white-space: initial;
|
||||
min-height: calc(1rem + 8px);
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div,
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>span:not(:empty) {
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs>div>p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs .code {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>.docs.markdown-docs .codicon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details>.monaco-scrollable-element>.body>p:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details code {
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-editor .suggest-details p code {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
181
lib/vscode/src/vs/editor/contrib/suggest/resizable.ts
Normal file
181
lib/vscode/src/vs/editor/contrib/suggest/resizable.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { Orientation, Sash, SashState } from 'vs/base/browser/ui/sash/sash';
|
||||
|
||||
|
||||
export interface IResizeEvent {
|
||||
dimension: Dimension;
|
||||
done: boolean;
|
||||
north?: boolean;
|
||||
east?: boolean;
|
||||
south?: boolean;
|
||||
west?: boolean;
|
||||
}
|
||||
|
||||
export class ResizableHTMLElement {
|
||||
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
private readonly _onDidWillResize = new Emitter<void>();
|
||||
readonly onDidWillResize: Event<void> = this._onDidWillResize.event;
|
||||
|
||||
private readonly _onDidResize = new Emitter<IResizeEvent>();
|
||||
readonly onDidResize: Event<IResizeEvent> = this._onDidResize.event;
|
||||
|
||||
private readonly _northSash: Sash;
|
||||
private readonly _eastSash: Sash;
|
||||
private readonly _southSash: Sash;
|
||||
private readonly _westSash: Sash;
|
||||
private readonly _sashListener = new DisposableStore();
|
||||
|
||||
private _size = new Dimension(0, 0);
|
||||
private _minSize = new Dimension(0, 0);
|
||||
private _maxSize = new Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
||||
private _preferredSize?: Dimension;
|
||||
|
||||
constructor() {
|
||||
this.domNode = document.createElement('div');
|
||||
this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size.width }, { orientation: Orientation.VERTICAL });
|
||||
this._westSash = new Sash(this.domNode, { getVerticalSashLeft: () => 0 }, { orientation: Orientation.VERTICAL });
|
||||
this._northSash = new Sash(this.domNode, { getHorizontalSashTop: () => 0 }, { orientation: Orientation.HORIZONTAL });
|
||||
this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size.height }, { orientation: Orientation.HORIZONTAL });
|
||||
|
||||
this._northSash.orthogonalStartSash = this._westSash;
|
||||
this._northSash.orthogonalEndSash = this._eastSash;
|
||||
this._southSash.orthogonalStartSash = this._westSash;
|
||||
this._southSash.orthogonalEndSash = this._eastSash;
|
||||
|
||||
let currentSize: Dimension | undefined;
|
||||
let deltaY = 0;
|
||||
let deltaX = 0;
|
||||
|
||||
this._sashListener.add(Event.any(this._northSash.onDidStart, this._eastSash.onDidStart, this._southSash.onDidStart, this._westSash.onDidStart)(() => {
|
||||
if (currentSize === undefined) {
|
||||
this._onDidWillResize.fire();
|
||||
currentSize = this._size;
|
||||
deltaY = 0;
|
||||
deltaX = 0;
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._northSash.onDidEnd, this._eastSash.onDidEnd, this._southSash.onDidEnd, this._westSash.onDidEnd)(() => {
|
||||
if (currentSize !== undefined) {
|
||||
currentSize = undefined;
|
||||
deltaY = 0;
|
||||
deltaX = 0;
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
|
||||
this._sashListener.add(this._eastSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaX = e.currentX - e.startX;
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, east: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._westSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaX = -(e.currentX - e.startX);
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, west: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._northSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaY = -(e.currentY - e.startY);
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, north: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._southSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaY = e.currentY - e.startY;
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, south: true });
|
||||
}
|
||||
}));
|
||||
|
||||
this._sashListener.add(Event.any(this._eastSash.onDidReset, this._westSash.onDidReset)(e => {
|
||||
if (this._preferredSize) {
|
||||
this.layout(this._size.height, this._preferredSize.width);
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._northSash.onDidReset, this._southSash.onDidReset)(e => {
|
||||
if (this._preferredSize) {
|
||||
this.layout(this._preferredSize.height, this._size.width);
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._northSash.dispose();
|
||||
this._southSash.dispose();
|
||||
this._eastSash.dispose();
|
||||
this._westSash.dispose();
|
||||
this._sashListener.dispose();
|
||||
this.domNode.remove();
|
||||
}
|
||||
|
||||
enableSashes(north: boolean, east: boolean, south: boolean, west: boolean): void {
|
||||
this._northSash.state = north ? SashState.Enabled : SashState.Disabled;
|
||||
this._eastSash.state = east ? SashState.Enabled : SashState.Disabled;
|
||||
this._southSash.state = south ? SashState.Enabled : SashState.Disabled;
|
||||
this._westSash.state = west ? SashState.Enabled : SashState.Disabled;
|
||||
}
|
||||
|
||||
layout(height: number = this.size.height, width: number = this.size.width): void {
|
||||
|
||||
const { height: minHeight, width: minWidth } = this._minSize;
|
||||
const { height: maxHeight, width: maxWidth } = this._maxSize;
|
||||
|
||||
height = Math.max(minHeight, Math.min(maxHeight, height));
|
||||
width = Math.max(minWidth, Math.min(maxWidth, width));
|
||||
|
||||
const newSize = new Dimension(width, height);
|
||||
if (!Dimension.equals(newSize, this._size)) {
|
||||
this.domNode.style.height = height + 'px';
|
||||
this.domNode.style.width = width + 'px';
|
||||
this._size = newSize;
|
||||
this._northSash.layout();
|
||||
this._eastSash.layout();
|
||||
this._southSash.layout();
|
||||
this._westSash.layout();
|
||||
}
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
set maxSize(value: Dimension) {
|
||||
this._maxSize = value;
|
||||
}
|
||||
|
||||
get maxSize() {
|
||||
return this._maxSize;
|
||||
}
|
||||
|
||||
set minSize(value: Dimension) {
|
||||
this._minSize = value;
|
||||
}
|
||||
|
||||
get minSize() {
|
||||
return this._minSize;
|
||||
}
|
||||
|
||||
set preferredSize(value: Dimension | undefined) {
|
||||
this._preferredSize = value;
|
||||
}
|
||||
|
||||
get preferredSize() {
|
||||
return this._preferredSize;
|
||||
}
|
||||
}
|
||||
394
lib/vscode/src/vs/editor/contrib/suggest/suggest.ts
Normal file
394
lib/vscode/src/vs/editor/contrib/suggest/suggest.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 * as modes from 'vs/editor/common/modes';
|
||||
import { Position, IPosition } from 'vs/editor/common/core/position';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { isDisposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
export const Context = {
|
||||
Visible: new RawContextKey<boolean>('suggestWidgetVisible', false),
|
||||
DetailsVisible: new RawContextKey<boolean>('suggestWidgetDetailsVisible', false),
|
||||
MultipleSuggestions: new RawContextKey<boolean>('suggestWidgetMultipleSuggestions', false),
|
||||
MakesTextEdit: new RawContextKey('suggestionMakesTextEdit', true),
|
||||
AcceptSuggestionsOnEnter: new RawContextKey<boolean>('acceptSuggestionOnEnter', true),
|
||||
HasInsertAndReplaceRange: new RawContextKey('suggestionHasInsertAndReplaceRange', false),
|
||||
InsertMode: new RawContextKey<'insert' | 'replace'>('suggestionInsertMode', undefined),
|
||||
CanResolve: new RawContextKey('suggestionCanResolve', false),
|
||||
};
|
||||
|
||||
export const suggestWidgetStatusbarMenu = new MenuId('suggestWidgetStatusBar');
|
||||
|
||||
export class CompletionItem {
|
||||
|
||||
_brand!: 'ISuggestionItem';
|
||||
|
||||
//
|
||||
readonly editStart: IPosition;
|
||||
readonly editInsertEnd: IPosition;
|
||||
readonly editReplaceEnd: IPosition;
|
||||
|
||||
//
|
||||
readonly textLabel: string;
|
||||
|
||||
// perf
|
||||
readonly labelLow: string;
|
||||
readonly sortTextLow?: string;
|
||||
readonly filterTextLow?: string;
|
||||
|
||||
// validation
|
||||
readonly isInvalid: boolean = false;
|
||||
|
||||
// sorting, filtering
|
||||
score: FuzzyScore = FuzzyScore.Default;
|
||||
distance: number = 0;
|
||||
idx?: number;
|
||||
word?: string;
|
||||
|
||||
// resolving
|
||||
private _isResolved?: boolean;
|
||||
private _resolveCache?: Promise<void>;
|
||||
|
||||
constructor(
|
||||
readonly position: IPosition,
|
||||
readonly completion: modes.CompletionItem,
|
||||
readonly container: modes.CompletionList,
|
||||
readonly provider: modes.CompletionItemProvider,
|
||||
) {
|
||||
this.textLabel = typeof completion.label === 'string'
|
||||
? completion.label
|
||||
: completion.label.name;
|
||||
|
||||
// ensure lower-variants (perf)
|
||||
this.labelLow = this.textLabel.toLowerCase();
|
||||
|
||||
// validate label
|
||||
this.isInvalid = !this.textLabel;
|
||||
|
||||
this.sortTextLow = completion.sortText && completion.sortText.toLowerCase();
|
||||
this.filterTextLow = completion.filterText && completion.filterText.toLowerCase();
|
||||
|
||||
// normalize ranges
|
||||
if (Range.isIRange(completion.range)) {
|
||||
this.editStart = new Position(completion.range.startLineNumber, completion.range.startColumn);
|
||||
this.editInsertEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
|
||||
this.editReplaceEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
|
||||
|
||||
// validate range
|
||||
this.isInvalid = this.isInvalid
|
||||
|| Range.spansMultipleLines(completion.range) || completion.range.startLineNumber !== position.lineNumber;
|
||||
|
||||
} else {
|
||||
this.editStart = new Position(completion.range.insert.startLineNumber, completion.range.insert.startColumn);
|
||||
this.editInsertEnd = new Position(completion.range.insert.endLineNumber, completion.range.insert.endColumn);
|
||||
this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn);
|
||||
|
||||
// validate ranges
|
||||
this.isInvalid = this.isInvalid
|
||||
|| Range.spansMultipleLines(completion.range.insert) || Range.spansMultipleLines(completion.range.replace)
|
||||
|| completion.range.insert.startLineNumber !== position.lineNumber || completion.range.replace.startLineNumber !== position.lineNumber
|
||||
|| completion.range.insert.startColumn !== completion.range.replace.startColumn;
|
||||
}
|
||||
|
||||
// create the suggestion resolver
|
||||
if (typeof provider.resolveCompletionItem !== 'function') {
|
||||
this._resolveCache = Promise.resolve();
|
||||
this._isResolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- resolving
|
||||
|
||||
get isResolved(): boolean {
|
||||
return !!this._isResolved;
|
||||
}
|
||||
|
||||
async resolve(token: CancellationToken) {
|
||||
if (!this._resolveCache) {
|
||||
const sub = token.onCancellationRequested(() => {
|
||||
this._resolveCache = undefined;
|
||||
this._isResolved = false;
|
||||
});
|
||||
this._resolveCache = Promise.resolve(this.provider.resolveCompletionItem!(this.completion, token)).then(value => {
|
||||
Object.assign(this.completion, value);
|
||||
this._isResolved = true;
|
||||
sub.dispose();
|
||||
}, err => {
|
||||
if (isPromiseCanceledError(err)) {
|
||||
// the IPC queue will reject the request with the
|
||||
// cancellation error -> reset cached
|
||||
this._resolveCache = undefined;
|
||||
this._isResolved = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this._resolveCache;
|
||||
}
|
||||
}
|
||||
|
||||
export const enum SnippetSortOrder {
|
||||
Top, Inline, Bottom
|
||||
}
|
||||
|
||||
export class CompletionOptions {
|
||||
|
||||
static readonly default = new CompletionOptions();
|
||||
|
||||
constructor(
|
||||
readonly snippetSortOrder = SnippetSortOrder.Bottom,
|
||||
readonly kindFilter = new Set<modes.CompletionItemKind>(),
|
||||
readonly providerFilter = new Set<modes.CompletionItemProvider>(),
|
||||
) { }
|
||||
}
|
||||
|
||||
let _snippetSuggestSupport: modes.CompletionItemProvider;
|
||||
|
||||
export function getSnippetSuggestSupport(): modes.CompletionItemProvider {
|
||||
return _snippetSuggestSupport;
|
||||
}
|
||||
|
||||
export function setSnippetSuggestSupport(support: modes.CompletionItemProvider): modes.CompletionItemProvider {
|
||||
const old = _snippetSuggestSupport;
|
||||
_snippetSuggestSupport = support;
|
||||
return old;
|
||||
}
|
||||
|
||||
export interface CompletionDurationEntry {
|
||||
readonly providerName: string;
|
||||
readonly elapsedProvider: number;
|
||||
readonly elapsedOverall: number;
|
||||
}
|
||||
|
||||
export interface CompletionDurations {
|
||||
readonly entries: readonly CompletionDurationEntry[];
|
||||
readonly elapsed: number;
|
||||
}
|
||||
|
||||
export class CompletionItemModel {
|
||||
constructor(
|
||||
readonly items: CompletionItem[],
|
||||
readonly needsClipboard: boolean,
|
||||
readonly durations: CompletionDurations,
|
||||
readonly disposable: IDisposable,
|
||||
) { }
|
||||
}
|
||||
|
||||
export async function provideSuggestionItems(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
options: CompletionOptions = CompletionOptions.default,
|
||||
context: modes.CompletionContext = { triggerKind: modes.CompletionTriggerKind.Invoke },
|
||||
token: CancellationToken = CancellationToken.None
|
||||
): Promise<CompletionItemModel> {
|
||||
|
||||
const sw = new StopWatch(true);
|
||||
position = position.clone();
|
||||
|
||||
const word = model.getWordAtPosition(position);
|
||||
const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position);
|
||||
const defaultRange = { replace: defaultReplaceRange, insert: defaultReplaceRange.setEndPosition(position.lineNumber, position.column) };
|
||||
|
||||
const result: CompletionItem[] = [];
|
||||
const disposables = new DisposableStore();
|
||||
const durations: CompletionDurationEntry[] = [];
|
||||
let needsClipboard = false;
|
||||
|
||||
const onCompletionList = (provider: modes.CompletionItemProvider, container: modes.CompletionList | null | undefined, sw: StopWatch) => {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
for (let suggestion of container.suggestions) {
|
||||
if (!options.kindFilter.has(suggestion.kind)) {
|
||||
// fill in default range when missing
|
||||
if (!suggestion.range) {
|
||||
suggestion.range = defaultRange;
|
||||
}
|
||||
// fill in default sortText when missing
|
||||
if (!suggestion.sortText) {
|
||||
suggestion.sortText = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.name;
|
||||
}
|
||||
if (!needsClipboard && suggestion.insertTextRules && suggestion.insertTextRules & modes.CompletionItemInsertTextRule.InsertAsSnippet) {
|
||||
needsClipboard = SnippetParser.guessNeedsClipboard(suggestion.insertText);
|
||||
}
|
||||
result.push(new CompletionItem(position, suggestion, container, provider));
|
||||
}
|
||||
}
|
||||
if (isDisposable(container)) {
|
||||
disposables.add(container);
|
||||
}
|
||||
durations.push({
|
||||
providerName: provider._debugDisplayName ?? 'unkown_provider', elapsedProvider: container.duration ?? -1, elapsedOverall: sw.elapsed()
|
||||
});
|
||||
};
|
||||
|
||||
// ask for snippets in parallel to asking "real" providers. Only do something if configured to
|
||||
// do so - no snippet filter, no special-providers-only request
|
||||
const snippetCompletions = (async () => {
|
||||
if (!_snippetSuggestSupport || options.kindFilter.has(modes.CompletionItemKind.Snippet)) {
|
||||
return;
|
||||
}
|
||||
if (options.providerFilter.size > 0 && !options.providerFilter.has(_snippetSuggestSupport)) {
|
||||
return;
|
||||
}
|
||||
const sw = new StopWatch(true);
|
||||
const list = await _snippetSuggestSupport.provideCompletionItems(model, position, context, token);
|
||||
onCompletionList(_snippetSuggestSupport, list, sw);
|
||||
})();
|
||||
|
||||
// add suggestions from contributed providers - providers are ordered in groups of
|
||||
// equal score and once a group produces a result the process stops
|
||||
// get provider groups, always add snippet suggestion provider
|
||||
for (let providerGroup of modes.CompletionProviderRegistry.orderedGroups(model)) {
|
||||
|
||||
// for each support in the group ask for suggestions
|
||||
let lenBefore = result.length;
|
||||
|
||||
await Promise.all(providerGroup.map(async provider => {
|
||||
if (options.providerFilter.size > 0 && !options.providerFilter.has(provider)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sw = new StopWatch(true);
|
||||
const list = await provider.provideCompletionItems(model, position, context, token);
|
||||
onCompletionList(provider, list, sw);
|
||||
} catch (err) {
|
||||
onUnexpectedExternalError(err);
|
||||
}
|
||||
}));
|
||||
|
||||
if (lenBefore !== result.length || token.isCancellationRequested) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await snippetCompletions;
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
disposables.dispose();
|
||||
return Promise.reject<any>(canceled());
|
||||
}
|
||||
|
||||
return new CompletionItemModel(
|
||||
result.sort(getSuggestionComparator(options.snippetSortOrder)),
|
||||
needsClipboard,
|
||||
{ entries: durations, elapsed: sw.elapsed() },
|
||||
disposables,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function defaultComparator(a: CompletionItem, b: CompletionItem): number {
|
||||
// check with 'sortText'
|
||||
if (a.sortTextLow && b.sortTextLow) {
|
||||
if (a.sortTextLow < b.sortTextLow) {
|
||||
return -1;
|
||||
} else if (a.sortTextLow > b.sortTextLow) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// check with 'label'
|
||||
if (a.completion.label < b.completion.label) {
|
||||
return -1;
|
||||
} else if (a.completion.label > b.completion.label) {
|
||||
return 1;
|
||||
}
|
||||
// check with 'type'
|
||||
return a.completion.kind - b.completion.kind;
|
||||
}
|
||||
|
||||
function snippetUpComparator(a: CompletionItem, b: CompletionItem): number {
|
||||
if (a.completion.kind !== b.completion.kind) {
|
||||
if (a.completion.kind === modes.CompletionItemKind.Snippet) {
|
||||
return -1;
|
||||
} else if (b.completion.kind === modes.CompletionItemKind.Snippet) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return defaultComparator(a, b);
|
||||
}
|
||||
|
||||
function snippetDownComparator(a: CompletionItem, b: CompletionItem): number {
|
||||
if (a.completion.kind !== b.completion.kind) {
|
||||
if (a.completion.kind === modes.CompletionItemKind.Snippet) {
|
||||
return 1;
|
||||
} else if (b.completion.kind === modes.CompletionItemKind.Snippet) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return defaultComparator(a, b);
|
||||
}
|
||||
|
||||
interface Comparator<T> { (a: T, b: T): number; }
|
||||
const _snippetComparators = new Map<SnippetSortOrder, Comparator<CompletionItem>>();
|
||||
_snippetComparators.set(SnippetSortOrder.Top, snippetUpComparator);
|
||||
_snippetComparators.set(SnippetSortOrder.Bottom, snippetDownComparator);
|
||||
_snippetComparators.set(SnippetSortOrder.Inline, defaultComparator);
|
||||
|
||||
export function getSuggestionComparator(snippetConfig: SnippetSortOrder): (a: CompletionItem, b: CompletionItem) => number {
|
||||
return _snippetComparators.get(snippetConfig)!;
|
||||
}
|
||||
|
||||
registerDefaultLanguageCommand('_executeCompletionItemProvider', async (model, position, args) => {
|
||||
|
||||
const result: modes.CompletionList = {
|
||||
incomplete: false,
|
||||
suggestions: []
|
||||
};
|
||||
|
||||
const resolving: Promise<any>[] = [];
|
||||
const maxItemsToResolve = args['maxItemsToResolve'] || 0;
|
||||
|
||||
const completions = await provideSuggestionItems(model, position);
|
||||
for (const item of completions.items) {
|
||||
if (resolving.length < maxItemsToResolve) {
|
||||
resolving.push(item.resolve(CancellationToken.None));
|
||||
}
|
||||
result.incomplete = result.incomplete || item.container.incomplete;
|
||||
result.suggestions.push(item.completion);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(resolving);
|
||||
return result;
|
||||
} finally {
|
||||
setTimeout(() => completions.disposable.dispose(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
interface SuggestController extends IEditorContribution {
|
||||
triggerSuggest(onlyFrom?: Set<modes.CompletionItemProvider>): void;
|
||||
}
|
||||
|
||||
const _provider = new class implements modes.CompletionItemProvider {
|
||||
|
||||
onlyOnceSuggestions: modes.CompletionItem[] = [];
|
||||
|
||||
provideCompletionItems(): modes.CompletionList {
|
||||
let suggestions = this.onlyOnceSuggestions.slice(0);
|
||||
let result = { suggestions };
|
||||
this.onlyOnceSuggestions.length = 0;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
modes.CompletionProviderRegistry.register('*', _provider);
|
||||
|
||||
export function showSimpleSuggestions(editor: ICodeEditor, suggestions: modes.CompletionItem[]) {
|
||||
setTimeout(() => {
|
||||
_provider.onlyOnceSuggestions.push(...suggestions);
|
||||
editor.getContribution<SuggestController>('editor.contrib.suggestController').triggerSuggest(new Set<modes.CompletionItemProvider>().add(_provider));
|
||||
}, 0);
|
||||
}
|
||||
104
lib/vscode/src/vs/editor/contrib/suggest/suggestAlternatives.ts
Normal file
104
lib/vscode/src/vs/editor/contrib/suggest/suggestAlternatives.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { 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 readonly OtherSuggestions = new RawContextKey<boolean>('hasOtherSuggestions', false);
|
||||
|
||||
private readonly _ckOtherSuggestions: IContextKey<boolean>;
|
||||
|
||||
private _index: number = 0;
|
||||
private _model: CompletionModel | undefined;
|
||||
private _acceptNext: ((selected: ISelectedSuggestion) => any) | undefined;
|
||||
private _listener: IDisposable | undefined;
|
||||
private _ignore: boolean | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
this._ckOtherSuggestions = SuggestAlternatives.OtherSuggestions.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._ckOtherSuggestions.reset();
|
||||
this._listener?.dispose();
|
||||
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].completion.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ISelectedSuggestion, SuggestWidget } from './suggestWidget';
|
||||
import { CharacterSet } from 'vs/editor/common/core/characterClassifier';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export class CommitCharacterController {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private _active?: {
|
||||
readonly acceptCharacters: CharacterSet;
|
||||
readonly item: ISelectedSuggestion;
|
||||
};
|
||||
|
||||
constructor(editor: ICodeEditor, widget: SuggestWidget, accept: (selected: ISelectedSuggestion) => any) {
|
||||
|
||||
this._disposables.add(widget.onDidShow(() => this._onItem(widget.getFocusedItem())));
|
||||
this._disposables.add(widget.onDidFocus(this._onItem, this));
|
||||
this._disposables.add(widget.onDidHide(this.reset, this));
|
||||
|
||||
this._disposables.add(editor.onWillType(text => {
|
||||
if (this._active && !widget.isFrozen()) {
|
||||
const ch = text.charCodeAt(text.length - 1);
|
||||
if (this._active.acceptCharacters.has(ch) && editor.getOption(EditorOption.acceptSuggestionOnCommitCharacter)) {
|
||||
accept(this._active.item);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _onItem(selected: ISelectedSuggestion | undefined): void {
|
||||
if (!selected || !isNonEmptyArray(selected.item.completion.commitCharacters)) {
|
||||
// no item or no commit characters
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._active && this._active.item.item === selected.item) {
|
||||
// still the same item
|
||||
return;
|
||||
}
|
||||
|
||||
// keep item and its commit characters
|
||||
const acceptCharacters = new CharacterSet();
|
||||
for (const ch of selected.item.completion.commitCharacters) {
|
||||
if (ch.length > 0) {
|
||||
acceptCharacters.add(ch.charCodeAt(0));
|
||||
}
|
||||
}
|
||||
this._active = { acceptCharacters, item: selected };
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._active = undefined;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
898
lib/vscode/src/vs/editor/contrib/suggest/suggestController.ts
Normal file
898
lib/vscode/src/vs/editor/contrib/suggest/suggestController.ts
Normal file
@@ -0,0 +1,898 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { KeyCode, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { dispose, IDisposable, DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { StableEditorScrollState } from 'vs/editor/browser/core/editorState';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { CompletionItemProvider, CompletionItemInsertTextRule } from 'vs/editor/common/modes';
|
||||
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
|
||||
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
|
||||
import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICommandService, CommandsRegistry } 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, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { Context as SuggestContext, CompletionItem, suggestWidgetStatusbarMenu } 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 { Event } from 'vs/base/common/event';
|
||||
import { IdleValue } from 'vs/base/common/async';
|
||||
import { isObject, assertType } from 'vs/base/common/types';
|
||||
import { CommitCharacterController } from './suggestCommitCharacters';
|
||||
import { OvertypingCapturer } from './suggestOvertypingCapturer';
|
||||
import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
import { TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
// sticky suggest widget which doesn't disappear on focus out and such
|
||||
let _sticky = false;
|
||||
// _sticky = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
|
||||
|
||||
class LineSuffix {
|
||||
|
||||
private readonly _marker: string[] | undefined;
|
||||
|
||||
constructor(private readonly _model: ITextModel, private readonly _position: IPosition) {
|
||||
// spy on what's happening right of the cursor. two cases:
|
||||
// 1. end of line -> check that it's still end of line
|
||||
// 2. mid of line -> add a marker and compute the delta
|
||||
const maxColumn = _model.getLineMaxColumn(_position.lineNumber);
|
||||
if (maxColumn !== _position.column) {
|
||||
const offset = _model.getOffsetAt(_position);
|
||||
const end = _model.getPositionAt(offset + 1);
|
||||
this._marker = _model.deltaDecorations([], [{
|
||||
range: Range.fromPositions(_position, end),
|
||||
options: { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._marker && !this._model.isDisposed()) {
|
||||
this._model.deltaDecorations(this._marker, []);
|
||||
}
|
||||
}
|
||||
|
||||
delta(position: IPosition): number {
|
||||
if (this._model.isDisposed() || this._position.lineNumber !== position.lineNumber) {
|
||||
// bail out early if things seems fishy
|
||||
return 0;
|
||||
}
|
||||
// read the marker (in case suggest was triggered at line end) or compare
|
||||
// the cursor to the line end.
|
||||
if (this._marker) {
|
||||
const range = this._model.getDecorationRange(this._marker[0]);
|
||||
const end = this._model.getOffsetAt(range!.getStartPosition());
|
||||
return end - this._model.getOffsetAt(position);
|
||||
} else {
|
||||
return this._model.getLineMaxColumn(position.lineNumber) - position.column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const enum InsertFlags {
|
||||
NoBeforeUndoStop = 1,
|
||||
NoAfterUndoStop = 2,
|
||||
KeepAlternativeSuggestions = 4,
|
||||
AlternativeOverwriteConfig = 8
|
||||
}
|
||||
|
||||
export class SuggestController implements IEditorContribution {
|
||||
|
||||
public static readonly ID: string = 'editor.contrib.suggestController';
|
||||
|
||||
public static get(editor: ICodeEditor): SuggestController {
|
||||
return editor.getContribution<SuggestController>(SuggestController.ID);
|
||||
}
|
||||
|
||||
readonly editor: ICodeEditor;
|
||||
readonly model: SuggestModel;
|
||||
readonly widget: IdleValue<SuggestWidget>;
|
||||
|
||||
private readonly _alternatives: IdleValue<SuggestAlternatives>;
|
||||
private readonly _lineSuffix = new MutableDisposable<LineSuffix>();
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private readonly _overtypingCapturer: IdleValue<OvertypingCapturer>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@ISuggestMemoryService private readonly _memoryService: ISuggestMemoryService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
this.editor = editor;
|
||||
this.model = _instantiationService.createInstance(SuggestModel, this.editor,);
|
||||
|
||||
// context key: update insert/replace mode
|
||||
const ctxInsertMode = SuggestContext.InsertMode.bindTo(_contextKeyService);
|
||||
ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode);
|
||||
this.model.onDidTrigger(() => ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode));
|
||||
|
||||
this.widget = this._toDispose.add(new IdleValue(() => {
|
||||
|
||||
const widget = this._instantiationService.createInstance(SuggestWidget, this.editor);
|
||||
|
||||
this._toDispose.add(widget);
|
||||
this._toDispose.add(widget.onDidSelect(item => this._insertSuggestion(item, 0), this));
|
||||
|
||||
// Wire up logic to accept a suggestion on certain characters
|
||||
const commitCharacterController = new CommitCharacterController(this.editor, widget, item => this._insertSuggestion(item, InsertFlags.NoAfterUndoStop));
|
||||
this._toDispose.add(commitCharacterController);
|
||||
this._toDispose.add(this.model.onDidSuggest(e => {
|
||||
if (e.completionModel.items.length === 0) {
|
||||
commitCharacterController.reset();
|
||||
}
|
||||
}));
|
||||
|
||||
// Wire up makes text edit context key
|
||||
const ctxMakesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService);
|
||||
const ctxHasInsertAndReplace = SuggestContext.HasInsertAndReplaceRange.bindTo(this._contextKeyService);
|
||||
const ctxCanResolve = SuggestContext.CanResolve.bindTo(this._contextKeyService);
|
||||
|
||||
this._toDispose.add(toDisposable(() => {
|
||||
ctxMakesTextEdit.reset();
|
||||
ctxHasInsertAndReplace.reset();
|
||||
ctxCanResolve.reset();
|
||||
}));
|
||||
|
||||
this._toDispose.add(widget.onDidFocus(({ item }) => {
|
||||
|
||||
// (ctx: makesTextEdit)
|
||||
const position = this.editor.getPosition()!;
|
||||
const startColumn = item.editStart.column;
|
||||
const endColumn = position.column;
|
||||
let value = true;
|
||||
if (
|
||||
this.editor.getOption(EditorOption.acceptSuggestionOnEnter) === 'smart'
|
||||
&& this.model.state === State.Auto
|
||||
&& !item.completion.command
|
||||
&& !item.completion.additionalTextEdits
|
||||
&& !(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)
|
||||
&& endColumn - startColumn === item.completion.insertText.length
|
||||
) {
|
||||
const oldText = this.editor.getModel()!.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn
|
||||
});
|
||||
value = oldText !== item.completion.insertText;
|
||||
}
|
||||
ctxMakesTextEdit.set(value);
|
||||
|
||||
// (ctx: hasInsertAndReplaceRange)
|
||||
ctxHasInsertAndReplace.set(!Position.equals(item.editInsertEnd, item.editReplaceEnd));
|
||||
|
||||
// (ctx: canResolve)
|
||||
ctxCanResolve.set(Boolean(item.provider.resolveCompletionItem) || Boolean(item.completion.documentation) || item.completion.detail !== item.completion.label);
|
||||
}));
|
||||
|
||||
this._toDispose.add(widget.onDetailsKeyDown(e => {
|
||||
// cmd + c on macOS, ctrl + c on Win / Linux
|
||||
if (
|
||||
e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, KeyCode.KEY_C)) ||
|
||||
(platform.isMacintosh && e.toKeybinding().equals(new SimpleKeybinding(false, false, false, true, KeyCode.KEY_C)))
|
||||
) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.toKeybinding().isModifierKey()) {
|
||||
this.editor.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
return widget;
|
||||
}));
|
||||
|
||||
// Wire up text overtyping capture
|
||||
this._overtypingCapturer = this._toDispose.add(new IdleValue(() => {
|
||||
return this._toDispose.add(new OvertypingCapturer(this.editor, this.model));
|
||||
}));
|
||||
|
||||
this._alternatives = this._toDispose.add(new IdleValue(() => {
|
||||
return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService));
|
||||
}));
|
||||
|
||||
this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor));
|
||||
|
||||
this._toDispose.add(this.model.onDidTrigger(e => {
|
||||
this.widget.value.showTriggered(e.auto, e.shy ? 250 : 50);
|
||||
this._lineSuffix.value = new LineSuffix(this.editor.getModel()!, e.position);
|
||||
}));
|
||||
this._toDispose.add(this.model.onDidSuggest(e => {
|
||||
if (!e.shy) {
|
||||
let index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items);
|
||||
this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
|
||||
}
|
||||
}));
|
||||
this._toDispose.add(this.model.onDidCancel(e => {
|
||||
if (!e.retrigger) {
|
||||
this.widget.value.hideWidget();
|
||||
}
|
||||
}));
|
||||
this._toDispose.add(this.editor.onDidBlurEditorWidget(() => {
|
||||
if (!_sticky) {
|
||||
this.model.cancel();
|
||||
this.model.clear();
|
||||
}
|
||||
}));
|
||||
|
||||
// Manage the acceptSuggestionsOnEnter context key
|
||||
let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService);
|
||||
let updateFromConfig = () => {
|
||||
const acceptSuggestionOnEnter = this.editor.getOption(EditorOption.acceptSuggestionOnEnter);
|
||||
acceptSuggestionsOnEnter.set(acceptSuggestionOnEnter === 'on' || acceptSuggestionOnEnter === 'smart');
|
||||
};
|
||||
this._toDispose.add(this.editor.onDidChangeConfiguration(() => updateFromConfig()));
|
||||
updateFromConfig();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._alternatives.dispose();
|
||||
this._toDispose.dispose();
|
||||
this.widget.dispose();
|
||||
this.model.dispose();
|
||||
this._lineSuffix.dispose();
|
||||
}
|
||||
|
||||
protected _insertSuggestion(
|
||||
event: ISelectedSuggestion | undefined,
|
||||
flags: InsertFlags
|
||||
): void {
|
||||
if (!event || !event.item) {
|
||||
this._alternatives.value.reset();
|
||||
this.model.cancel();
|
||||
this.model.clear();
|
||||
return;
|
||||
}
|
||||
if (!this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this.editor.getModel();
|
||||
const modelVersionNow = model.getAlternativeVersionId();
|
||||
const { item } = event;
|
||||
|
||||
//
|
||||
const tasks: Promise<any>[] = [];
|
||||
const cts = new CancellationTokenSource();
|
||||
|
||||
// pushing undo stops *before* additional text edits and
|
||||
// *after* the main edit
|
||||
if (!(flags & InsertFlags.NoBeforeUndoStop)) {
|
||||
this.editor.pushUndoStop();
|
||||
}
|
||||
|
||||
// compute overwrite[Before|After] deltas BEFORE applying extra edits
|
||||
const info = this.getOverwriteInfo(item, Boolean(flags & InsertFlags.AlternativeOverwriteConfig));
|
||||
|
||||
// keep item in memory
|
||||
this._memoryService.memorize(model, this.editor.getPosition(), item);
|
||||
|
||||
|
||||
if (Array.isArray(item.completion.additionalTextEdits)) {
|
||||
// sync additional edits
|
||||
const scrollState = StableEditorScrollState.capture(this.editor);
|
||||
this.editor.executeEdits(
|
||||
'suggestController.additionalTextEdits.sync',
|
||||
item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))
|
||||
);
|
||||
scrollState.restoreRelativeVerticalPositionOfCursor(this.editor);
|
||||
|
||||
} else if (!item.isResolved) {
|
||||
// async additional edits
|
||||
const sw = new StopWatch(true);
|
||||
let position: IPosition | undefined;
|
||||
|
||||
const docListener = model.onDidChangeContent(e => {
|
||||
if (e.isFlush) {
|
||||
cts.cancel();
|
||||
docListener.dispose();
|
||||
return;
|
||||
}
|
||||
for (let change of e.changes) {
|
||||
const thisPosition = Range.getEndPosition(change.range);
|
||||
if (!position || Position.isBefore(thisPosition, position)) {
|
||||
position = thisPosition;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let oldFlags = flags;
|
||||
flags |= InsertFlags.NoAfterUndoStop;
|
||||
let didType = false;
|
||||
let typeListener = this.editor.onWillType(() => {
|
||||
typeListener.dispose();
|
||||
didType = true;
|
||||
if (!(oldFlags & InsertFlags.NoAfterUndoStop)) {
|
||||
this.editor.pushUndoStop();
|
||||
}
|
||||
});
|
||||
|
||||
tasks.push(item.resolve(cts.token).then(() => {
|
||||
if (!item.completion.additionalTextEdits || cts.token.isCancellationRequested) {
|
||||
return false;
|
||||
}
|
||||
if (position && item.completion.additionalTextEdits.some(edit => Position.isBefore(position!, Range.getStartPosition(edit.range)))) {
|
||||
return false;
|
||||
}
|
||||
if (didType) {
|
||||
this.editor.pushUndoStop();
|
||||
}
|
||||
const scrollState = StableEditorScrollState.capture(this.editor);
|
||||
this.editor.executeEdits(
|
||||
'suggestController.additionalTextEdits.async',
|
||||
item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))
|
||||
);
|
||||
scrollState.restoreRelativeVerticalPositionOfCursor(this.editor);
|
||||
if (didType || !(oldFlags & InsertFlags.NoAfterUndoStop)) {
|
||||
this.editor.pushUndoStop();
|
||||
}
|
||||
return true;
|
||||
}).then(applied => {
|
||||
this._logService.trace('[suggest] async resolving of edits DONE (ms, applied?)', sw.elapsed(), applied);
|
||||
docListener.dispose();
|
||||
typeListener.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
let { insertText } = item.completion;
|
||||
if (!(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) {
|
||||
insertText = SnippetParser.escape(insertText);
|
||||
}
|
||||
|
||||
SnippetController2.get(this.editor).insert(insertText, {
|
||||
overwriteBefore: info.overwriteBefore,
|
||||
overwriteAfter: info.overwriteAfter,
|
||||
undoStopBefore: false,
|
||||
undoStopAfter: false,
|
||||
adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace),
|
||||
clipboardText: event.model.clipboardText,
|
||||
overtypingCapturer: this._overtypingCapturer.value
|
||||
});
|
||||
|
||||
if (!(flags & InsertFlags.NoAfterUndoStop)) {
|
||||
this.editor.pushUndoStop();
|
||||
}
|
||||
|
||||
if (!item.completion.command) {
|
||||
// done
|
||||
this.model.cancel();
|
||||
|
||||
} else if (item.completion.command.id === TriggerSuggestAction.id) {
|
||||
// retigger
|
||||
this.model.trigger({ auto: true, shy: false }, true);
|
||||
|
||||
} else {
|
||||
// exec command, done
|
||||
tasks.push(this._commandService.executeCommand(item.completion.command.id, ...(item.completion.command.arguments ? [...item.completion.command.arguments] : [])).catch(onUnexpectedError));
|
||||
this.model.cancel();
|
||||
}
|
||||
|
||||
if (flags & InsertFlags.KeepAlternativeSuggestions) {
|
||||
this._alternatives.value.set(event, next => {
|
||||
|
||||
// cancel resolving of additional edits
|
||||
cts.cancel();
|
||||
|
||||
// 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._insertSuggestion(
|
||||
next,
|
||||
InsertFlags.NoBeforeUndoStop | InsertFlags.NoAfterUndoStop | (flags & InsertFlags.AlternativeOverwriteConfig ? InsertFlags.AlternativeOverwriteConfig : 0)
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._alertCompletionItem(item);
|
||||
|
||||
// clear only now - after all tasks are done
|
||||
Promise.all(tasks).finally(() => {
|
||||
this.model.clear();
|
||||
cts.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
getOverwriteInfo(item: CompletionItem, toggleMode: boolean): { overwriteBefore: number, overwriteAfter: number } {
|
||||
assertType(this.editor.hasModel());
|
||||
|
||||
let replace = this.editor.getOption(EditorOption.suggest).insertMode === 'replace';
|
||||
if (toggleMode) {
|
||||
replace = !replace;
|
||||
}
|
||||
const overwriteBefore = item.position.column - item.editStart.column;
|
||||
const overwriteAfter = (replace ? item.editReplaceEnd.column : item.editInsertEnd.column) - item.position.column;
|
||||
const columnDelta = this.editor.getPosition().column - item.position.column;
|
||||
const suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this.editor.getPosition()) : 0;
|
||||
|
||||
return {
|
||||
overwriteBefore: overwriteBefore + columnDelta,
|
||||
overwriteAfter: overwriteAfter + suffixDelta
|
||||
};
|
||||
}
|
||||
|
||||
private _alertCompletionItem({ completion: suggestion }: CompletionItem): void {
|
||||
const textLabel = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.name;
|
||||
if (isNonEmptyArray(suggestion.additionalTextEdits)) {
|
||||
let msg = nls.localize('aria.alert.snippet', "Accepting '{0}' made {1} additional edits", textLabel, suggestion.additionalTextEdits.length);
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
|
||||
triggerSuggest(onlyFrom?: Set<CompletionItemProvider>): void {
|
||||
if (this.editor.hasModel()) {
|
||||
this.model.trigger({ auto: false, shy: false }, false, onlyFrom);
|
||||
this.editor.revealLine(this.editor.getPosition().lineNumber, ScrollType.Smooth);
|
||||
this.editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
triggerSuggestAndAcceptBest(arg: { fallback: string }): void {
|
||||
if (!this.editor.hasModel()) {
|
||||
return;
|
||||
|
||||
}
|
||||
const positionNow = this.editor.getPosition();
|
||||
|
||||
const fallback = () => {
|
||||
if (positionNow.equals(this.editor.getPosition()!)) {
|
||||
this._commandService.executeCommand(arg.fallback);
|
||||
}
|
||||
};
|
||||
|
||||
const makesTextEdit = (item: CompletionItem): boolean => {
|
||||
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet || item.completion.additionalTextEdits) {
|
||||
// snippet, other editor -> makes edit
|
||||
return true;
|
||||
}
|
||||
const position = this.editor.getPosition()!;
|
||||
const startColumn = item.editStart.column;
|
||||
const endColumn = position.column;
|
||||
if (endColumn - startColumn !== item.completion.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.completion.insertText;
|
||||
};
|
||||
|
||||
Event.once(this.model.onDidTrigger)(_ => {
|
||||
// wait for trigger because only then the cancel-event is trustworthy
|
||||
let listener: IDisposable[] = [];
|
||||
|
||||
Event.any<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._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, completionModel.items);
|
||||
const item = completionModel.items[index];
|
||||
if (!makesTextEdit(item)) {
|
||||
fallback();
|
||||
return;
|
||||
}
|
||||
this.editor.pushUndoStop();
|
||||
this._insertSuggestion({ index, item, model: completionModel }, InsertFlags.KeepAlternativeSuggestions | InsertFlags.NoBeforeUndoStop | InsertFlags.NoAfterUndoStop);
|
||||
|
||||
}, undefined, listener);
|
||||
});
|
||||
|
||||
this.model.trigger({ auto: false, shy: true });
|
||||
this.editor.revealLine(positionNow.lineNumber, ScrollType.Smooth);
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
acceptSelectedSuggestion(keepAlternativeSuggestions: boolean, alternativeOverwriteConfig: boolean): void {
|
||||
const item = this.widget.value.getFocusedItem();
|
||||
let flags = 0;
|
||||
if (keepAlternativeSuggestions) {
|
||||
flags |= InsertFlags.KeepAlternativeSuggestions;
|
||||
}
|
||||
if (alternativeOverwriteConfig) {
|
||||
flags |= InsertFlags.AlternativeOverwriteConfig;
|
||||
}
|
||||
this._insertSuggestion(item, flags);
|
||||
}
|
||||
acceptNextSuggestion() {
|
||||
this._alternatives.value.next();
|
||||
}
|
||||
|
||||
acceptPrevSuggestion() {
|
||||
this._alternatives.value.prev();
|
||||
}
|
||||
|
||||
cancelSuggestWidget(): void {
|
||||
this.model.cancel();
|
||||
this.model.clear();
|
||||
this.widget.value.hideWidget();
|
||||
}
|
||||
|
||||
selectNextSuggestion(): void {
|
||||
this.widget.value.selectNext();
|
||||
}
|
||||
|
||||
selectNextPageSuggestion(): void {
|
||||
this.widget.value.selectNextPage();
|
||||
}
|
||||
|
||||
selectLastSuggestion(): void {
|
||||
this.widget.value.selectLast();
|
||||
}
|
||||
|
||||
selectPrevSuggestion(): void {
|
||||
this.widget.value.selectPrevious();
|
||||
}
|
||||
|
||||
selectPrevPageSuggestion(): void {
|
||||
this.widget.value.selectPreviousPage();
|
||||
}
|
||||
|
||||
selectFirstSuggestion(): void {
|
||||
this.widget.value.selectFirst();
|
||||
}
|
||||
|
||||
toggleSuggestionDetails(): void {
|
||||
this.widget.value.toggleDetails();
|
||||
}
|
||||
|
||||
toggleExplainMode(): void {
|
||||
this.widget.value.toggleExplainMode();
|
||||
}
|
||||
|
||||
toggleSuggestionFocus(): void {
|
||||
this.widget.value.toggleDetailsFocus();
|
||||
}
|
||||
|
||||
resetWidgetSize(): void {
|
||||
this.widget.value.resetPersistedSize();
|
||||
}
|
||||
}
|
||||
|
||||
export class TriggerSuggestAction extends EditorAction {
|
||||
|
||||
static readonly id = 'editor.action.triggerSuggest';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: TriggerSuggestAction.id,
|
||||
label: nls.localize('suggest.trigger.label', "Trigger Suggest"),
|
||||
alias: 'Trigger Suggest',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCompletionItemProvider),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Space,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.KEY_I],
|
||||
mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.Alt | KeyCode.Escape, KeyMod.CtrlCmd | KeyCode.KEY_I] },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
const controller = SuggestController.get(editor);
|
||||
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
controller.triggerSuggest();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(SuggestController.ID, SuggestController);
|
||||
registerEditorAction(TriggerSuggestAction);
|
||||
|
||||
const weight = KeybindingWeight.EditorContrib + 90;
|
||||
|
||||
const SuggestCommand = EditorCommand.bindToContribution<SuggestController>(SuggestController.get);
|
||||
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'acceptSelectedSuggestion',
|
||||
precondition: SuggestContext.Visible,
|
||||
handler(x) {
|
||||
x.acceptSelectedSuggestion(true, false);
|
||||
}
|
||||
}));
|
||||
|
||||
// normal tab
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
id: 'acceptSelectedSuggestion',
|
||||
when: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus),
|
||||
primary: KeyCode.Tab,
|
||||
weight
|
||||
});
|
||||
|
||||
// accept on enter has special rules
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
id: 'acceptSelectedSuggestion',
|
||||
when: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus, SuggestContext.AcceptSuggestionsOnEnter, SuggestContext.MakesTextEdit),
|
||||
primary: KeyCode.Enter,
|
||||
weight,
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, {
|
||||
command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.insert', "Insert") },
|
||||
group: 'left',
|
||||
order: 1,
|
||||
when: SuggestContext.HasInsertAndReplaceRange.toNegated()
|
||||
});
|
||||
MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, {
|
||||
command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.insert', "Insert") },
|
||||
group: 'left',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert'))
|
||||
});
|
||||
MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, {
|
||||
command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.replace', "Replace") },
|
||||
group: 'left',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace'))
|
||||
});
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'acceptAlternativeSelectedSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.Shift | KeyCode.Enter,
|
||||
secondary: [KeyMod.Shift | KeyCode.Tab],
|
||||
},
|
||||
handler(x) {
|
||||
x.acceptSelectedSuggestion(false, true);
|
||||
},
|
||||
menuOpts: [{
|
||||
menuId: suggestWidgetStatusbarMenu,
|
||||
group: 'left',
|
||||
order: 2,
|
||||
when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')),
|
||||
title: nls.localize('accept.replace', "Replace")
|
||||
}, {
|
||||
menuId: suggestWidgetStatusbarMenu,
|
||||
group: 'left',
|
||||
order: 2,
|
||||
when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')),
|
||||
title: nls.localize('accept.insert', "Insert")
|
||||
}]
|
||||
}));
|
||||
|
||||
|
||||
// continue to support the old command
|
||||
CommandsRegistry.registerCommandAlias('acceptSelectedSuggestionOnEnter', 'acceptSelectedSuggestion');
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'hideSuggestWidget',
|
||||
precondition: SuggestContext.Visible,
|
||||
handler: x => x.cancelSuggestWidget(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape]
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'selectNextSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
|
||||
handler: c => c.selectNextSuggestion(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyCode.DownArrow,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow],
|
||||
mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KEY_N] }
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'selectNextPageSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
|
||||
handler: c => c.selectNextPageSuggestion(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyCode.PageDown,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.PageDown]
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'selectLastSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
|
||||
handler: c => c.selectLastSuggestion()
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'selectPrevSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
|
||||
handler: c => c.selectPrevSuggestion(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyCode.UpArrow,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow],
|
||||
mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KEY_P] }
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'selectPrevPageSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
|
||||
handler: c => c.selectPrevPageSuggestion(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyCode.PageUp,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.PageUp]
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'selectFirstSuggestion',
|
||||
precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
|
||||
handler: c => c.selectFirstSuggestion()
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'toggleSuggestionDetails',
|
||||
precondition: SuggestContext.Visible,
|
||||
handler: x => x.toggleSuggestionDetails(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Space,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyCode.Space }
|
||||
},
|
||||
menuOpts: [{
|
||||
menuId: suggestWidgetStatusbarMenu,
|
||||
group: 'right',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(SuggestContext.DetailsVisible, SuggestContext.CanResolve),
|
||||
title: nls.localize('detail.more', "show less")
|
||||
}, {
|
||||
menuId: suggestWidgetStatusbarMenu,
|
||||
group: 'right',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(SuggestContext.DetailsVisible.toNegated(), SuggestContext.CanResolve),
|
||||
title: nls.localize('detail.less', "show more")
|
||||
}]
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'toggleExplainMode',
|
||||
precondition: SuggestContext.Visible,
|
||||
handler: x => x.toggleExplainMode(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_SLASH,
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'toggleSuggestionFocus',
|
||||
precondition: SuggestContext.Visible,
|
||||
handler: x => x.toggleSuggestionFocus(),
|
||||
kbOpts: {
|
||||
weight: weight,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Space,
|
||||
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space }
|
||||
}
|
||||
}));
|
||||
|
||||
//#region tab completions
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'insertBestCompletion',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.textInputFocus,
|
||||
ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),
|
||||
WordContextKey.AtEnd,
|
||||
SuggestContext.Visible.toNegated(),
|
||||
SuggestAlternatives.OtherSuggestions.toNegated(),
|
||||
SnippetController2.InSnippetMode.toNegated()
|
||||
),
|
||||
handler: (x, arg) => {
|
||||
|
||||
x.triggerSuggestAndAcceptBest(isObject(arg) ? { fallback: 'tab', ...arg } : { fallback: 'tab' });
|
||||
},
|
||||
kbOpts: {
|
||||
weight,
|
||||
primary: KeyCode.Tab
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new SuggestCommand({
|
||||
id: 'insertNextSuggestion',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.textInputFocus,
|
||||
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(
|
||||
EditorContextKeys.textInputFocus,
|
||||
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
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
registerEditorAction(class extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.resetSuggestSize',
|
||||
label: nls.localize('suggest.reset.label', "Reset Suggest Widget Size"),
|
||||
alias: 'Reset Suggest Widget Size',
|
||||
precondition: undefined
|
||||
});
|
||||
}
|
||||
|
||||
run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
SuggestController.get(editor).resetWidgetSize();
|
||||
}
|
||||
});
|
||||
312
lib/vscode/src/vs/editor/contrib/suggest/suggestMemory.ts
Normal file
312
lib/vscode/src/vs/editor/contrib/suggest/suggestMemory.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { LRUCache, TernarySearchTree } from 'vs/base/common/map';
|
||||
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { CompletionItemKind, completionKindFromString } from 'vs/editor/common/modes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
|
||||
export abstract class Memory {
|
||||
|
||||
constructor(readonly name: MemMode) { }
|
||||
|
||||
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
||||
if (items.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
let topScore = items[0].score[0];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const { score, completion: suggestion } = items[i];
|
||||
if (score[0] !== topScore) {
|
||||
// stop when leaving the group of top matches
|
||||
break;
|
||||
}
|
||||
if (suggestion.preselect) {
|
||||
// stop when seeing an auto-select-item
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
abstract memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
|
||||
|
||||
abstract toJSON(): object | undefined;
|
||||
|
||||
abstract fromJSON(data: object): void;
|
||||
}
|
||||
|
||||
export class NoMemory extends Memory {
|
||||
|
||||
constructor() {
|
||||
super('first');
|
||||
}
|
||||
|
||||
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
fromJSON() {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export interface MemItem {
|
||||
type: string | CompletionItemKind;
|
||||
insertText: string;
|
||||
touch: number;
|
||||
}
|
||||
|
||||
export class LRUMemory extends Memory {
|
||||
|
||||
constructor() {
|
||||
super('recentlyUsed');
|
||||
}
|
||||
|
||||
private _cache = new LRUCache<string, MemItem>(300, 0.66);
|
||||
private _seq = 0;
|
||||
|
||||
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
||||
const { label } = item.completion;
|
||||
const key = `${model.getLanguageIdentifier().language}/${label}`;
|
||||
this._cache.set(key, {
|
||||
touch: this._seq++,
|
||||
type: item.completion.kind,
|
||||
insertText: item.completion.insertText
|
||||
});
|
||||
}
|
||||
|
||||
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
||||
|
||||
if (items.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lineSuffix = model.getLineContent(pos.lineNumber).substr(pos.column - 10, pos.column - 1);
|
||||
if (/\s$/.test(lineSuffix)) {
|
||||
return super.select(model, pos, items);
|
||||
}
|
||||
|
||||
let topScore = items[0].score[0];
|
||||
let indexPreselect = -1;
|
||||
let indexRecency = -1;
|
||||
let seq = -1;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].score[0] !== topScore) {
|
||||
// consider only top items
|
||||
break;
|
||||
}
|
||||
const key = `${model.getLanguageIdentifier().language}/${items[i].completion.label}`;
|
||||
const item = this._cache.peek(key);
|
||||
if (item && item.touch > seq && item.type === items[i].completion.kind && item.insertText === items[i].completion.insertText) {
|
||||
seq = item.touch;
|
||||
indexRecency = i;
|
||||
}
|
||||
if (items[i].completion.preselect && indexPreselect === -1) {
|
||||
// stop when seeing an auto-select-item
|
||||
return indexPreselect = i;
|
||||
}
|
||||
}
|
||||
if (indexRecency !== -1) {
|
||||
return indexRecency;
|
||||
} else if (indexPreselect !== -1) {
|
||||
return indexPreselect;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
return this._cache.toJSON();
|
||||
}
|
||||
|
||||
fromJSON(data: [string, MemItem][]): void {
|
||||
this._cache.clear();
|
||||
let seq = 0;
|
||||
for (const [key, value] of data) {
|
||||
value.touch = seq;
|
||||
value.type = typeof value.type === 'number' ? value.type : completionKindFromString(value.type);
|
||||
this._cache.set(key, value);
|
||||
}
|
||||
this._seq = this._cache.size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PrefixMemory extends Memory {
|
||||
|
||||
constructor() {
|
||||
super('recentlyUsedByPrefix');
|
||||
}
|
||||
|
||||
private _trie = TernarySearchTree.forStrings<MemItem>();
|
||||
private _seq = 0;
|
||||
|
||||
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
||||
const { word } = model.getWordUntilPosition(pos);
|
||||
const key = `${model.getLanguageIdentifier().language}/${word}`;
|
||||
this._trie.set(key, {
|
||||
type: item.completion.kind,
|
||||
insertText: item.completion.insertText,
|
||||
touch: this._seq++
|
||||
});
|
||||
}
|
||||
|
||||
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
||||
let { word } = model.getWordUntilPosition(pos);
|
||||
if (!word) {
|
||||
return super.select(model, pos, items);
|
||||
}
|
||||
let key = `${model.getLanguageIdentifier().language}/${word}`;
|
||||
let item = this._trie.get(key);
|
||||
if (!item) {
|
||||
item = this._trie.findSubstr(key);
|
||||
}
|
||||
if (item) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let { kind, insertText } = items[i].completion;
|
||||
if (kind === item.type && insertText === item.insertText) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.select(model, pos, items);
|
||||
}
|
||||
|
||||
toJSON(): object {
|
||||
|
||||
let entries: [string, MemItem][] = [];
|
||||
this._trie.forEach((value, key) => entries.push([key, value]));
|
||||
|
||||
// sort by last recently used (touch), then
|
||||
// take the top 200 item and normalize their
|
||||
// touch
|
||||
entries
|
||||
.sort((a, b) => -(a[1].touch - b[1].touch))
|
||||
.forEach((value, i) => value[1].touch = i);
|
||||
|
||||
return entries.slice(0, 200);
|
||||
}
|
||||
|
||||
fromJSON(data: [string, MemItem][]): void {
|
||||
this._trie.clear();
|
||||
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 : completionKindFromString(value.type);
|
||||
this._trie.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MemMode = 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix';
|
||||
|
||||
export class SuggestMemoryService implements ISuggestMemoryService {
|
||||
|
||||
private static readonly _strategyCtors = new Map<MemMode, { new(): Memory }>([
|
||||
['recentlyUsedByPrefix', PrefixMemory],
|
||||
['recentlyUsed', LRUMemory],
|
||||
['first', NoMemory]
|
||||
]);
|
||||
|
||||
private static readonly _storagePrefix = 'suggest/memories';
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
||||
private readonly _persistSoon: RunOnceScheduler;
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private _strategy?: Memory;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@IConfigurationService private readonly _configService: IConfigurationService,
|
||||
) {
|
||||
this._persistSoon = new RunOnceScheduler(() => this._saveState(), 500);
|
||||
this._disposables.add(_storageService.onWillSaveState(e => {
|
||||
if (e.reason === WillSaveStateReason.SHUTDOWN) {
|
||||
this._saveState();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this._persistSoon.dispose();
|
||||
}
|
||||
|
||||
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
||||
this._withStrategy(model, pos).memorize(model, pos, item);
|
||||
this._persistSoon.schedule();
|
||||
}
|
||||
|
||||
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
||||
return this._withStrategy(model, pos).select(model, pos, items);
|
||||
}
|
||||
|
||||
private _withStrategy(model: ITextModel, pos: IPosition): Memory {
|
||||
|
||||
const mode = this._configService.getValue<MemMode>('editor.suggestSelection', {
|
||||
overrideIdentifier: this._modeService.getLanguageIdentifier(model.getLanguageIdAtPosition(pos.lineNumber, pos.column))?.language,
|
||||
resource: model.uri
|
||||
});
|
||||
|
||||
if (this._strategy?.name !== mode) {
|
||||
|
||||
this._saveState();
|
||||
const ctor = SuggestMemoryService._strategyCtors.get(mode) || NoMemory;
|
||||
this._strategy = new ctor();
|
||||
|
||||
try {
|
||||
const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');
|
||||
const scope = share ? StorageScope.GLOBAL : StorageScope.WORKSPACE;
|
||||
const raw = this._storageService.get(`${SuggestMemoryService._storagePrefix}/${mode}`, scope);
|
||||
if (raw) {
|
||||
this._strategy.fromJSON(JSON.parse(raw));
|
||||
}
|
||||
} catch (e) {
|
||||
// things can go wrong with JSON...
|
||||
}
|
||||
}
|
||||
|
||||
return this._strategy;
|
||||
}
|
||||
|
||||
private _saveState() {
|
||||
if (this._strategy) {
|
||||
const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');
|
||||
const scope = share ? StorageScope.GLOBAL : StorageScope.WORKSPACE;
|
||||
const raw = JSON.stringify(this._strategy);
|
||||
this._storageService.store(`${SuggestMemoryService._storagePrefix}/${this._strategy.name}`, raw, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ISuggestMemoryService = createDecorator<ISuggestMemoryService>('ISuggestMemories');
|
||||
|
||||
export interface ISuggestMemoryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
|
||||
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number;
|
||||
}
|
||||
|
||||
registerSingleton(ISuggestMemoryService, SuggestMemoryService, true);
|
||||
642
lib/vscode/src/vs/editor/contrib/suggest/suggestModel.ts
Normal file
642
lib/vscode/src/vs/editor/contrib/suggest/suggestModel.ts
Normal file
@@ -0,0 +1,642 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { Position, IPosition } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ITextModel, IWordAtPosition } from 'vs/editor/common/model';
|
||||
import { CompletionItemProvider, StandardTokenType, CompletionContext, CompletionProviderRegistry, CompletionTriggerKind, CompletionItemKind } from 'vs/editor/common/modes';
|
||||
import { CompletionModel } from './completionModel';
|
||||
import { CompletionItem, getSuggestionComparator, provideSuggestionItems, getSnippetSuggestSupport, SnippetSortOrder, CompletionOptions, CompletionDurations } 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';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { isLowSurrogate, isHighSurrogate } from 'vs/base/common/strings';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export interface ICancelEvent {
|
||||
readonly retrigger: boolean;
|
||||
}
|
||||
|
||||
export interface ITriggerEvent {
|
||||
readonly auto: boolean;
|
||||
readonly shy: boolean;
|
||||
readonly position: IPosition;
|
||||
}
|
||||
|
||||
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 triggerKind?: CompletionTriggerKind;
|
||||
readonly triggerCharacter?: string;
|
||||
}
|
||||
|
||||
export class LineContext {
|
||||
|
||||
static shouldAutoTrigger(editor: ICodeEditor): boolean {
|
||||
if (!editor.hasModel()) {
|
||||
return false;
|
||||
}
|
||||
const model = editor.getModel();
|
||||
const pos = editor.getPosition();
|
||||
model.tokenizeIfCheap(pos.lineNumber);
|
||||
|
||||
const word = model.getWordAtPosition(pos);
|
||||
if (!word) {
|
||||
return false;
|
||||
}
|
||||
if (word.endColumn !== pos.column) {
|
||||
return false;
|
||||
}
|
||||
if (!isNaN(Number(word.word))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly lineNumber: number;
|
||||
readonly column: number;
|
||||
readonly leadingLineContent: string;
|
||||
readonly leadingWord: IWordAtPosition;
|
||||
readonly auto: boolean;
|
||||
readonly shy: 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;
|
||||
}
|
||||
}
|
||||
|
||||
export const enum State {
|
||||
Idle = 0,
|
||||
Manual = 1,
|
||||
Auto = 2
|
||||
}
|
||||
|
||||
export class SuggestModel implements IDisposable {
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private _quickSuggestDelay: number = 10;
|
||||
private readonly _triggerCharacterListener = new DisposableStore();
|
||||
private readonly _triggerQuickSuggest = new TimeoutTimer();
|
||||
private _state: State = State.Idle;
|
||||
|
||||
private _requestToken?: CancellationTokenSource;
|
||||
private _context?: LineContext;
|
||||
private _currentSelection: Selection;
|
||||
|
||||
private _completionModel: CompletionModel | undefined;
|
||||
private readonly _completionDisposables = new DisposableStore();
|
||||
private readonly _onDidCancel = new Emitter<ICancelEvent>();
|
||||
private readonly _onDidTrigger = new Emitter<ITriggerEvent>();
|
||||
private readonly _onDidSuggest = new Emitter<ISuggestEvent>();
|
||||
|
||||
readonly onDidCancel: Event<ICancelEvent> = this._onDidCancel.event;
|
||||
readonly onDidTrigger: Event<ITriggerEvent> = this._onDidTrigger.event;
|
||||
readonly onDidSuggest: Event<ISuggestEvent> = this._onDidSuggest.event;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
|
||||
@IClipboardService private readonly _clipboardService: IClipboardService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
this._currentSelection = this._editor.getSelection() || new Selection(1, 1, 1, 1);
|
||||
|
||||
// wire up various listeners
|
||||
this._toDispose.add(this._editor.onDidChangeModel(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this.cancel();
|
||||
}));
|
||||
this._toDispose.add(this._editor.onDidChangeModelLanguage(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this.cancel();
|
||||
}));
|
||||
this._toDispose.add(this._editor.onDidChangeConfiguration(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this._updateQuickSuggest();
|
||||
}));
|
||||
this._toDispose.add(CompletionProviderRegistry.onDidChange(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this._updateActiveSuggestSession();
|
||||
}));
|
||||
this._toDispose.add(this._editor.onDidChangeCursorSelection(e => {
|
||||
this._onCursorChange(e);
|
||||
}));
|
||||
|
||||
let editorIsComposing = false;
|
||||
this._toDispose.add(this._editor.onDidCompositionStart(() => {
|
||||
editorIsComposing = true;
|
||||
}));
|
||||
this._toDispose.add(this._editor.onDidCompositionEnd(() => {
|
||||
// refilter when composition ends
|
||||
editorIsComposing = false;
|
||||
this._refilterCompletionItems();
|
||||
}));
|
||||
this._toDispose.add(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();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._triggerCharacterListener);
|
||||
dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this._triggerQuickSuggest]);
|
||||
this._toDispose.dispose();
|
||||
this._completionDisposables.dispose();
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
// --- handle configuration & precondition changes
|
||||
|
||||
private _updateQuickSuggest(): void {
|
||||
this._quickSuggestDelay = this._editor.getOption(EditorOption.quickSuggestionsDelay);
|
||||
|
||||
if (isNaN(this._quickSuggestDelay) || (!this._quickSuggestDelay && this._quickSuggestDelay !== 0) || this._quickSuggestDelay < 0) {
|
||||
this._quickSuggestDelay = 10;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateTriggerCharacters(): void {
|
||||
this._triggerCharacterListener.clear();
|
||||
|
||||
if (this._editor.getOption(EditorOption.readOnly)
|
||||
|| !this._editor.hasModel()
|
||||
|| !this._editor.getOption(EditorOption.suggestOnTriggerCharacters)) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const supportsByTriggerCharacter = new Map<string, Set<CompletionItemProvider>>();
|
||||
for (const support of CompletionProviderRegistry.all(this._editor.getModel())) {
|
||||
for (const ch of support.triggerCharacters || []) {
|
||||
let set = supportsByTriggerCharacter.get(ch);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
set.add(getSnippetSuggestSupport());
|
||||
supportsByTriggerCharacter.set(ch, set);
|
||||
}
|
||||
set.add(support);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const checkTriggerCharacter = (text?: string) => {
|
||||
|
||||
if (!text) {
|
||||
// came here from the compositionEnd-event
|
||||
const position = this._editor.getPosition()!;
|
||||
const model = this._editor.getModel()!;
|
||||
text = model.getLineContent(position.lineNumber).substr(0, position.column - 1);
|
||||
}
|
||||
|
||||
let lastChar = '';
|
||||
if (isLowSurrogate(text.charCodeAt(text.length - 1))) {
|
||||
if (isHighSurrogate(text.charCodeAt(text.length - 2))) {
|
||||
lastChar = text.substr(text.length - 2);
|
||||
}
|
||||
} else {
|
||||
lastChar = text.charAt(text.length - 1);
|
||||
}
|
||||
|
||||
const supports = supportsByTriggerCharacter.get(lastChar);
|
||||
if (supports) {
|
||||
// keep existing items that where not computed by the
|
||||
// supports/providers that want to trigger now
|
||||
const existing = this._completionModel
|
||||
? { items: this._completionModel.adopt(supports), clipboardText: this._completionModel.clipboardText }
|
||||
: undefined;
|
||||
this.trigger({ auto: true, shy: false, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, existing);
|
||||
}
|
||||
};
|
||||
|
||||
this._triggerCharacterListener.add(this._editor.onDidType(checkTriggerCharacter));
|
||||
this._triggerCharacterListener.add(this._editor.onDidCompositionEnd(checkTriggerCharacter));
|
||||
}
|
||||
|
||||
// --- trigger/retrigger/cancel suggest
|
||||
|
||||
get state(): State {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
cancel(retrigger: boolean = false): void {
|
||||
if (this._state !== State.Idle) {
|
||||
this._triggerQuickSuggest.cancel();
|
||||
this._requestToken?.cancel();
|
||||
this._requestToken = undefined;
|
||||
this._state = State.Idle;
|
||||
this._completionModel = undefined;
|
||||
this._context = undefined;
|
||||
this._onDidCancel.fire({ retrigger });
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._completionDisposables.clear();
|
||||
}
|
||||
|
||||
private _updateActiveSuggestSession(): void {
|
||||
if (this._state !== State.Idle) {
|
||||
if (!this._editor.hasModel() || !CompletionProviderRegistry.has(this._editor.getModel())) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.trigger({ auto: this._state === State.Auto, shy: false }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onCursorChange(e: ICursorSelectionChangedEvent): void {
|
||||
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const prevSelection = this._currentSelection;
|
||||
this._currentSelection = this._editor.getSelection();
|
||||
|
||||
if (!e.selection.isEmpty()
|
||||
|| (e.reason !== CursorChangeReason.NotSet && e.reason !== CursorChangeReason.Explicit)
|
||||
|| (e.source !== 'keyboard' && e.source !== 'deleteLeft')
|
||||
) {
|
||||
// Early exit if nothing needs to be done!
|
||||
// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CompletionProviderRegistry.has(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === State.Idle && e.reason === CursorChangeReason.NotSet) {
|
||||
|
||||
if (this._editor.getOption(EditorOption.quickSuggestions) === false) {
|
||||
// not enabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prevSelection.containsRange(this._currentSelection) && !prevSelection.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())) {
|
||||
// cursor didn't move RIGHT
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._editor.getOption(EditorOption.suggest).snippetsPreventQuickSuggestions && SnippetController2.get(this._editor).isInSnippet()) {
|
||||
// no quick suggestion when in snippet mode
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancel();
|
||||
|
||||
this._triggerQuickSuggest.cancelAndSet(() => {
|
||||
if (this._state !== State.Idle) {
|
||||
return;
|
||||
}
|
||||
if (!LineContext.shouldAutoTrigger(this._editor)) {
|
||||
return;
|
||||
}
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
const pos = this._editor.getPosition();
|
||||
// validate enabled now
|
||||
const quickSuggestions = this._editor.getOption(EditorOption.quickSuggestions);
|
||||
if (quickSuggestions === false) {
|
||||
return;
|
||||
} else if (quickSuggestions === true) {
|
||||
// all good
|
||||
} else {
|
||||
// Check the type of the token that triggered this
|
||||
model.tokenizeIfCheap(pos.lineNumber);
|
||||
const lineTokens = model.getLineTokens(pos.lineNumber);
|
||||
const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0)));
|
||||
const inValidScope = quickSuggestions.other && tokenType === StandardTokenType.Other
|
||||
|| quickSuggestions.comments && tokenType === StandardTokenType.Comment
|
||||
|| quickSuggestions.strings && tokenType === StandardTokenType.String;
|
||||
|
||||
if (!inValidScope) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// we made it till here -> trigger now
|
||||
this.trigger({ auto: true, shy: false });
|
||||
|
||||
}, this._quickSuggestDelay);
|
||||
|
||||
|
||||
} else if (this._state !== State.Idle && e.reason === CursorChangeReason.Explicit) {
|
||||
// suggest is active and something like cursor keys are used to move
|
||||
// the cursor. this means we can refilter at the new position
|
||||
this._refilterCompletionItems();
|
||||
}
|
||||
}
|
||||
|
||||
private _refilterCompletionItems(): void {
|
||||
// Re-filter suggestions. This MUST run async because filtering/scoring
|
||||
// uses the model content AND the cursor position. The latter is NOT
|
||||
// updated when the document has changed (the event which drives this method)
|
||||
// and therefore a little pause (next mirco task) is needed. See:
|
||||
// https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context#25933985
|
||||
Promise.resolve().then(() => {
|
||||
if (this._state === State.Idle) {
|
||||
return;
|
||||
}
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
const position = this._editor.getPosition();
|
||||
const ctx = new LineContext(model, position, this._state === State.Auto, false);
|
||||
this._onNewContext(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set<CompletionItemProvider>, existing?: { items: CompletionItem[], clipboardText: string | undefined }): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const auto = context.auto;
|
||||
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
|
||||
|
||||
// Cancel previous requests, change state & update UI
|
||||
this.cancel(retrigger);
|
||||
this._state = auto ? State.Auto : State.Manual;
|
||||
this._onDidTrigger.fire({ auto, shy: context.shy, position: this._editor.getPosition() });
|
||||
|
||||
// Capture context when request was sent
|
||||
this._context = ctx;
|
||||
|
||||
// Build context for request
|
||||
let suggestCtx: CompletionContext = { triggerKind: context.triggerKind ?? CompletionTriggerKind.Invoke };
|
||||
if (context.triggerCharacter) {
|
||||
suggestCtx = {
|
||||
triggerKind: CompletionTriggerKind.TriggerCharacter,
|
||||
triggerCharacter: context.triggerCharacter
|
||||
};
|
||||
}
|
||||
|
||||
this._requestToken = new CancellationTokenSource();
|
||||
|
||||
// kind filter and snippet sort rules
|
||||
const snippetSuggestions = this._editor.getOption(EditorOption.snippetSuggestions);
|
||||
let snippetSortOrder = SnippetSortOrder.Inline;
|
||||
switch (snippetSuggestions) {
|
||||
case 'top':
|
||||
snippetSortOrder = SnippetSortOrder.Top;
|
||||
break;
|
||||
// ↓ that's the default anyways...
|
||||
// case 'inline':
|
||||
// snippetSortOrder = SnippetSortOrder.Inline;
|
||||
// break;
|
||||
case 'bottom':
|
||||
snippetSortOrder = SnippetSortOrder.Bottom;
|
||||
break;
|
||||
}
|
||||
|
||||
const itemKindFilter = SuggestModel._createItemKindFilter(this._editor);
|
||||
const wordDistance = WordDistance.create(this._editorWorkerService, this._editor);
|
||||
|
||||
const completions = provideSuggestionItems(
|
||||
model,
|
||||
this._editor.getPosition(),
|
||||
new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom),
|
||||
suggestCtx,
|
||||
this._requestToken.token
|
||||
);
|
||||
|
||||
Promise.all([completions, wordDistance]).then(async ([completions, wordDistance]) => {
|
||||
|
||||
this._requestToken?.dispose();
|
||||
|
||||
if (this._state === State.Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clipboardText = existing?.clipboardText;
|
||||
if (!clipboardText && completions.needsClipboard) {
|
||||
clipboardText = await this._clipboardService.readText();
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
let items = completions.items;
|
||||
|
||||
if (existing) {
|
||||
const cmpFn = getSuggestionComparator(snippetSortOrder);
|
||||
items = items.concat(existing.items).sort(cmpFn);
|
||||
}
|
||||
|
||||
const ctx = new LineContext(model, this._editor.getPosition(), auto, context.shy);
|
||||
this._completionModel = new CompletionModel(items, this._context!.column, {
|
||||
leadingLineContent: ctx.leadingLineContent,
|
||||
characterCountDelta: ctx.column - this._context!.column
|
||||
},
|
||||
wordDistance,
|
||||
this._editor.getOption(EditorOption.suggest),
|
||||
this._editor.getOption(EditorOption.snippetSuggestions),
|
||||
clipboardText
|
||||
);
|
||||
|
||||
// store containers so that they can be disposed later
|
||||
this._completionDisposables.add(completions.disposable);
|
||||
|
||||
this._onNewContext(ctx);
|
||||
|
||||
// finally report telemetry about durations
|
||||
this._reportDurationsTelemetry(completions.durations);
|
||||
|
||||
}).catch(onUnexpectedError);
|
||||
}
|
||||
|
||||
private _reportDurationsTelemetry(durations: CompletionDurations): void {
|
||||
|
||||
setTimeout(() => {
|
||||
type Durations = { data: string; };
|
||||
type DurationsClassification = { data: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' } };
|
||||
this._telemetryService.publicLog2<Durations, DurationsClassification>('suggest.durations.json', { data: JSON.stringify(durations) });
|
||||
this._logService.debug('suggest.durations.json', durations);
|
||||
});
|
||||
}
|
||||
|
||||
private static _createItemKindFilter(editor: ICodeEditor): Set<CompletionItemKind> {
|
||||
// kind filter and snippet sort rules
|
||||
const result = new Set<CompletionItemKind>();
|
||||
|
||||
// snippet setting
|
||||
const snippetSuggestions = editor.getOption(EditorOption.snippetSuggestions);
|
||||
if (snippetSuggestions === 'none') {
|
||||
result.add(CompletionItemKind.Snippet);
|
||||
}
|
||||
|
||||
// type setting
|
||||
const suggestOptions = editor.getOption(EditorOption.suggest);
|
||||
if (!suggestOptions.showMethods) { result.add(CompletionItemKind.Method); }
|
||||
if (!suggestOptions.showFunctions) { result.add(CompletionItemKind.Function); }
|
||||
if (!suggestOptions.showConstructors) { result.add(CompletionItemKind.Constructor); }
|
||||
if (!suggestOptions.showFields) { result.add(CompletionItemKind.Field); }
|
||||
if (!suggestOptions.showVariables) { result.add(CompletionItemKind.Variable); }
|
||||
if (!suggestOptions.showClasses) { result.add(CompletionItemKind.Class); }
|
||||
if (!suggestOptions.showStructs) { result.add(CompletionItemKind.Struct); }
|
||||
if (!suggestOptions.showInterfaces) { result.add(CompletionItemKind.Interface); }
|
||||
if (!suggestOptions.showModules) { result.add(CompletionItemKind.Module); }
|
||||
if (!suggestOptions.showProperties) { result.add(CompletionItemKind.Property); }
|
||||
if (!suggestOptions.showEvents) { result.add(CompletionItemKind.Event); }
|
||||
if (!suggestOptions.showOperators) { result.add(CompletionItemKind.Operator); }
|
||||
if (!suggestOptions.showUnits) { result.add(CompletionItemKind.Unit); }
|
||||
if (!suggestOptions.showValues) { result.add(CompletionItemKind.Value); }
|
||||
if (!suggestOptions.showConstants) { result.add(CompletionItemKind.Constant); }
|
||||
if (!suggestOptions.showEnums) { result.add(CompletionItemKind.Enum); }
|
||||
if (!suggestOptions.showEnumMembers) { result.add(CompletionItemKind.EnumMember); }
|
||||
if (!suggestOptions.showKeywords) { result.add(CompletionItemKind.Keyword); }
|
||||
if (!suggestOptions.showWords) { result.add(CompletionItemKind.Text); }
|
||||
if (!suggestOptions.showColors) { result.add(CompletionItemKind.Color); }
|
||||
if (!suggestOptions.showFiles) { result.add(CompletionItemKind.File); }
|
||||
if (!suggestOptions.showReferences) { result.add(CompletionItemKind.Reference); }
|
||||
if (!suggestOptions.showColors) { result.add(CompletionItemKind.Customcolor); }
|
||||
if (!suggestOptions.showFolders) { result.add(CompletionItemKind.Folder); }
|
||||
if (!suggestOptions.showTypeParameters) { result.add(CompletionItemKind.TypeParameter); }
|
||||
if (!suggestOptions.showSnippets) { result.add(CompletionItemKind.Snippet); }
|
||||
if (!suggestOptions.showUsers) { result.add(CompletionItemKind.User); }
|
||||
if (!suggestOptions.showIssues) { result.add(CompletionItemKind.Issue); }
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _onNewContext(ctx: LineContext): void {
|
||||
|
||||
if (!this._context) {
|
||||
// happens when 24x7 IntelliSense is enabled and still in its delay
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.lineNumber !== this._context.lineNumber) {
|
||||
// e.g. happens when pressing Enter while IntelliSense is computed
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.leadingWord.startColumn < this._context.leadingWord.startColumn) {
|
||||
// happens when the current word gets outdented
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.column < this._context.column) {
|
||||
// typed -> moved cursor LEFT -> retrigger if still on a word
|
||||
if (ctx.leadingWord.word) {
|
||||
this.trigger({ auto: this._context.auto, shy: false }, true);
|
||||
} else {
|
||||
this.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._completionModel) {
|
||||
// happens when IntelliSense is not yet computed
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.leadingWord.word.length !== 0 && ctx.leadingWord.startColumn > this._context.leadingWord.startColumn) {
|
||||
// started a new word while IntelliSense shows -> retrigger
|
||||
|
||||
// Select those providers have not contributed to this completion model and re-trigger completions for
|
||||
// them. Also adopt the existing items and merge them into the new completion model
|
||||
const inactiveProvider = new Set(CompletionProviderRegistry.all(this._editor.getModel()!));
|
||||
for (let provider of this._completionModel.allProvider) {
|
||||
inactiveProvider.delete(provider);
|
||||
}
|
||||
const items = this._completionModel.adopt(new Set());
|
||||
this.trigger({ auto: this._context.auto, shy: false }, true, inactiveProvider, { items, clipboardText: this._completionModel.clipboardText });
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.column > this._context.column && this._completionModel.incomplete.size > 0 && ctx.leadingWord.word.length !== 0) {
|
||||
// typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger
|
||||
const { incomplete } = this._completionModel;
|
||||
const items = this._completionModel.adopt(incomplete);
|
||||
this.trigger({ auto: this._state === State.Auto, shy: false, triggerKind: CompletionTriggerKind.TriggerForIncompleteCompletions }, true, incomplete, { items, clipboardText: this._completionModel.clipboardText });
|
||||
|
||||
} else {
|
||||
// typed -> moved cursor RIGHT -> update UI
|
||||
let oldLineContext = this._completionModel.lineContext;
|
||||
let isFrozen = false;
|
||||
|
||||
this._completionModel.lineContext = {
|
||||
leadingLineContent: ctx.leadingLineContent,
|
||||
characterCountDelta: ctx.column - this._context.column
|
||||
};
|
||||
|
||||
if (this._completionModel.items.length === 0) {
|
||||
|
||||
if (LineContext.shouldAutoTrigger(this._editor) && this._context.leadingWord.endColumn < ctx.leadingWord.startColumn) {
|
||||
// retrigger when heading into a new word
|
||||
this.trigger({ auto: this._context.auto, shy: false }, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._context.auto) {
|
||||
// freeze when IntelliSense was manually requested
|
||||
this._completionModel.lineContext = oldLineContext;
|
||||
isFrozen = this._completionModel.items.length > 0;
|
||||
|
||||
if (isFrozen && ctx.leadingWord.word.length === 0) {
|
||||
// there were results before but now there aren't
|
||||
// and also we are not on a word anymore -> cancel
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
// nothing left
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._onDidSuggest.fire({
|
||||
completionModel: this._completionModel,
|
||||
auto: this._context.auto,
|
||||
shy: this._context.shy,
|
||||
isFrozen,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { SuggestModel } from 'vs/editor/contrib/suggest/suggestModel';
|
||||
|
||||
export class OvertypingCapturer implements IDisposable {
|
||||
|
||||
private static readonly _maxSelectionLength = 51200;
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private _lastOvertyped: { value: string; multiline: boolean }[] = [];
|
||||
private _empty: boolean = true;
|
||||
|
||||
constructor(editor: ICodeEditor, suggestModel: SuggestModel) {
|
||||
|
||||
this._disposables.add(editor.onWillType(() => {
|
||||
if (!this._empty) {
|
||||
return;
|
||||
}
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selections = editor.getSelections();
|
||||
const selectionsLength = selections.length;
|
||||
|
||||
// Check if it will overtype any selections
|
||||
let willOvertype = false;
|
||||
for (let i = 0; i < selectionsLength; i++) {
|
||||
if (!selections[i].isEmpty()) {
|
||||
willOvertype = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!willOvertype) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastOvertyped = [];
|
||||
const model = editor.getModel();
|
||||
for (let i = 0; i < selectionsLength; i++) {
|
||||
const selection = selections[i];
|
||||
// Check for overtyping capturer restrictions
|
||||
if (model.getValueLengthInRange(selection) > OvertypingCapturer._maxSelectionLength) {
|
||||
return;
|
||||
}
|
||||
this._lastOvertyped[i] = { value: model.getValueInRange(selection), multiline: selection.startLineNumber !== selection.endLineNumber };
|
||||
}
|
||||
this._empty = false;
|
||||
}));
|
||||
|
||||
this._disposables.add(suggestModel.onDidCancel(e => {
|
||||
if (!this._empty && !e.retrigger) {
|
||||
this._empty = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getLastOvertypedInfo(idx: number): { value: string; multiline: boolean } | undefined {
|
||||
if (!this._empty && idx >= 0 && idx < this._lastOvertyped.length) {
|
||||
return this._lastOvertyped[idx];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
972
lib/vscode/src/vs/editor/contrib/suggest/suggestWidget.ts
Normal file
972
lib/vscode/src/vs/editor/contrib/suggest/suggestWidget.ts
Normal file
@@ -0,0 +1,972 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/suggest';
|
||||
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
|
||||
import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded
|
||||
import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IListEvent, IListMouseEvent, IListGestureEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser';
|
||||
import { Context as SuggestContext, CompletionItem } from './suggest';
|
||||
import { CompletionModel } from './completionModel';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { registerColor, editorWidgetBackground, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { SuggestDetailsWidget, canExpandCompletionItem, SuggestDetailsOverlay } from './suggestWidgetDetails';
|
||||
import { SuggestWidgetStatus } from 'vs/editor/contrib/suggest/suggestWidgetStatus';
|
||||
import { getAriaId, ItemRenderer } from './suggestWidgetRenderer';
|
||||
import { ResizableHTMLElement } from './resizable';
|
||||
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
|
||||
/**
|
||||
* Suggest widget colors
|
||||
*/
|
||||
export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.'));
|
||||
export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.'));
|
||||
export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hc: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.'));
|
||||
export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.'));
|
||||
export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.'));
|
||||
|
||||
const enum State {
|
||||
Hidden,
|
||||
Loading,
|
||||
Empty,
|
||||
Open,
|
||||
Frozen,
|
||||
Details
|
||||
}
|
||||
|
||||
export interface ISelectedSuggestion {
|
||||
item: CompletionItem;
|
||||
index: number;
|
||||
model: CompletionModel;
|
||||
}
|
||||
|
||||
class PersistedWidgetSize {
|
||||
|
||||
private readonly _key: string;
|
||||
|
||||
constructor(
|
||||
private readonly _service: IStorageService,
|
||||
editor: ICodeEditor
|
||||
) {
|
||||
this._key = `suggestWidget.size/${editor.getEditorType()}/${editor instanceof EmbeddedCodeEditorWidget}`;
|
||||
}
|
||||
|
||||
restore(): dom.Dimension | undefined {
|
||||
const raw = this._service.get(this._key, StorageScope.GLOBAL) ?? '';
|
||||
try {
|
||||
const obj = JSON.parse(raw);
|
||||
if (dom.Dimension.is(obj)) {
|
||||
return dom.Dimension.lift(obj);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
store(size: dom.Dimension) {
|
||||
this._service.store(this._key, JSON.stringify(size), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._service.remove(this._key, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
export class SuggestWidget implements IDisposable {
|
||||
|
||||
private static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading...");
|
||||
private static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions.");
|
||||
|
||||
private state: State = State.Hidden;
|
||||
private isAuto: boolean = false;
|
||||
private loadingTimeout: IDisposable = Disposable.None;
|
||||
private currentSuggestionDetails?: CancelablePromise<void>;
|
||||
private focusedItem?: CompletionItem;
|
||||
private ignoreFocusEvents: boolean = false;
|
||||
private completionModel?: CompletionModel;
|
||||
private _cappedHeight?: { wanted: number, capped: number };
|
||||
|
||||
readonly element: ResizableHTMLElement;
|
||||
private readonly messageElement: HTMLElement;
|
||||
private readonly listElement: HTMLElement;
|
||||
private readonly list: List<CompletionItem>;
|
||||
private readonly status: SuggestWidgetStatus;
|
||||
private readonly _details: SuggestDetailsOverlay;
|
||||
private readonly _contentWidget: SuggestContentWidget;
|
||||
|
||||
private readonly ctxSuggestWidgetVisible: IContextKey<boolean>;
|
||||
private readonly ctxSuggestWidgetDetailsVisible: IContextKey<boolean>;
|
||||
private readonly ctxSuggestWidgetMultipleSuggestions: IContextKey<boolean>;
|
||||
|
||||
private readonly showTimeout = new TimeoutTimer();
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private readonly _persistedSize: PersistedWidgetSize;
|
||||
|
||||
private readonly onDidSelectEmitter = new Emitter<ISelectedSuggestion>();
|
||||
private readonly onDidFocusEmitter = new Emitter<ISelectedSuggestion>();
|
||||
private readonly onDidHideEmitter = new Emitter<this>();
|
||||
private readonly onDidShowEmitter = new Emitter<this>();
|
||||
|
||||
readonly onDidSelect: Event<ISelectedSuggestion> = this.onDidSelectEmitter.event;
|
||||
readonly onDidFocus: Event<ISelectedSuggestion> = this.onDidFocusEmitter.event;
|
||||
readonly onDidHide: Event<this> = this.onDidHideEmitter.event;
|
||||
readonly onDidShow: Event<this> = this.onDidShowEmitter.event;
|
||||
|
||||
private detailsFocusBorderColor?: string;
|
||||
private detailsBorderColor?: string;
|
||||
|
||||
private explainMode: boolean = false;
|
||||
|
||||
private readonly _onDetailsKeydown = new Emitter<IKeyboardEvent>();
|
||||
public readonly onDetailsKeyDown: Event<IKeyboardEvent> = this._onDetailsKeydown.event;
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IContextKeyService _contextKeyService: IContextKeyService,
|
||||
@IThemeService _themeService: IThemeService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
this.element = new ResizableHTMLElement();
|
||||
this.element.domNode.classList.add('editor-widget', 'suggest-widget');
|
||||
|
||||
this._contentWidget = new SuggestContentWidget(this, editor);
|
||||
this._persistedSize = new PersistedWidgetSize(_storageService, editor);
|
||||
|
||||
let persistedSize: dom.Dimension | undefined;
|
||||
let currentSize: dom.Dimension | undefined;
|
||||
let persistHeight = false;
|
||||
let persistWidth = false;
|
||||
this._disposables.add(this.element.onDidWillResize(() => {
|
||||
this._contentWidget.lockPreference();
|
||||
persistedSize = this._persistedSize.restore();
|
||||
currentSize = this.element.size;
|
||||
}));
|
||||
this._disposables.add(this.element.onDidResize(e => {
|
||||
|
||||
this._resize(e.dimension.width, e.dimension.height);
|
||||
|
||||
persistHeight = persistHeight || !!e.north || !!e.south;
|
||||
persistWidth = persistWidth || !!e.east || !!e.west;
|
||||
if (e.done) {
|
||||
// only store width or height value that have changed and also
|
||||
// only store changes that are above a certain threshold
|
||||
const threshold = Math.floor(this.getLayoutInfo().itemHeight / 2);
|
||||
let { width, height } = this.element.size;
|
||||
if (persistedSize && currentSize) {
|
||||
if (!persistHeight || Math.abs(currentSize.height - height) <= threshold) {
|
||||
height = persistedSize.height;
|
||||
}
|
||||
if (!persistWidth || Math.abs(currentSize.width - width) <= threshold) {
|
||||
width = persistedSize.width;
|
||||
}
|
||||
}
|
||||
this._persistedSize.store(new dom.Dimension(width, height));
|
||||
|
||||
// reset working state
|
||||
this._contentWidget.unlockPreference();
|
||||
persistedSize = undefined;
|
||||
currentSize = undefined;
|
||||
persistHeight = false;
|
||||
persistWidth = false;
|
||||
}
|
||||
}));
|
||||
|
||||
this.messageElement = dom.append(this.element.domNode, dom.$('.message'));
|
||||
this.listElement = dom.append(this.element.domNode, dom.$('.tree'));
|
||||
|
||||
const details = instantiationService.createInstance(SuggestDetailsWidget, this.editor);
|
||||
details.onDidClose(this.toggleDetails, this, this._disposables);
|
||||
this._details = new SuggestDetailsOverlay(details, this.editor);
|
||||
|
||||
const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons);
|
||||
applyIconStyle();
|
||||
|
||||
const renderer = instantiationService.createInstance(ItemRenderer, this.editor);
|
||||
this._disposables.add(renderer);
|
||||
this._disposables.add(renderer.onDidToggleDetails(() => this.toggleDetails()));
|
||||
|
||||
this.list = new List('SuggestWidget', this.listElement, {
|
||||
getHeight: (_element: CompletionItem): number => this.getLayoutInfo().itemHeight,
|
||||
getTemplateId: (_element: CompletionItem): string => 'suggestion'
|
||||
}, [renderer], {
|
||||
useShadows: false,
|
||||
mouseSupport: false,
|
||||
accessibilityProvider: {
|
||||
getRole: () => 'option',
|
||||
getAriaLabel: (item: CompletionItem) => {
|
||||
const textLabel = typeof item.completion.label === 'string' ? item.completion.label : item.completion.label.name;
|
||||
if (item.isResolved && this._isDetailsVisible()) {
|
||||
const { documentation, detail } = item.completion;
|
||||
const docs = strings.format(
|
||||
'{0}{1}',
|
||||
detail || '',
|
||||
documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : '');
|
||||
|
||||
return nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, docs: {1}", textLabel, docs);
|
||||
} else {
|
||||
return textLabel;
|
||||
}
|
||||
},
|
||||
getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"),
|
||||
getWidgetRole: () => 'listbox'
|
||||
}
|
||||
});
|
||||
|
||||
this.status = instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode);
|
||||
const applyStatusBarStyle = () => this.element.domNode.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).showStatusBar);
|
||||
applyStatusBarStyle();
|
||||
|
||||
this._disposables.add(attachListStyler(this.list, _themeService, {
|
||||
listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,
|
||||
listInactiveFocusOutline: activeContrastBorder
|
||||
}));
|
||||
this._disposables.add(_themeService.onDidColorThemeChange(t => this.onThemeChange(t)));
|
||||
this.onThemeChange(_themeService.getColorTheme());
|
||||
|
||||
this._disposables.add(this.list.onMouseDown(e => this.onListMouseDownOrTap(e)));
|
||||
this._disposables.add(this.list.onTap(e => this.onListMouseDownOrTap(e)));
|
||||
this._disposables.add(this.list.onDidChangeSelection(e => this.onListSelection(e)));
|
||||
this._disposables.add(this.list.onDidChangeFocus(e => this.onListFocus(e)));
|
||||
this._disposables.add(this.editor.onDidChangeCursorSelection(() => this.onCursorSelectionChanged()));
|
||||
this._disposables.add(this.editor.onDidChangeConfiguration(e => {
|
||||
if (e.hasChanged(EditorOption.suggest)) {
|
||||
applyStatusBarStyle();
|
||||
applyIconStyle();
|
||||
}
|
||||
}));
|
||||
|
||||
this.ctxSuggestWidgetVisible = SuggestContext.Visible.bindTo(_contextKeyService);
|
||||
this.ctxSuggestWidgetDetailsVisible = SuggestContext.DetailsVisible.bindTo(_contextKeyService);
|
||||
this.ctxSuggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(_contextKeyService);
|
||||
|
||||
|
||||
this._disposables.add(dom.addStandardDisposableListener(this._details.widget.domNode, 'keydown', e => {
|
||||
this._onDetailsKeydown.fire(e);
|
||||
}));
|
||||
|
||||
this._disposables.add(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e)));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._details.widget.dispose();
|
||||
this._details.dispose();
|
||||
this.list.dispose();
|
||||
this.status.dispose();
|
||||
this._disposables.dispose();
|
||||
this.loadingTimeout.dispose();
|
||||
this.showTimeout.dispose();
|
||||
this._contentWidget.dispose();
|
||||
this.element.dispose();
|
||||
}
|
||||
|
||||
private onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
|
||||
if (this._details.widget.domNode.contains(mouseEvent.target.element)) {
|
||||
// Clicking inside details
|
||||
this._details.widget.domNode.focus();
|
||||
} else {
|
||||
// Clicking outside details and inside suggest
|
||||
if (this.element.domNode.contains(mouseEvent.target.element)) {
|
||||
this.editor.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onCursorSelectionChanged(): void {
|
||||
if (this.state !== State.Hidden) {
|
||||
this._contentWidget.layout();
|
||||
}
|
||||
}
|
||||
|
||||
private onListMouseDownOrTap(e: IListMouseEvent<CompletionItem> | IListGestureEvent<CompletionItem>): void {
|
||||
if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent stealing browser focus from the editor
|
||||
e.browserEvent.preventDefault();
|
||||
e.browserEvent.stopPropagation();
|
||||
|
||||
this.select(e.element, e.index);
|
||||
}
|
||||
|
||||
private onListSelection(e: IListEvent<CompletionItem>): void {
|
||||
if (!e.elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.select(e.elements[0], e.indexes[0]);
|
||||
}
|
||||
|
||||
private select(item: CompletionItem, index: number): void {
|
||||
const completionModel = this.completionModel;
|
||||
|
||||
if (!completionModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onDidSelectEmitter.fire({ item, index, model: completionModel });
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
private onThemeChange(theme: IColorTheme) {
|
||||
const backgroundColor = theme.getColor(editorSuggestWidgetBackground);
|
||||
if (backgroundColor) {
|
||||
this.element.domNode.style.backgroundColor = backgroundColor.toString();
|
||||
this.messageElement.style.backgroundColor = backgroundColor.toString();
|
||||
this._details.widget.domNode.style.backgroundColor = backgroundColor.toString();
|
||||
}
|
||||
const borderColor = theme.getColor(editorSuggestWidgetBorder);
|
||||
if (borderColor) {
|
||||
this.element.domNode.style.borderColor = borderColor.toString();
|
||||
this.messageElement.style.borderColor = borderColor.toString();
|
||||
this.status.element.style.borderTopColor = borderColor.toString();
|
||||
this._details.widget.domNode.style.borderColor = borderColor.toString();
|
||||
this.detailsBorderColor = borderColor.toString();
|
||||
}
|
||||
const focusBorderColor = theme.getColor(focusBorder);
|
||||
if (focusBorderColor) {
|
||||
this.detailsFocusBorderColor = focusBorderColor.toString();
|
||||
}
|
||||
this._details.widget.borderWidth = theme.type === 'hc' ? 2 : 1;
|
||||
}
|
||||
|
||||
private onListFocus(e: IListEvent<CompletionItem>): void {
|
||||
if (this.ignoreFocusEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.elements.length) {
|
||||
if (this.currentSuggestionDetails) {
|
||||
this.currentSuggestionDetails.cancel();
|
||||
this.currentSuggestionDetails = undefined;
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
|
||||
this.editor.setAriaOptions({ activeDescendant: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.completionModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = e.elements[0];
|
||||
const index = e.indexes[0];
|
||||
|
||||
if (item !== this.focusedItem) {
|
||||
|
||||
this.currentSuggestionDetails?.cancel();
|
||||
this.currentSuggestionDetails = undefined;
|
||||
|
||||
this.focusedItem = item;
|
||||
|
||||
this.list.reveal(index);
|
||||
|
||||
this.currentSuggestionDetails = createCancelablePromise(async token => {
|
||||
const loading = disposableTimeout(() => {
|
||||
if (this._isDetailsVisible()) {
|
||||
this.showDetails(true);
|
||||
}
|
||||
}, 250);
|
||||
token.onCancellationRequested(() => loading.dispose());
|
||||
const result = await item.resolve(token);
|
||||
loading.dispose();
|
||||
return result;
|
||||
});
|
||||
|
||||
this.currentSuggestionDetails.then(() => {
|
||||
if (index >= this.list.length || item !== this.list.element(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._isDetailsVisible()) {
|
||||
this.showDetails(false);
|
||||
} else {
|
||||
this.element.domNode.classList.remove('docs-side');
|
||||
}
|
||||
|
||||
this.editor.setAriaOptions({ activeDescendant: getAriaId(index) });
|
||||
}).catch(onUnexpectedError);
|
||||
}
|
||||
|
||||
// emit an event
|
||||
this.onDidFocusEmitter.fire({ item, index, model: this.completionModel });
|
||||
}
|
||||
|
||||
private _setState(state: State): void {
|
||||
|
||||
if (this.state === state) {
|
||||
return;
|
||||
}
|
||||
this.state = state;
|
||||
|
||||
this.element.domNode.classList.toggle('frozen', state === State.Frozen);
|
||||
this.element.domNode.classList.remove('message');
|
||||
|
||||
switch (state) {
|
||||
case State.Hidden:
|
||||
dom.hide(this.messageElement, this.listElement, this.status.element);
|
||||
this._details.hide(true);
|
||||
this._contentWidget.hide();
|
||||
this.ctxSuggestWidgetVisible.reset();
|
||||
this.ctxSuggestWidgetMultipleSuggestions.reset();
|
||||
this.element.domNode.classList.remove('visible');
|
||||
this.list.splice(0, this.list.length);
|
||||
this.focusedItem = undefined;
|
||||
this._cappedHeight = undefined;
|
||||
this.explainMode = false;
|
||||
break;
|
||||
case State.Loading:
|
||||
this.element.domNode.classList.add('message');
|
||||
this.messageElement.textContent = SuggestWidget.LOADING_MESSAGE;
|
||||
dom.hide(this.listElement, this.status.element);
|
||||
dom.show(this.messageElement);
|
||||
this._details.hide();
|
||||
this._show();
|
||||
this.focusedItem = undefined;
|
||||
break;
|
||||
case State.Empty:
|
||||
this.element.domNode.classList.add('message');
|
||||
this.messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;
|
||||
dom.hide(this.listElement, this.status.element);
|
||||
dom.show(this.messageElement);
|
||||
this._details.hide();
|
||||
this._show();
|
||||
this.focusedItem = undefined;
|
||||
break;
|
||||
case State.Open:
|
||||
dom.hide(this.messageElement);
|
||||
dom.show(this.listElement, this.status.element);
|
||||
this._show();
|
||||
break;
|
||||
case State.Frozen:
|
||||
dom.hide(this.messageElement);
|
||||
dom.show(this.listElement, this.status.element);
|
||||
this._show();
|
||||
break;
|
||||
case State.Details:
|
||||
dom.hide(this.messageElement);
|
||||
dom.show(this.listElement, this.status.element);
|
||||
this._details.show();
|
||||
this._show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _show(): void {
|
||||
this._contentWidget.show();
|
||||
this._layout(this._persistedSize.restore());
|
||||
this.ctxSuggestWidgetVisible.set(true);
|
||||
|
||||
this.showTimeout.cancelAndSet(() => {
|
||||
this.element.domNode.classList.add('visible');
|
||||
this.onDidShowEmitter.fire(this);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
showTriggered(auto: boolean, delay: number) {
|
||||
if (this.state !== State.Hidden) {
|
||||
return;
|
||||
}
|
||||
this._contentWidget.setPosition(this.editor.getPosition());
|
||||
this.isAuto = !!auto;
|
||||
|
||||
if (!this.isAuto) {
|
||||
this.loadingTimeout = disposableTimeout(() => this._setState(State.Loading), delay);
|
||||
}
|
||||
}
|
||||
|
||||
showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean): void {
|
||||
|
||||
this._contentWidget.setPosition(this.editor.getPosition());
|
||||
this.loadingTimeout.dispose();
|
||||
|
||||
this.currentSuggestionDetails?.cancel();
|
||||
this.currentSuggestionDetails = undefined;
|
||||
|
||||
if (this.completionModel !== completionModel) {
|
||||
this.completionModel = completionModel;
|
||||
}
|
||||
|
||||
if (isFrozen && this.state !== State.Empty && this.state !== State.Hidden) {
|
||||
this._setState(State.Frozen);
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCount = this.completionModel.items.length;
|
||||
const isEmpty = visibleCount === 0;
|
||||
this.ctxSuggestWidgetMultipleSuggestions.set(visibleCount > 1);
|
||||
|
||||
if (isEmpty) {
|
||||
this._setState(isAuto ? State.Hidden : State.Empty);
|
||||
this.completionModel = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusedItem = undefined;
|
||||
this.list.splice(0, this.list.length, this.completionModel.items);
|
||||
this._setState(isFrozen ? State.Frozen : State.Open);
|
||||
this.list.reveal(selectionIndex, 0);
|
||||
this.list.setFocus([selectionIndex]);
|
||||
|
||||
this._layout(this.element.size);
|
||||
// Reset focus border
|
||||
if (this.detailsBorderColor) {
|
||||
this._details.widget.domNode.style.borderColor = this.detailsBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
selectNextPage(): boolean {
|
||||
switch (this.state) {
|
||||
case State.Hidden:
|
||||
return false;
|
||||
case State.Details:
|
||||
this._details.widget.pageDown();
|
||||
return true;
|
||||
case State.Loading:
|
||||
return !this.isAuto;
|
||||
default:
|
||||
this.list.focusNextPage();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
selectNext(): boolean {
|
||||
switch (this.state) {
|
||||
case State.Hidden:
|
||||
return false;
|
||||
case State.Loading:
|
||||
return !this.isAuto;
|
||||
default:
|
||||
this.list.focusNext(1, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
selectLast(): boolean {
|
||||
switch (this.state) {
|
||||
case State.Hidden:
|
||||
return false;
|
||||
case State.Details:
|
||||
this._details.widget.scrollBottom();
|
||||
return true;
|
||||
case State.Loading:
|
||||
return !this.isAuto;
|
||||
default:
|
||||
this.list.focusLast();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
selectPreviousPage(): boolean {
|
||||
switch (this.state) {
|
||||
case State.Hidden:
|
||||
return false;
|
||||
case State.Details:
|
||||
this._details.widget.pageUp();
|
||||
return true;
|
||||
case State.Loading:
|
||||
return !this.isAuto;
|
||||
default:
|
||||
this.list.focusPreviousPage();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
selectPrevious(): boolean {
|
||||
switch (this.state) {
|
||||
case State.Hidden:
|
||||
return false;
|
||||
case State.Loading:
|
||||
return !this.isAuto;
|
||||
default:
|
||||
this.list.focusPrevious(1, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
selectFirst(): boolean {
|
||||
switch (this.state) {
|
||||
case State.Hidden:
|
||||
return false;
|
||||
case State.Details:
|
||||
this._details.widget.scrollTop();
|
||||
return true;
|
||||
case State.Loading:
|
||||
return !this.isAuto;
|
||||
default:
|
||||
this.list.focusFirst();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getFocusedItem(): ISelectedSuggestion | undefined {
|
||||
if (this.state !== State.Hidden
|
||||
&& this.state !== State.Empty
|
||||
&& this.state !== State.Loading
|
||||
&& this.completionModel
|
||||
) {
|
||||
|
||||
return {
|
||||
item: this.list.getFocusedElements()[0],
|
||||
index: this.list.getFocus()[0],
|
||||
model: this.completionModel
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
toggleDetailsFocus(): void {
|
||||
if (this.state === State.Details) {
|
||||
this._setState(State.Open);
|
||||
if (this.detailsBorderColor) {
|
||||
this._details.widget.domNode.style.borderColor = this.detailsBorderColor;
|
||||
}
|
||||
} else if (this.state === State.Open && this._isDetailsVisible()) {
|
||||
this._setState(State.Details);
|
||||
if (this.detailsFocusBorderColor) {
|
||||
this._details.widget.domNode.style.borderColor = this.detailsFocusBorderColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleDetails(): void {
|
||||
if (this._isDetailsVisible()) {
|
||||
// hide details widget
|
||||
this.ctxSuggestWidgetDetailsVisible.set(false);
|
||||
this._setDetailsVisible(false);
|
||||
this._details.hide();
|
||||
this.element.domNode.classList.remove('shows-details');
|
||||
|
||||
} else if (canExpandCompletionItem(this.list.getFocusedElements()[0]) && (this.state === State.Open || this.state === State.Details || this.state === State.Frozen)) {
|
||||
// show details widget (iff possible)
|
||||
this.ctxSuggestWidgetDetailsVisible.set(true);
|
||||
this._setDetailsVisible(true);
|
||||
this.showDetails(false);
|
||||
}
|
||||
}
|
||||
|
||||
showDetails(loading: boolean): void {
|
||||
this._details.show();
|
||||
if (loading) {
|
||||
this._details.widget.renderLoading();
|
||||
} else {
|
||||
this._details.widget.renderItem(this.list.getFocusedElements()[0], this.explainMode);
|
||||
}
|
||||
this._positionDetails();
|
||||
this.editor.focus();
|
||||
this.element.domNode.classList.add('shows-details');
|
||||
}
|
||||
|
||||
toggleExplainMode(): void {
|
||||
if (this.list.getFocusedElements()[0] && this._isDetailsVisible()) {
|
||||
this.explainMode = !this.explainMode;
|
||||
this.showDetails(false);
|
||||
}
|
||||
}
|
||||
|
||||
resetPersistedSize(): void {
|
||||
this._persistedSize.reset();
|
||||
}
|
||||
|
||||
hideWidget(): void {
|
||||
this.loadingTimeout.dispose();
|
||||
this._setState(State.Hidden);
|
||||
this.onDidHideEmitter.fire(this);
|
||||
}
|
||||
|
||||
isFrozen(): boolean {
|
||||
return this.state === State.Frozen;
|
||||
}
|
||||
|
||||
_afterRender(position: ContentWidgetPositionPreference | null) {
|
||||
if (position === null) {
|
||||
if (this._isDetailsVisible()) {
|
||||
this._details.hide(); //todo@jrieken soft-hide
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.state === State.Empty || this.state === State.Loading) {
|
||||
// no special positioning when widget isn't showing list
|
||||
return;
|
||||
}
|
||||
if (this._isDetailsVisible()) {
|
||||
this._details.show();
|
||||
}
|
||||
this._positionDetails();
|
||||
}
|
||||
|
||||
private _layout(size: dom.Dimension | undefined): void {
|
||||
if (!this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
if (!this.editor.getDomNode()) {
|
||||
// happens when running tests
|
||||
return;
|
||||
}
|
||||
|
||||
let height = size?.height;
|
||||
let width = size?.width;
|
||||
|
||||
const bodyBox = dom.getClientArea(document.body);
|
||||
const info = this.getLayoutInfo();
|
||||
|
||||
// status bar
|
||||
this.status.element.style.lineHeight = `${info.itemHeight}px`;
|
||||
|
||||
if (this.state === State.Empty || this.state === State.Loading) {
|
||||
// showing a message only
|
||||
height = info.itemHeight + info.borderHeight;
|
||||
width = 230;
|
||||
this.element.enableSashes(false, false, false, false);
|
||||
this.element.minSize = this.element.maxSize = new dom.Dimension(width, height);
|
||||
this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW);
|
||||
|
||||
} else {
|
||||
// showing items
|
||||
|
||||
// width math
|
||||
const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding;
|
||||
if (width === undefined) {
|
||||
width = 430;
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
const preferredWidth = this.completionModel ? this.completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width;
|
||||
|
||||
// height math
|
||||
const fullHeight = info.statusBarHeight + this.list.contentHeight + info.borderHeight;
|
||||
const preferredHeight = info.statusBarHeight + 12 * info.itemHeight + info.borderHeight;
|
||||
const minHeight = info.itemHeight + info.statusBarHeight;
|
||||
const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode());
|
||||
const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition());
|
||||
const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height;
|
||||
const maxHeightBelow = Math.min(bodyBox.height - cursorBottom - info.verticalPadding, fullHeight);
|
||||
const maxHeightAbove = Math.min(editorBox.top + cursorBox.top - info.verticalPadding, fullHeight);
|
||||
let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight);
|
||||
|
||||
if (height && height === this._cappedHeight?.capped) {
|
||||
// Restore the old (wanted) height when the current
|
||||
// height is capped to fit
|
||||
height = this._cappedHeight.wanted;
|
||||
}
|
||||
|
||||
if (height === undefined) {
|
||||
height = Math.min(preferredHeight, fullHeight);
|
||||
}
|
||||
if (height < minHeight) {
|
||||
height = minHeight;
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
}
|
||||
|
||||
if (height > maxHeightBelow) {
|
||||
this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE);
|
||||
this.element.enableSashes(true, true, false, false);
|
||||
maxHeight = maxHeightAbove;
|
||||
|
||||
} else {
|
||||
this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW);
|
||||
this.element.enableSashes(false, true, true, false);
|
||||
maxHeight = maxHeightBelow;
|
||||
}
|
||||
this.element.preferredSize = new dom.Dimension(preferredWidth, preferredHeight);
|
||||
this.element.maxSize = new dom.Dimension(maxWidth, maxHeight);
|
||||
this.element.minSize = new dom.Dimension(220, minHeight);
|
||||
|
||||
// Know when the height was capped to fit and remember
|
||||
// the wanted height for later. This is required when going
|
||||
// left to widen suggestions.
|
||||
this._cappedHeight = size && height === fullHeight
|
||||
? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height }
|
||||
: undefined;
|
||||
}
|
||||
this._resize(width, height);
|
||||
}
|
||||
|
||||
private _resize(width: number, height: number): void {
|
||||
|
||||
const { width: maxWidth, height: maxHeight } = this.element.maxSize;
|
||||
width = Math.min(maxWidth, width);
|
||||
height = Math.min(maxHeight, height);
|
||||
|
||||
const { statusBarHeight } = this.getLayoutInfo();
|
||||
this.list.layout(height - statusBarHeight, width);
|
||||
this.listElement.style.height = `${height - statusBarHeight}px`;
|
||||
this.element.layout(height, width);
|
||||
this._contentWidget.layout();
|
||||
|
||||
this._positionDetails();
|
||||
}
|
||||
|
||||
private _positionDetails(): void {
|
||||
if (this._isDetailsVisible()) {
|
||||
this._details.placeAtAnchor(this.element.domNode);
|
||||
}
|
||||
}
|
||||
|
||||
getLayoutInfo() {
|
||||
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
|
||||
const itemHeight = this.editor.getOption(EditorOption.suggestLineHeight) || fontInfo.lineHeight;
|
||||
const statusBarHeight = !this.editor.getOption(EditorOption.suggest).showStatusBar || this.state === State.Empty || this.state === State.Loading ? 0 : itemHeight;
|
||||
const borderWidth = this._details.widget.borderWidth;
|
||||
const borderHeight = 2 * borderWidth;
|
||||
|
||||
return {
|
||||
itemHeight,
|
||||
statusBarHeight,
|
||||
borderWidth,
|
||||
borderHeight,
|
||||
typicalHalfwidthCharacterWidth: fontInfo.typicalHalfwidthCharacterWidth,
|
||||
verticalPadding: 22,
|
||||
horizontalPadding: 14
|
||||
};
|
||||
}
|
||||
|
||||
private _isDetailsVisible(): boolean {
|
||||
return this._storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, false);
|
||||
}
|
||||
|
||||
private _setDetailsVisible(value: boolean) {
|
||||
this._storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
export class SuggestContentWidget implements IContentWidget {
|
||||
|
||||
readonly allowEditorOverflow = true;
|
||||
readonly suppressMouseDown = false;
|
||||
|
||||
private _position?: IPosition | null;
|
||||
private _preference?: ContentWidgetPositionPreference;
|
||||
private _preferenceLocked = false;
|
||||
|
||||
private _added: boolean = false;
|
||||
private _hidden: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _widget: SuggestWidget,
|
||||
private readonly _editor: ICodeEditor
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
if (this._added) {
|
||||
this._added = false;
|
||||
this._editor.removeContentWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'editor.widget.suggestWidget';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._widget.element.domNode;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this._hidden = false;
|
||||
if (!this._added) {
|
||||
this._added = true;
|
||||
this._editor.addContentWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (!this._hidden) {
|
||||
this._hidden = true;
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition | null {
|
||||
if (this._hidden || !this._position || !this._preference) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
position: this._position,
|
||||
preference: [this._preference]
|
||||
};
|
||||
}
|
||||
|
||||
beforeRender() {
|
||||
const { height, width } = this._widget.element.size;
|
||||
const { borderWidth, horizontalPadding } = this._widget.getLayoutInfo();
|
||||
return new dom.Dimension(width + 2 * borderWidth + horizontalPadding, height + 2 * borderWidth);
|
||||
}
|
||||
|
||||
afterRender(position: ContentWidgetPositionPreference | null) {
|
||||
this._widget._afterRender(position);
|
||||
}
|
||||
|
||||
setPreference(preference: ContentWidgetPositionPreference) {
|
||||
if (!this._preferenceLocked) {
|
||||
this._preference = preference;
|
||||
}
|
||||
}
|
||||
|
||||
lockPreference() {
|
||||
this._preferenceLocked = true;
|
||||
}
|
||||
|
||||
unlockPreference() {
|
||||
this._preferenceLocked = false;
|
||||
}
|
||||
|
||||
setPosition(position: IPosition | null): void {
|
||||
this._position = position;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground);
|
||||
if (matchHighlight) {
|
||||
collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`);
|
||||
}
|
||||
const foreground = theme.getColor(editorSuggestWidgetForeground);
|
||||
if (foreground) {
|
||||
collector.addRule(`.monaco-editor .suggest-widget, .monaco-editor .suggest-details { color: ${foreground}; }`);
|
||||
}
|
||||
|
||||
const link = theme.getColor(textLinkForeground);
|
||||
if (link) {
|
||||
collector.addRule(`.monaco-editor .suggest-details a { color: ${link}; }`);
|
||||
}
|
||||
|
||||
const codeBackground = theme.getColor(textCodeBlockBackground);
|
||||
if (codeBackground) {
|
||||
collector.addRule(`.monaco-editor .suggest-details code { background-color: ${codeBackground}; }`);
|
||||
}
|
||||
});
|
||||
439
lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts
Normal file
439
lib/vscode/src/vs/editor/contrib/suggest/suggestWidgetDetails.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { ICodeEditor, IOverlayWidget } from 'vs/editor/browser/editorBrowser';
|
||||
import { CompletionItem } from './suggest';
|
||||
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ResizableHTMLElement } from 'vs/editor/contrib/suggest/resizable';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export function canExpandCompletionItem(item: CompletionItem | undefined): boolean {
|
||||
return !!item && Boolean(item.completion.documentation || item.completion.detail && item.completion.detail !== item.completion.label);
|
||||
}
|
||||
|
||||
export class SuggestDetailsWidget {
|
||||
|
||||
readonly domNode: HTMLDivElement;
|
||||
|
||||
private readonly _onDidClose = new Emitter<void>();
|
||||
readonly onDidClose: Event<void> = this._onDidClose.event;
|
||||
|
||||
private readonly _onDidChangeContents = new Emitter<this>();
|
||||
readonly onDidChangeContents: Event<this> = this._onDidChangeContents.event;
|
||||
|
||||
private readonly _close: HTMLElement;
|
||||
private readonly _scrollbar: DomScrollableElement;
|
||||
private readonly _body: HTMLElement;
|
||||
private readonly _header: HTMLElement;
|
||||
private readonly _type: HTMLElement;
|
||||
private readonly _docs: HTMLElement;
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private readonly _markdownRenderer: MarkdownRenderer;
|
||||
private readonly _renderDisposeable = new DisposableStore();
|
||||
private _borderWidth: number = 1;
|
||||
private _size = new dom.Dimension(330, 0);
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
) {
|
||||
this.domNode = dom.$('.suggest-details');
|
||||
this.domNode.classList.add('no-docs');
|
||||
|
||||
this._markdownRenderer = instaService.createInstance(MarkdownRenderer, { editor: _editor });
|
||||
|
||||
this._body = dom.$('.body');
|
||||
|
||||
this._scrollbar = new DomScrollableElement(this._body, {});
|
||||
dom.append(this.domNode, this._scrollbar.getDomNode());
|
||||
this._disposables.add(this._scrollbar);
|
||||
|
||||
this._header = dom.append(this._body, dom.$('.header'));
|
||||
this._close = dom.append(this._header, dom.$('span' + Codicon.close.cssSelector));
|
||||
this._close.title = nls.localize('details.close', "Close");
|
||||
this._type = dom.append(this._header, dom.$('p.type'));
|
||||
|
||||
this._docs = dom.append(this._body, dom.$('p.docs'));
|
||||
|
||||
this._configureFont();
|
||||
|
||||
this._disposables.add(this._editor.onDidChangeConfiguration(e => {
|
||||
if (e.hasChanged(EditorOption.fontInfo)) {
|
||||
this._configureFont();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this._renderDisposeable.dispose();
|
||||
}
|
||||
|
||||
private _configureFont(): void {
|
||||
const options = this._editor.getOptions();
|
||||
const fontInfo = options.get(EditorOption.fontInfo);
|
||||
const fontFamily = fontInfo.fontFamily;
|
||||
const fontSize = options.get(EditorOption.suggestFontSize) || fontInfo.fontSize;
|
||||
const lineHeight = options.get(EditorOption.suggestLineHeight) || fontInfo.lineHeight;
|
||||
const fontWeight = fontInfo.fontWeight;
|
||||
const fontSizePx = `${fontSize}px`;
|
||||
const lineHeightPx = `${lineHeight}px`;
|
||||
|
||||
this.domNode.style.fontSize = fontSizePx;
|
||||
this.domNode.style.fontWeight = fontWeight;
|
||||
this.domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings;
|
||||
this._type.style.fontFamily = fontFamily;
|
||||
this._close.style.height = lineHeightPx;
|
||||
this._close.style.width = lineHeightPx;
|
||||
}
|
||||
|
||||
getLayoutInfo() {
|
||||
const lineHeight = this._editor.getOption(EditorOption.suggestLineHeight) || this._editor.getOption(EditorOption.fontInfo).lineHeight;
|
||||
const borderWidth = this._borderWidth;
|
||||
const borderHeight = borderWidth * 2;
|
||||
return {
|
||||
lineHeight,
|
||||
borderWidth,
|
||||
borderHeight,
|
||||
verticalPadding: 22,
|
||||
horizontalPadding: 14
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
renderLoading(): void {
|
||||
this._type.textContent = nls.localize('loading', "Loading...");
|
||||
this._docs.textContent = '';
|
||||
this.domNode.classList.remove('no-docs', 'no-type');
|
||||
this.layout(this.size.width, this.getLayoutInfo().lineHeight * 2);
|
||||
this._onDidChangeContents.fire(this);
|
||||
}
|
||||
|
||||
renderItem(item: CompletionItem, explainMode: boolean): void {
|
||||
this._renderDisposeable.clear();
|
||||
|
||||
let { detail, documentation } = item.completion;
|
||||
|
||||
if (explainMode) {
|
||||
let md = '';
|
||||
md += `score: ${item.score[0]}${item.word ? `, compared '${item.completion.filterText && (item.completion.filterText + ' (filterText)') || item.completion.label}' with '${item.word}'` : ' (no prefix)'}\n`;
|
||||
md += `distance: ${item.distance}, see localityBonus-setting\n`;
|
||||
md += `index: ${item.idx}, based on ${item.completion.sortText && `sortText: "${item.completion.sortText}"` || 'label'}\n`;
|
||||
documentation = new MarkdownString().appendCodeblock('empty', md);
|
||||
detail = `Provider: ${item.provider._debugDisplayName}`;
|
||||
}
|
||||
|
||||
if (!explainMode && !canExpandCompletionItem(item)) {
|
||||
this.clearContents();
|
||||
return;
|
||||
}
|
||||
|
||||
this.domNode.classList.remove('no-docs', 'no-type');
|
||||
|
||||
// --- details
|
||||
|
||||
if (detail) {
|
||||
const cappedDetail = detail.length > 100000 ? `${detail.substr(0, 100000)}…` : detail;
|
||||
this._type.textContent = cappedDetail;
|
||||
this._type.title = cappedDetail;
|
||||
dom.show(this._type);
|
||||
this._type.classList.toggle('auto-wrap', !/\r?\n^\s+/gmi.test(cappedDetail));
|
||||
} else {
|
||||
dom.clearNode(this._type);
|
||||
this._type.title = '';
|
||||
dom.hide(this._type);
|
||||
this.domNode.classList.add('no-type');
|
||||
}
|
||||
|
||||
// --- documentation
|
||||
dom.clearNode(this._docs);
|
||||
if (typeof documentation === 'string') {
|
||||
this._docs.classList.remove('markdown-docs');
|
||||
this._docs.textContent = documentation;
|
||||
|
||||
} else if (documentation) {
|
||||
this._docs.classList.add('markdown-docs');
|
||||
dom.clearNode(this._docs);
|
||||
const renderedContents = this._markdownRenderer.render(documentation);
|
||||
this._docs.appendChild(renderedContents.element);
|
||||
this._renderDisposeable.add(renderedContents);
|
||||
this._renderDisposeable.add(this._markdownRenderer.onDidRenderCodeBlock(() => {
|
||||
this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight);
|
||||
this._onDidChangeContents.fire(this);
|
||||
}));
|
||||
}
|
||||
|
||||
this.domNode.style.userSelect = 'text';
|
||||
this.domNode.tabIndex = -1;
|
||||
|
||||
this._close.onmousedown = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
this._close.onclick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._onDidClose.fire();
|
||||
};
|
||||
|
||||
this._body.scrollTop = 0;
|
||||
|
||||
this.layout(this._size.width, this._type.clientHeight + this._docs.clientHeight);
|
||||
this._onDidChangeContents.fire(this);
|
||||
}
|
||||
|
||||
clearContents() {
|
||||
this.domNode.classList.add('no-docs');
|
||||
this._type.textContent = '';
|
||||
this._docs.textContent = '';
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
layout(width: number, height: number): void {
|
||||
const newSize = new dom.Dimension(width, height);
|
||||
if (!dom.Dimension.equals(newSize, this._size)) {
|
||||
this._size = newSize;
|
||||
dom.size(this.domNode, width, height);
|
||||
}
|
||||
this._scrollbar.scanDomNode();
|
||||
}
|
||||
|
||||
scrollDown(much = 8): void {
|
||||
this._body.scrollTop += much;
|
||||
}
|
||||
|
||||
scrollUp(much = 8): void {
|
||||
this._body.scrollTop -= much;
|
||||
}
|
||||
|
||||
scrollTop(): void {
|
||||
this._body.scrollTop = 0;
|
||||
}
|
||||
|
||||
scrollBottom(): void {
|
||||
this._body.scrollTop = this._body.scrollHeight;
|
||||
}
|
||||
|
||||
pageDown(): void {
|
||||
this.scrollDown(80);
|
||||
}
|
||||
|
||||
pageUp(): void {
|
||||
this.scrollUp(80);
|
||||
}
|
||||
|
||||
set borderWidth(width: number) {
|
||||
this._borderWidth = width;
|
||||
}
|
||||
|
||||
get borderWidth() {
|
||||
return this._borderWidth;
|
||||
}
|
||||
}
|
||||
|
||||
interface TopLeftPosition {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
export class SuggestDetailsOverlay implements IOverlayWidget {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _resizable: ResizableHTMLElement;
|
||||
|
||||
private _added: boolean = false;
|
||||
private _anchorBox?: dom.IDomNodePagePosition;
|
||||
private _userSize?: dom.Dimension;
|
||||
private _topLeft?: TopLeftPosition;
|
||||
|
||||
constructor(
|
||||
readonly widget: SuggestDetailsWidget,
|
||||
private readonly _editor: ICodeEditor
|
||||
) {
|
||||
|
||||
this._resizable = new ResizableHTMLElement();
|
||||
this._resizable.domNode.classList.add('suggest-details-container');
|
||||
this._resizable.domNode.appendChild(widget.domNode);
|
||||
this._resizable.enableSashes(false, true, true, false);
|
||||
|
||||
let topLeftNow: TopLeftPosition | undefined;
|
||||
let sizeNow: dom.Dimension | undefined;
|
||||
let deltaTop: number = 0;
|
||||
let deltaLeft: number = 0;
|
||||
this._disposables.add(this._resizable.onDidWillResize(() => {
|
||||
topLeftNow = this._topLeft;
|
||||
sizeNow = this._resizable.size;
|
||||
}));
|
||||
|
||||
this._disposables.add(this._resizable.onDidResize(e => {
|
||||
if (topLeftNow && sizeNow) {
|
||||
this.widget.layout(e.dimension.width, e.dimension.height);
|
||||
|
||||
let updateTopLeft = false;
|
||||
if (e.west) {
|
||||
deltaLeft = sizeNow.width - e.dimension.width;
|
||||
updateTopLeft = true;
|
||||
}
|
||||
if (e.north) {
|
||||
deltaTop = sizeNow.height - e.dimension.height;
|
||||
updateTopLeft = true;
|
||||
}
|
||||
if (updateTopLeft) {
|
||||
this._applyTopLeft({
|
||||
top: topLeftNow.top + deltaTop,
|
||||
left: topLeftNow.left + deltaLeft,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (e.done) {
|
||||
topLeftNow = undefined;
|
||||
sizeNow = undefined;
|
||||
deltaTop = 0;
|
||||
deltaLeft = 0;
|
||||
this._userSize = e.dimension;
|
||||
}
|
||||
}));
|
||||
|
||||
this._disposables.add(this.widget.onDidChangeContents(() => {
|
||||
if (this._anchorBox) {
|
||||
this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'suggest.details';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._resizable.domNode;
|
||||
}
|
||||
|
||||
getPosition(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
if (!this._added) {
|
||||
this._editor.addOverlayWidget(this);
|
||||
this.getDomNode().style.position = 'fixed';
|
||||
this._added = true;
|
||||
}
|
||||
}
|
||||
|
||||
hide(sessionEnded: boolean = false): void {
|
||||
if (this._added) {
|
||||
this._editor.removeOverlayWidget(this);
|
||||
this._added = false;
|
||||
this._anchorBox = undefined;
|
||||
this._topLeft = undefined;
|
||||
}
|
||||
if (sessionEnded) {
|
||||
this._userSize = undefined;
|
||||
this.widget.clearContents();
|
||||
}
|
||||
}
|
||||
|
||||
placeAtAnchor(anchor: HTMLElement) {
|
||||
const anchorBox = dom.getDomNodePagePosition(anchor);
|
||||
this._anchorBox = anchorBox;
|
||||
this._placeAtAnchor(this._anchorBox, this._userSize ?? this.widget.size);
|
||||
}
|
||||
|
||||
_placeAtAnchor(anchorBox: dom.IDomNodePagePosition, size: dom.Dimension) {
|
||||
const bodyBox = dom.getClientArea(document.body);
|
||||
|
||||
const info = this.widget.getLayoutInfo();
|
||||
|
||||
let maxSizeTop: dom.Dimension;
|
||||
let maxSizeBottom: dom.Dimension;
|
||||
let minSize = new dom.Dimension(220, 2 * info.lineHeight);
|
||||
|
||||
let left = 0;
|
||||
let top = anchorBox.top;
|
||||
let bottom = anchorBox.top + anchorBox.height - info.borderHeight;
|
||||
|
||||
let alignAtTop: boolean;
|
||||
let alignEast: boolean;
|
||||
|
||||
// position: EAST, west, south
|
||||
let width = bodyBox.width - (anchorBox.left + anchorBox.width + info.borderWidth + info.horizontalPadding);
|
||||
left = -info.borderWidth + anchorBox.left + anchorBox.width;
|
||||
alignEast = true;
|
||||
maxSizeTop = new dom.Dimension(width, bodyBox.height - anchorBox.top - info.borderHeight - info.verticalPadding);
|
||||
maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top + anchorBox.height - info.borderHeight - info.verticalPadding);
|
||||
|
||||
// find a better place if the widget is wider than there is space available
|
||||
if (size.width > width) {
|
||||
// position: east, WEST, south
|
||||
if (anchorBox.left > width) {
|
||||
// pos = SuggestDetailsPosition.West;
|
||||
width = anchorBox.left - info.borderWidth - info.horizontalPadding;
|
||||
alignEast = false;
|
||||
left = Math.max(info.horizontalPadding, anchorBox.left - size.width - info.borderWidth);
|
||||
maxSizeTop = maxSizeTop.with(width);
|
||||
maxSizeBottom = maxSizeTop.with(undefined, maxSizeBottom.height);
|
||||
}
|
||||
|
||||
// position: east, west, SOUTH
|
||||
if (anchorBox.width > width * 1.3 && bodyBox.height - (anchorBox.top + anchorBox.height) > anchorBox.height) {
|
||||
width = anchorBox.width;
|
||||
left = anchorBox.left;
|
||||
top = -info.borderWidth + anchorBox.top + anchorBox.height;
|
||||
maxSizeTop = new dom.Dimension(anchorBox.width - info.borderHeight, bodyBox.height - anchorBox.top - anchorBox.height - info.verticalPadding);
|
||||
maxSizeBottom = maxSizeTop.with(undefined, anchorBox.top - info.verticalPadding);
|
||||
minSize = minSize.with(maxSizeTop.width);
|
||||
}
|
||||
}
|
||||
|
||||
// top/bottom placement
|
||||
let height = size.height;
|
||||
let maxHeight = Math.max(maxSizeTop.height, maxSizeBottom.height);
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
}
|
||||
let maxSize: dom.Dimension;
|
||||
if (height <= maxSizeTop.height) {
|
||||
alignAtTop = true;
|
||||
maxSize = maxSizeTop;
|
||||
} else {
|
||||
alignAtTop = false;
|
||||
maxSize = maxSizeBottom;
|
||||
}
|
||||
|
||||
this._applyTopLeft({ left, top: alignAtTop ? top : bottom - height });
|
||||
this.getDomNode().style.position = 'fixed';
|
||||
|
||||
this._resizable.enableSashes(!alignAtTop, alignEast, alignAtTop, !alignEast);
|
||||
|
||||
this._resizable.minSize = minSize;
|
||||
this._resizable.maxSize = maxSize;
|
||||
this._resizable.layout(height, Math.min(maxSize.width, size.width));
|
||||
this.widget.layout(this._resizable.size.width, this._resizable.size.height);
|
||||
}
|
||||
|
||||
private _applyTopLeft(topLeft: TopLeftPosition): void {
|
||||
this._topLeft = topLeft;
|
||||
this.getDomNode().style.left = `${this._topLeft.left}px`;
|
||||
this.getDomNode().style.top = `${this._topLeft.top}px`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { createMatches } from 'vs/base/common/filters';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { append, $, hide, show } from 'vs/base/browser/dom';
|
||||
import { IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { CompletionItem } from './suggest';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { CompletionItemKind, completionKindToCssClass, CompletionItemTag } 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 { FileKind } from 'vs/platform/files/common/files';
|
||||
import { flatten } from 'vs/base/common/arrays';
|
||||
import { canExpandCompletionItem } from './suggestWidgetDetails';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
||||
export function getAriaId(index: number): string {
|
||||
return `suggest-aria-id:${index}`;
|
||||
}
|
||||
|
||||
export const suggestMoreInfoIcon = registerIcon('suggest-more-info', Codicon.chevronRight);
|
||||
|
||||
const colorRegExp = /^(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))$/i;
|
||||
|
||||
function extractColor(item: CompletionItem, out: string[]): boolean {
|
||||
const label = typeof item.completion.label === 'string'
|
||||
? item.completion.label
|
||||
: item.completion.label.name;
|
||||
|
||||
if (label.match(colorRegExp)) {
|
||||
out[0] = label;
|
||||
return true;
|
||||
}
|
||||
if (typeof item.completion.documentation === 'string' && item.completion.documentation.match(colorRegExp)) {
|
||||
out[0] = item.completion.documentation;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
export interface ISuggestionTemplateData {
|
||||
root: HTMLElement;
|
||||
|
||||
/**
|
||||
* Flexbox
|
||||
* < ------------- left ------------ > < --- right -- >
|
||||
* <icon><label><signature><qualifier> <type><readmore>
|
||||
*/
|
||||
left: HTMLElement;
|
||||
right: HTMLElement;
|
||||
|
||||
icon: HTMLElement;
|
||||
colorspan: HTMLElement;
|
||||
iconLabel: IconLabel;
|
||||
iconContainer: HTMLElement;
|
||||
parametersLabel: HTMLElement;
|
||||
qualifierLabel: HTMLElement;
|
||||
/**
|
||||
* Showing either `CompletionItem#details` or `CompletionItemLabel#type`
|
||||
*/
|
||||
detailsLabel: HTMLElement;
|
||||
readMore: HTMLElement;
|
||||
disposables: DisposableStore;
|
||||
}
|
||||
|
||||
export class ItemRenderer implements IListRenderer<CompletionItem, ISuggestionTemplateData> {
|
||||
|
||||
private readonly _onDidToggleDetails = new Emitter<void>();
|
||||
readonly onDidToggleDetails: Event<void> = this._onDidToggleDetails.event;
|
||||
|
||||
readonly templateId = 'suggestion';
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@IThemeService private readonly _themeService: IThemeService
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
this._onDidToggleDetails.dispose();
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): ISuggestionTemplateData {
|
||||
const data = <ISuggestionTemplateData>Object.create(null);
|
||||
data.disposables = new DisposableStore();
|
||||
|
||||
data.root = container;
|
||||
data.root.classList.add('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.iconContainer = append(main, $('.icon-label.codicon'));
|
||||
data.left = append(main, $('span.left'));
|
||||
data.right = append(main, $('span.right'));
|
||||
|
||||
data.iconLabel = new IconLabel(data.left, { supportHighlights: true, supportCodicons: true });
|
||||
data.disposables.add(data.iconLabel);
|
||||
|
||||
data.parametersLabel = append(data.left, $('span.signature-label'));
|
||||
data.qualifierLabel = append(data.left, $('span.qualifier-label'));
|
||||
data.detailsLabel = append(data.right, $('span.details-label'));
|
||||
|
||||
data.readMore = append(data.right, $('span.readMore' + suggestMoreInfoIcon.cssSelector));
|
||||
data.readMore.title = nls.localize('readMore', "Read More");
|
||||
|
||||
const configureFont = () => {
|
||||
const options = this._editor.getOptions();
|
||||
const fontInfo = options.get(EditorOption.fontInfo);
|
||||
const fontFamily = fontInfo.fontFamily;
|
||||
const fontFeatureSettings = fontInfo.fontFeatureSettings;
|
||||
const fontSize = options.get(EditorOption.suggestFontSize) || fontInfo.fontSize;
|
||||
const lineHeight = options.get(EditorOption.suggestLineHeight) || fontInfo.lineHeight;
|
||||
const fontWeight = 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.fontFeatureSettings = fontFeatureSettings;
|
||||
main.style.lineHeight = lineHeightPx;
|
||||
data.icon.style.height = lineHeightPx;
|
||||
data.icon.style.width = lineHeightPx;
|
||||
data.readMore.style.height = lineHeightPx;
|
||||
data.readMore.style.width = lineHeightPx;
|
||||
};
|
||||
|
||||
configureFont();
|
||||
|
||||
data.disposables.add(this._editor.onDidChangeConfiguration(e => {
|
||||
if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.suggestFontSize) || e.hasChanged(EditorOption.suggestLineHeight)) {
|
||||
configureFont();
|
||||
}
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(element: CompletionItem, index: number, data: ISuggestionTemplateData): void {
|
||||
const { completion } = element;
|
||||
const textLabel = typeof completion.label === 'string' ? completion.label : completion.label.name;
|
||||
|
||||
data.root.id = getAriaId(index);
|
||||
data.colorspan.style.backgroundColor = '';
|
||||
|
||||
const labelOptions: IIconLabelValueOptions = {
|
||||
labelEscapeNewLines: true,
|
||||
matches: createMatches(element.score)
|
||||
};
|
||||
|
||||
let color: string[] = [];
|
||||
if (completion.kind === CompletionItemKind.Color && extractColor(element, color)) {
|
||||
// special logic for 'color' completion items
|
||||
data.icon.className = 'icon customcolor';
|
||||
data.iconContainer.className = 'icon hide';
|
||||
data.colorspan.style.backgroundColor = color[0];
|
||||
|
||||
} else if (completion.kind === CompletionItemKind.File && this._themeService.getFileIconTheme().hasFileIcons) {
|
||||
// special logic for 'file' completion items
|
||||
data.icon.className = 'icon hide';
|
||||
data.iconContainer.className = 'icon hide';
|
||||
const labelClasses = getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: textLabel }), FileKind.FILE);
|
||||
const detailClasses = getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: completion.detail }), FileKind.FILE);
|
||||
labelOptions.extraClasses = labelClasses.length > detailClasses.length ? labelClasses : detailClasses;
|
||||
|
||||
} else if (completion.kind === CompletionItemKind.Folder && this._themeService.getFileIconTheme().hasFolderIcons) {
|
||||
// special logic for 'folder' completion items
|
||||
data.icon.className = 'icon hide';
|
||||
data.iconContainer.className = 'icon hide';
|
||||
labelOptions.extraClasses = flatten([
|
||||
getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: textLabel }), FileKind.FOLDER),
|
||||
getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: completion.detail }), FileKind.FOLDER)
|
||||
]);
|
||||
} else {
|
||||
// normal icon
|
||||
data.icon.className = 'icon hide';
|
||||
data.iconContainer.className = '';
|
||||
data.iconContainer.classList.add('suggest-icon', ...completionKindToCssClass(completion.kind).split(' '));
|
||||
}
|
||||
|
||||
if (completion.tags && completion.tags.indexOf(CompletionItemTag.Deprecated) >= 0) {
|
||||
labelOptions.extraClasses = (labelOptions.extraClasses || []).concat(['deprecated']);
|
||||
labelOptions.matches = [];
|
||||
}
|
||||
|
||||
data.iconLabel.setLabel(textLabel, undefined, labelOptions);
|
||||
if (typeof completion.label === 'string') {
|
||||
data.parametersLabel.textContent = '';
|
||||
data.qualifierLabel.textContent = '';
|
||||
data.detailsLabel.textContent = (completion.detail || '').replace(/\n.*$/m, '');
|
||||
data.root.classList.add('string-label');
|
||||
data.root.title = '';
|
||||
} else {
|
||||
data.parametersLabel.textContent = (completion.label.parameters || '').replace(/\n.*$/m, '');
|
||||
data.qualifierLabel.textContent = (completion.label.qualifier || '').replace(/\n.*$/m, '');
|
||||
data.detailsLabel.textContent = (completion.label.type || '').replace(/\n.*$/m, '');
|
||||
data.root.classList.remove('string-label');
|
||||
data.root.title = `${textLabel}${completion.label.parameters ?? ''} ${completion.label.qualifier ?? ''} ${completion.label.type ?? ''}`;
|
||||
}
|
||||
|
||||
if (canExpandCompletionItem(element)) {
|
||||
data.right.classList.add('can-expand-details');
|
||||
show(data.readMore);
|
||||
data.readMore.onmousedown = e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
data.readMore.onclick = e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._onDidToggleDetails.fire();
|
||||
};
|
||||
} else {
|
||||
data.right.classList.remove('can-expand-details');
|
||||
hide(data.readMore);
|
||||
data.readMore.onmousedown = null;
|
||||
data.readMore.onclick = null;
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ISuggestionTemplateData): void {
|
||||
templateData.disposables.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IActionViewItemProvider, IAction } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { suggestWidgetStatusbarMenu } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { localize } from 'vs/nls';
|
||||
import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
class StatusBarViewItem extends MenuEntryActionViewItem {
|
||||
|
||||
updateLabel() {
|
||||
const kb = this._keybindingService.lookupKeybinding(this._action.id);
|
||||
if (!kb) {
|
||||
return super.updateLabel();
|
||||
}
|
||||
if (this.label) {
|
||||
this.label.textContent = localize('ddd', '{0} ({1})', this._action.label, StatusBarViewItem.symbolPrintEnter(kb));
|
||||
}
|
||||
}
|
||||
|
||||
static symbolPrintEnter(kb: ResolvedKeybinding) {
|
||||
return kb.getLabel()?.replace(/\benter\b/gi, '\u23CE');
|
||||
}
|
||||
}
|
||||
|
||||
export class SuggestWidgetStatus {
|
||||
|
||||
readonly element: HTMLElement;
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
this.element = dom.append(container, dom.$('.suggest-status-bar'));
|
||||
|
||||
const actionViewItemProvider = <IActionViewItemProvider>(action => {
|
||||
return action instanceof MenuItemAction
|
||||
? instantiationService.createInstance(StatusBarViewItem, action)
|
||||
: undefined;
|
||||
});
|
||||
const leftActions = new ActionBar(this.element, { actionViewItemProvider });
|
||||
const rightActions = new ActionBar(this.element, { actionViewItemProvider });
|
||||
const menu = menuService.createMenu(suggestWidgetStatusbarMenu, contextKeyService);
|
||||
|
||||
leftActions.domNode.classList.add('left');
|
||||
rightActions.domNode.classList.add('right');
|
||||
|
||||
const renderMenu = () => {
|
||||
const left: IAction[] = [];
|
||||
const right: IAction[] = [];
|
||||
for (let [group, actions] of menu.getActions()) {
|
||||
if (group === 'left') {
|
||||
left.push(...actions);
|
||||
} else {
|
||||
right.push(...actions);
|
||||
}
|
||||
}
|
||||
leftActions.clear();
|
||||
leftActions.push(left);
|
||||
rightActions.clear();
|
||||
rightActions.push(right);
|
||||
};
|
||||
this._disposables.add(menu.onDidChange(() => renderMenu()));
|
||||
this._disposables.add(menu);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { CompletionModel } from 'vs/editor/contrib/suggest/completionModel';
|
||||
import { CompletionItem, getSuggestionComparator, SnippetSortOrder } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
|
||||
import { EditorOptions, InternalSuggestOptions } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export function createSuggestItem(label: string, overwriteBefore: number, kind = modes.CompletionItemKind.Property, incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }, sortText?: string, filterText?: string): CompletionItem {
|
||||
const suggestion: modes.CompletionItem = {
|
||||
label,
|
||||
sortText,
|
||||
filterText,
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column - overwriteBefore, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
insertText: label,
|
||||
kind
|
||||
};
|
||||
const container: modes.CompletionList = {
|
||||
incomplete,
|
||||
suggestions: [suggestion]
|
||||
};
|
||||
const provider: modes.CompletionItemProvider = {
|
||||
provideCompletionItems(): any {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return new CompletionItem(position, suggestion, container, provider);
|
||||
}
|
||||
suite('CompletionModel', function () {
|
||||
|
||||
let defaultOptions = <InternalSuggestOptions>{
|
||||
insertMode: 'insert',
|
||||
snippetsPreventQuickSuggestions: true,
|
||||
filterGraceful: true,
|
||||
localityBonus: false,
|
||||
shareSuggestSelections: false,
|
||||
showIcons: true,
|
||||
showMethods: true,
|
||||
showFunctions: true,
|
||||
showConstructors: true,
|
||||
showFields: true,
|
||||
showVariables: true,
|
||||
showClasses: true,
|
||||
showStructs: true,
|
||||
showInterfaces: true,
|
||||
showModules: true,
|
||||
showProperties: true,
|
||||
showEvents: true,
|
||||
showOperators: true,
|
||||
showUnits: true,
|
||||
showValues: true,
|
||||
showConstants: true,
|
||||
showEnums: true,
|
||||
showEnumMembers: true,
|
||||
showKeywords: true,
|
||||
showWords: true,
|
||||
showColors: true,
|
||||
showFiles: true,
|
||||
showReferences: true,
|
||||
showFolders: true,
|
||||
showTypeParameters: true,
|
||||
showSnippets: true,
|
||||
};
|
||||
|
||||
let model: CompletionModel;
|
||||
|
||||
setup(function () {
|
||||
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('foo', 3),
|
||||
createSuggestItem('Foo', 3),
|
||||
createSuggestItem('foo', 2),
|
||||
], 1, {
|
||||
leadingLineContent: 'foo',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
});
|
||||
|
||||
test('filtering - cached', function () {
|
||||
|
||||
const itemsNow = model.items;
|
||||
let itemsThen = model.items;
|
||||
assert.ok(itemsNow === itemsThen);
|
||||
|
||||
// still the same context
|
||||
model.lineContext = { leadingLineContent: 'foo', characterCountDelta: 0 };
|
||||
itemsThen = model.items;
|
||||
assert.ok(itemsNow === itemsThen);
|
||||
|
||||
// different context, refilter
|
||||
model.lineContext = { leadingLineContent: 'foo1', characterCountDelta: 1 };
|
||||
itemsThen = model.items;
|
||||
assert.ok(itemsNow !== itemsThen);
|
||||
});
|
||||
|
||||
|
||||
test('complete/incomplete', () => {
|
||||
|
||||
assert.equal(model.incomplete.size, 0);
|
||||
|
||||
let incompleteModel = new CompletionModel([
|
||||
createSuggestItem('foo', 3, undefined, true),
|
||||
createSuggestItem('foo', 2),
|
||||
], 1, {
|
||||
leadingLineContent: 'foo',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
assert.equal(incompleteModel.incomplete.size, 1);
|
||||
});
|
||||
|
||||
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 }, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
assert.equal(model.incomplete.size, 1);
|
||||
assert.equal(model.items.length, 2);
|
||||
|
||||
const { incomplete } = model;
|
||||
const complete = model.adopt(incomplete);
|
||||
|
||||
assert.equal(incomplete.size, 1);
|
||||
assert.ok(incomplete.has(incompleteItem.provider));
|
||||
assert.equal(complete.length, 1);
|
||||
assert.ok(complete[0] === completeItem);
|
||||
});
|
||||
|
||||
test('Fuzzy matching of snippets stopped working with inline snippet suggestions #49895', function () {
|
||||
const completeItem1 = createSuggestItem('foobar1', 1, undefined, false, { lineNumber: 1, column: 2 });
|
||||
const completeItem2 = createSuggestItem('foobar2', 1, undefined, false, { lineNumber: 1, column: 2 });
|
||||
const completeItem3 = createSuggestItem('foobar3', 1, undefined, false, { lineNumber: 1, column: 2 });
|
||||
const completeItem4 = createSuggestItem('foobar4', 1, undefined, false, { lineNumber: 1, column: 2 });
|
||||
const completeItem5 = createSuggestItem('foobar5', 1, undefined, false, { lineNumber: 1, column: 2 });
|
||||
const incompleteItem1 = createSuggestItem('foofoo1', 1, undefined, true, { lineNumber: 1, column: 2 });
|
||||
|
||||
const model = new CompletionModel(
|
||||
[
|
||||
completeItem1,
|
||||
completeItem2,
|
||||
completeItem3,
|
||||
completeItem4,
|
||||
completeItem5,
|
||||
incompleteItem1,
|
||||
], 2, { leadingLineContent: 'f', characterCountDelta: 0 }, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined
|
||||
);
|
||||
assert.equal(model.incomplete.size, 1);
|
||||
assert.equal(model.items.length, 6);
|
||||
|
||||
const { incomplete } = model;
|
||||
const complete = model.adopt(incomplete);
|
||||
|
||||
assert.equal(incomplete.size, 1);
|
||||
assert.ok(incomplete.has(incompleteItem1.provider));
|
||||
assert.equal(complete.length, 5);
|
||||
});
|
||||
|
||||
test('proper current word when length=0, #16380', function () {
|
||||
|
||||
model = new CompletionModel([
|
||||
createSuggestItem(' </div', 4),
|
||||
createSuggestItem('a', 0),
|
||||
createSuggestItem('p', 0),
|
||||
createSuggestItem(' </tag', 4),
|
||||
createSuggestItem(' XYZ', 4),
|
||||
], 1, {
|
||||
leadingLineContent: ' <',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
|
||||
assert.equal(model.items.length, 4);
|
||||
|
||||
const [a, b, c, d] = model.items;
|
||||
assert.equal(a.completion.label, ' </div');
|
||||
assert.equal(b.completion.label, ' </tag');
|
||||
assert.equal(c.completion.label, 'a');
|
||||
assert.equal(d.completion.label, 'p');
|
||||
});
|
||||
|
||||
test('keep snippet sorting with prefix: top, #25495', function () {
|
||||
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('Snippet1', 1, modes.CompletionItemKind.Snippet),
|
||||
createSuggestItem('tnippet2', 1, modes.CompletionItemKind.Snippet),
|
||||
createSuggestItem('semver', 1, modes.CompletionItemKind.Property),
|
||||
], 1, {
|
||||
leadingLineContent: 's',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, defaultOptions, 'top', undefined);
|
||||
|
||||
assert.equal(model.items.length, 2);
|
||||
const [a, b] = model.items;
|
||||
assert.equal(a.completion.label, 'Snippet1');
|
||||
assert.equal(b.completion.label, 'semver');
|
||||
assert.ok(a.score < b.score); // snippet really promoted
|
||||
|
||||
});
|
||||
|
||||
test('keep snippet sorting with prefix: bottom, #25495', function () {
|
||||
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('snippet1', 1, modes.CompletionItemKind.Snippet),
|
||||
createSuggestItem('tnippet2', 1, modes.CompletionItemKind.Snippet),
|
||||
createSuggestItem('Semver', 1, modes.CompletionItemKind.Property),
|
||||
], 1, {
|
||||
leadingLineContent: 's',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, defaultOptions, 'bottom', undefined);
|
||||
|
||||
assert.equal(model.items.length, 2);
|
||||
const [a, b] = model.items;
|
||||
assert.equal(a.completion.label, 'Semver');
|
||||
assert.equal(b.completion.label, 'snippet1');
|
||||
assert.ok(a.score < b.score); // snippet really demoted
|
||||
});
|
||||
|
||||
test('keep snippet sorting with prefix: inline, #25495', function () {
|
||||
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('snippet1', 1, modes.CompletionItemKind.Snippet),
|
||||
createSuggestItem('tnippet2', 1, modes.CompletionItemKind.Snippet),
|
||||
createSuggestItem('Semver', 1),
|
||||
], 1, {
|
||||
leadingLineContent: 's',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, defaultOptions, 'inline', undefined);
|
||||
|
||||
assert.equal(model.items.length, 2);
|
||||
const [a, b] = model.items;
|
||||
assert.equal(a.completion.label, 'snippet1');
|
||||
assert.equal(b.completion.label, 'Semver');
|
||||
assert.ok(a.score > b.score); // snippet really demoted
|
||||
});
|
||||
|
||||
test('filterText seems ignored in autocompletion, #26874', function () {
|
||||
|
||||
const item1 = createSuggestItem('Map - java.util', 1, undefined, undefined, undefined, undefined, 'Map');
|
||||
const item2 = createSuggestItem('Map - java.util', 1);
|
||||
|
||||
model = new CompletionModel([item1, item2], 1, {
|
||||
leadingLineContent: 'M',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
|
||||
assert.equal(model.items.length, 2);
|
||||
|
||||
model.lineContext = {
|
||||
leadingLineContent: 'Map ',
|
||||
characterCountDelta: 3
|
||||
};
|
||||
assert.equal(model.items.length, 1);
|
||||
});
|
||||
|
||||
test('Vscode 1.12 no longer obeys \'sortText\' in completion items (from language server), #26096', function () {
|
||||
|
||||
const item1 = createSuggestItem('<- groups', 2, modes.CompletionItemKind.Property, false, { lineNumber: 1, column: 3 }, '00002', ' groups');
|
||||
const item2 = createSuggestItem('source', 0, modes.CompletionItemKind.Property, false, { lineNumber: 1, column: 3 }, '00001', 'source');
|
||||
const items = [item1, item2].sort(getSuggestionComparator(SnippetSortOrder.Inline));
|
||||
|
||||
model = new CompletionModel(items, 3, {
|
||||
leadingLineContent: ' ',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
|
||||
assert.equal(model.items.length, 2);
|
||||
|
||||
const [first, second] = model.items;
|
||||
assert.equal(first.completion.label, 'source');
|
||||
assert.equal(second.completion.label, '<- groups');
|
||||
});
|
||||
|
||||
test('Score only filtered items when typing more, score all when typing less', function () {
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('console', 0),
|
||||
createSuggestItem('co_new', 0),
|
||||
createSuggestItem('bar', 0),
|
||||
createSuggestItem('car', 0),
|
||||
createSuggestItem('foo', 0),
|
||||
], 1, {
|
||||
leadingLineContent: '',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
|
||||
assert.equal(model.items.length, 5);
|
||||
|
||||
// narrow down once
|
||||
model.lineContext = { leadingLineContent: 'c', characterCountDelta: 1 };
|
||||
assert.equal(model.items.length, 3);
|
||||
|
||||
// query gets longer, narrow down the narrow-down'ed-set from before
|
||||
model.lineContext = { leadingLineContent: 'cn', characterCountDelta: 2 };
|
||||
assert.equal(model.items.length, 2);
|
||||
|
||||
// query gets shorter, refilter everything
|
||||
model.lineContext = { leadingLineContent: '', characterCountDelta: 0 };
|
||||
assert.equal(model.items.length, 5);
|
||||
});
|
||||
|
||||
test('Have more relaxed suggest matching algorithm #15419', function () {
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('result', 0),
|
||||
createSuggestItem('replyToUser', 0),
|
||||
createSuggestItem('randomLolut', 0),
|
||||
createSuggestItem('car', 0),
|
||||
createSuggestItem('foo', 0),
|
||||
], 1, {
|
||||
leadingLineContent: '',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
|
||||
// query gets longer, narrow down the narrow-down'ed-set from before
|
||||
model.lineContext = { leadingLineContent: 'rlut', characterCountDelta: 4 };
|
||||
assert.equal(model.items.length, 3);
|
||||
|
||||
const [first, second, third] = model.items;
|
||||
assert.equal(first.completion.label, 'result'); // best with `rult`
|
||||
assert.equal(second.completion.label, 'replyToUser'); // best with `rltu`
|
||||
assert.equal(third.completion.label, 'randomLolut'); // best with `rlut`
|
||||
});
|
||||
|
||||
test('Emmet suggestion not appearing at the top of the list in jsx files, #39518', function () {
|
||||
model = new CompletionModel([
|
||||
createSuggestItem('from', 0),
|
||||
createSuggestItem('form', 0),
|
||||
createSuggestItem('form:get', 0),
|
||||
createSuggestItem('testForeignMeasure', 0),
|
||||
createSuggestItem('fooRoom', 0),
|
||||
], 1, {
|
||||
leadingLineContent: '',
|
||||
characterCountDelta: 0
|
||||
}, WordDistance.None, EditorOptions.suggest.defaultValue, EditorOptions.snippetSuggestions.defaultValue, undefined);
|
||||
|
||||
model.lineContext = { leadingLineContent: 'form', characterCountDelta: 4 };
|
||||
assert.equal(model.items.length, 5);
|
||||
const [first, second, third] = model.items;
|
||||
assert.equal(first.completion.label, 'form'); // best with `form`
|
||||
assert.equal(second.completion.label, 'form:get'); // best with `form`
|
||||
assert.equal(third.completion.label, 'from'); // best with `from`
|
||||
});
|
||||
});
|
||||
152
lib/vscode/src/vs/editor/contrib/suggest/test/suggest.test.ts
Normal file
152
lib/vscode/src/vs/editor/contrib/suggest/test/suggest.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { CompletionProviderRegistry, CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/modes';
|
||||
import { provideSuggestionItems, SnippetSortOrder, CompletionOptions } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
|
||||
|
||||
suite('Suggest', function () {
|
||||
|
||||
let model: TextModel;
|
||||
let registration: IDisposable;
|
||||
|
||||
setup(function () {
|
||||
|
||||
model = createTextModel('FOO\nbar\BAR\nfoo', undefined, undefined, URI.parse('foo:bar/path'));
|
||||
registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, {
|
||||
provideCompletionItems(_doc, pos) {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
label: 'aaa',
|
||||
kind: CompletionItemKind.Snippet,
|
||||
insertText: 'aaa',
|
||||
range: Range.fromPositions(pos)
|
||||
}, {
|
||||
label: 'zzz',
|
||||
kind: CompletionItemKind.Snippet,
|
||||
insertText: 'zzz',
|
||||
range: Range.fromPositions(pos)
|
||||
}, {
|
||||
label: 'fff',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'fff',
|
||||
range: Range.fromPositions(pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
registration.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('sort - snippet inline', async function () {
|
||||
const { items } = await provideSuggestionItems(model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Inline));
|
||||
assert.equal(items.length, 3);
|
||||
assert.equal(items[0].completion.label, 'aaa');
|
||||
assert.equal(items[1].completion.label, 'fff');
|
||||
assert.equal(items[2].completion.label, 'zzz');
|
||||
});
|
||||
|
||||
test('sort - snippet top', async function () {
|
||||
const { items } = await provideSuggestionItems(model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Top));
|
||||
assert.equal(items.length, 3);
|
||||
assert.equal(items[0].completion.label, 'aaa');
|
||||
assert.equal(items[1].completion.label, 'zzz');
|
||||
assert.equal(items[2].completion.label, 'fff');
|
||||
});
|
||||
|
||||
test('sort - snippet bottom', async function () {
|
||||
const { items } = await provideSuggestionItems(model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Bottom));
|
||||
assert.equal(items.length, 3);
|
||||
assert.equal(items[0].completion.label, 'fff');
|
||||
assert.equal(items[1].completion.label, 'aaa');
|
||||
assert.equal(items[2].completion.label, 'zzz');
|
||||
});
|
||||
|
||||
test('sort - snippet none', async function () {
|
||||
const { items } = await provideSuggestionItems(model, new Position(1, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)));
|
||||
assert.equal(items.length, 1);
|
||||
assert.equal(items[0].completion.label, 'fff');
|
||||
});
|
||||
|
||||
test('only from', function () {
|
||||
|
||||
const foo: any = {
|
||||
triggerCharacters: [],
|
||||
provideCompletionItems() {
|
||||
return {
|
||||
currentWord: '',
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
label: 'jjj',
|
||||
type: 'property',
|
||||
insertText: 'jjj'
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
const registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, foo);
|
||||
|
||||
provideSuggestionItems(model, new Position(1, 1), new CompletionOptions(undefined, undefined, new Set<CompletionItemProvider>().add(foo))).then(({ items }) => {
|
||||
registration.dispose();
|
||||
|
||||
assert.equal(items.length, 1);
|
||||
assert.ok(items[0].provider === foo);
|
||||
});
|
||||
});
|
||||
|
||||
test('Ctrl+space completions stopped working with the latest Insiders, #97650', async function () {
|
||||
|
||||
|
||||
const foo = new class implements CompletionItemProvider {
|
||||
|
||||
triggerCharacters = [];
|
||||
|
||||
provideCompletionItems() {
|
||||
return {
|
||||
suggestions: [{
|
||||
label: 'one',
|
||||
kind: CompletionItemKind.Class,
|
||||
insertText: 'one',
|
||||
range: {
|
||||
insert: new Range(0, 0, 0, 0),
|
||||
replace: new Range(0, 0, 0, 10)
|
||||
}
|
||||
}, {
|
||||
label: 'two',
|
||||
kind: CompletionItemKind.Class,
|
||||
insertText: 'two',
|
||||
range: {
|
||||
insert: new Range(0, 0, 0, 0),
|
||||
replace: new Range(0, 1, 0, 10)
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, foo);
|
||||
const { items } = await provideSuggestionItems(model, new Position(0, 0), new CompletionOptions(undefined, undefined, new Set<CompletionItemProvider>().add(foo)));
|
||||
registration.dispose();
|
||||
|
||||
assert.equal(items.length, 2);
|
||||
const [a, b] = items;
|
||||
|
||||
assert.equal(a.completion.label, 'one');
|
||||
assert.equal(a.isInvalid, false);
|
||||
assert.equal(b.completion.label, 'two');
|
||||
assert.equal(b.isInvalid, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,417 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
|
||||
import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { mock } from 'vs/base/test/common/mock';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { CompletionProviderRegistry, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
|
||||
import { IMenuService, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
suite('SuggestController', function () {
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
let controller: SuggestController;
|
||||
let editor: ITestCodeEditor;
|
||||
let model: TextModel;
|
||||
|
||||
setup(function () {
|
||||
disposables.clear();
|
||||
|
||||
const serviceCollection = new ServiceCollection(
|
||||
[ITelemetryService, NullTelemetryService],
|
||||
[ILogService, new NullLogService()],
|
||||
[IStorageService, new InMemoryStorageService()],
|
||||
[IKeybindingService, new MockKeybindingService()],
|
||||
[IEditorWorkerService, new class extends mock<IEditorWorkerService>() {
|
||||
computeWordRanges() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}],
|
||||
[ISuggestMemoryService, new class extends mock<ISuggestMemoryService>() {
|
||||
memorize(): void { }
|
||||
select(): number { return 0; }
|
||||
}],
|
||||
[IMenuService, new class extends mock<IMenuService>() {
|
||||
createMenu() {
|
||||
return new class extends mock<IMenu>() {
|
||||
onDidChange = Event.None;
|
||||
};
|
||||
}
|
||||
}]
|
||||
);
|
||||
|
||||
model = createTextModel('', undefined, undefined, URI.from({ scheme: 'test-ctrl', path: '/path.tst' }));
|
||||
editor = createTestCodeEditor({
|
||||
model,
|
||||
serviceCollection,
|
||||
});
|
||||
|
||||
editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
|
||||
controller = editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController);
|
||||
});
|
||||
|
||||
test('postfix completion reports incorrect position #86984', async function () {
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'let ${1:name} = foo$0',
|
||||
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
range: { startLineNumber: 1, startColumn: 9, endLineNumber: 1, endColumn: 11 },
|
||||
additionalTextEdits: [{
|
||||
text: '',
|
||||
range: { startLineNumber: 1, startColumn: 5, endLineNumber: 1, endColumn: 9 }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue(' foo.le');
|
||||
editor.setSelection(new Selection(1, 11, 1, 11));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(false, false);
|
||||
await p2;
|
||||
|
||||
assert.equal(editor.getValue(), ' let name = foo');
|
||||
});
|
||||
|
||||
test('use additionalTextEdits sync when possible', async function () {
|
||||
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hello',
|
||||
range: Range.fromPositions(pos),
|
||||
additionalTextEdits: [{
|
||||
text: 'I came sync',
|
||||
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
},
|
||||
async resolveCompletionItem(item) {
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue('hello\nhallo');
|
||||
editor.setSelection(new Selection(2, 6, 2, 6));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(false, false);
|
||||
await p2;
|
||||
|
||||
// insertText happens sync!
|
||||
assert.equal(editor.getValue(), 'I came synchello\nhallohello');
|
||||
});
|
||||
|
||||
test('resolve additionalTextEdits async when needed', async function () {
|
||||
|
||||
let resolveCallCount = 0;
|
||||
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hello',
|
||||
range: Range.fromPositions(pos)
|
||||
}]
|
||||
};
|
||||
},
|
||||
async resolveCompletionItem(item) {
|
||||
resolveCallCount += 1;
|
||||
await timeout(10);
|
||||
item.additionalTextEdits = [{
|
||||
text: 'I came late',
|
||||
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }
|
||||
}];
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue('hello\nhallo');
|
||||
editor.setSelection(new Selection(2, 6, 2, 6));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(false, false);
|
||||
await p2;
|
||||
|
||||
// insertText happens sync!
|
||||
assert.equal(editor.getValue(), 'hello\nhallohello');
|
||||
assert.equal(resolveCallCount, 1);
|
||||
|
||||
// additional edits happened after a litte wait
|
||||
await timeout(20);
|
||||
assert.equal(editor.getValue(), 'I came latehello\nhallohello');
|
||||
|
||||
// single undo stop
|
||||
editor.getModel()?.undo();
|
||||
assert.equal(editor.getValue(), 'hello\nhallo');
|
||||
});
|
||||
|
||||
test('resolve additionalTextEdits async when needed (typing)', async function () {
|
||||
|
||||
let resolveCallCount = 0;
|
||||
let resolve: Function = () => { };
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hello',
|
||||
range: Range.fromPositions(pos)
|
||||
}]
|
||||
};
|
||||
},
|
||||
async resolveCompletionItem(item) {
|
||||
resolveCallCount += 1;
|
||||
await new Promise(_resolve => resolve = _resolve);
|
||||
item.additionalTextEdits = [{
|
||||
text: 'I came late',
|
||||
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }
|
||||
}];
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue('hello\nhallo');
|
||||
editor.setSelection(new Selection(2, 6, 2, 6));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(false, false);
|
||||
await p2;
|
||||
|
||||
// insertText happens sync!
|
||||
assert.equal(editor.getValue(), 'hello\nhallohello');
|
||||
assert.equal(resolveCallCount, 1);
|
||||
|
||||
// additional edits happened after a litte wait
|
||||
assert.ok(editor.getSelection()?.equalsSelection(new Selection(2, 11, 2, 11)));
|
||||
editor.trigger('test', 'type', { text: 'TYPING' });
|
||||
|
||||
assert.equal(editor.getValue(), 'hello\nhallohelloTYPING');
|
||||
|
||||
resolve();
|
||||
await timeout(10);
|
||||
assert.equal(editor.getValue(), 'I came latehello\nhallohelloTYPING');
|
||||
assert.ok(editor.getSelection()?.equalsSelection(new Selection(2, 17, 2, 17)));
|
||||
});
|
||||
|
||||
// additional edit come late and are AFTER the selection -> cancel
|
||||
test('resolve additionalTextEdits async when needed (simple conflict)', async function () {
|
||||
|
||||
let resolveCallCount = 0;
|
||||
let resolve: Function = () => { };
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hello',
|
||||
range: Range.fromPositions(pos)
|
||||
}]
|
||||
};
|
||||
},
|
||||
async resolveCompletionItem(item) {
|
||||
resolveCallCount += 1;
|
||||
await new Promise(_resolve => resolve = _resolve);
|
||||
item.additionalTextEdits = [{
|
||||
text: 'I came late',
|
||||
range: { startLineNumber: 1, startColumn: 6, endLineNumber: 1, endColumn: 6 }
|
||||
}];
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue('');
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(false, false);
|
||||
await p2;
|
||||
|
||||
// insertText happens sync!
|
||||
assert.equal(editor.getValue(), 'hello');
|
||||
assert.equal(resolveCallCount, 1);
|
||||
|
||||
resolve();
|
||||
await timeout(10);
|
||||
assert.equal(editor.getValue(), 'hello');
|
||||
});
|
||||
|
||||
// additional edit come late and are AFTER the position at which the user typed -> cancelled
|
||||
test('resolve additionalTextEdits async when needed (conflict)', async function () {
|
||||
|
||||
let resolveCallCount = 0;
|
||||
let resolve: Function = () => { };
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hello',
|
||||
range: Range.fromPositions(pos)
|
||||
}]
|
||||
};
|
||||
},
|
||||
async resolveCompletionItem(item) {
|
||||
resolveCallCount += 1;
|
||||
await new Promise(_resolve => resolve = _resolve);
|
||||
item.additionalTextEdits = [{
|
||||
text: 'I came late',
|
||||
range: { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 }
|
||||
}];
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue('hello\nhallo');
|
||||
editor.setSelection(new Selection(2, 6, 2, 6));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(false, false);
|
||||
await p2;
|
||||
|
||||
// insertText happens sync!
|
||||
assert.equal(editor.getValue(), 'hello\nhallohello');
|
||||
assert.equal(resolveCallCount, 1);
|
||||
|
||||
// additional edits happened after a litte wait
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
editor.trigger('test', 'type', { text: 'TYPING' });
|
||||
|
||||
assert.equal(editor.getValue(), 'TYPINGhello\nhallohello');
|
||||
|
||||
resolve();
|
||||
await timeout(10);
|
||||
assert.equal(editor.getValue(), 'TYPINGhello\nhallohello');
|
||||
assert.ok(editor.getSelection()?.equalsSelection(new Selection(1, 7, 1, 7)));
|
||||
});
|
||||
|
||||
test('resolve additionalTextEdits async when needed (cancel)', async function () {
|
||||
|
||||
let resolve: Function[] = [];
|
||||
disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hello',
|
||||
range: Range.fromPositions(pos)
|
||||
}, {
|
||||
kind: CompletionItemKind.Snippet,
|
||||
label: 'let',
|
||||
insertText: 'hallo',
|
||||
range: Range.fromPositions(pos)
|
||||
}]
|
||||
};
|
||||
},
|
||||
async resolveCompletionItem(item) {
|
||||
await new Promise(_resolve => resolve.push(_resolve));
|
||||
item.additionalTextEdits = [{
|
||||
text: 'additionalTextEdits',
|
||||
range: { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 }
|
||||
}];
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.setValue('abc');
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
|
||||
// trigger
|
||||
let p1 = Event.toPromise(controller.model.onDidSuggest);
|
||||
controller.triggerSuggest();
|
||||
await p1;
|
||||
|
||||
//
|
||||
let p2 = Event.toPromise(controller.model.onDidCancel);
|
||||
controller.acceptSelectedSuggestion(true, false);
|
||||
await p2;
|
||||
|
||||
// insertText happens sync!
|
||||
assert.equal(editor.getValue(), 'helloabc');
|
||||
|
||||
// next
|
||||
controller.acceptNextSuggestion();
|
||||
|
||||
// resolve additional edits (MUST be cancelled)
|
||||
resolve.forEach(fn => fn);
|
||||
resolve.length = 0;
|
||||
await timeout(10);
|
||||
|
||||
// next suggestion used
|
||||
assert.equal(editor.getValue(), 'halloabc');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { LRUMemory, NoMemory, PrefixMemory, Memory } from 'vs/editor/contrib/suggest/suggestMemory';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { createSuggestItem } from 'vs/editor/contrib/suggest/test/completionModel.test';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
|
||||
|
||||
suite('SuggestMemories', function () {
|
||||
|
||||
let pos: IPosition;
|
||||
let buffer: ITextModel;
|
||||
let items: CompletionItem[];
|
||||
|
||||
setup(function () {
|
||||
pos = { lineNumber: 1, column: 1 };
|
||||
buffer = createTextModel('This is some text.\nthis.\nfoo: ,');
|
||||
items = [
|
||||
createSuggestItem('foo', 0),
|
||||
createSuggestItem('bar', 0)
|
||||
];
|
||||
});
|
||||
|
||||
test('AbstractMemory, select', function () {
|
||||
|
||||
const mem = new class extends Memory {
|
||||
constructor() {
|
||||
super('first');
|
||||
}
|
||||
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
||||
throw new Error('Method not implemented.');
|
||||
} toJSON(): object {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
fromJSON(data: object): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
};
|
||||
|
||||
let item1 = createSuggestItem('fazz', 0);
|
||||
let item2 = createSuggestItem('bazz', 0);
|
||||
let item3 = createSuggestItem('bazz', 0);
|
||||
let item4 = createSuggestItem('bazz', 0);
|
||||
item1.completion.preselect = false;
|
||||
item2.completion.preselect = true;
|
||||
item3.completion.preselect = true;
|
||||
|
||||
assert.equal(mem.select(buffer, pos, [item1, item2, item3, item4]), 1);
|
||||
});
|
||||
|
||||
test('[No|Prefix|LRU]Memory honor selection boost', function () {
|
||||
let item1 = createSuggestItem('fazz', 0);
|
||||
let item2 = createSuggestItem('bazz', 0);
|
||||
let item3 = createSuggestItem('bazz', 0);
|
||||
let item4 = createSuggestItem('bazz', 0);
|
||||
item1.completion.preselect = false;
|
||||
item2.completion.preselect = true;
|
||||
item3.completion.preselect = true;
|
||||
let items = [item1, item2, item3, item4];
|
||||
|
||||
|
||||
assert.equal(new NoMemory().select(buffer, pos, items), 1);
|
||||
assert.equal(new LRUMemory().select(buffer, pos, items), 1);
|
||||
assert.equal(new PrefixMemory().select(buffer, pos, items), 1);
|
||||
});
|
||||
|
||||
test('NoMemory', () => {
|
||||
|
||||
const mem = new NoMemory();
|
||||
|
||||
assert.equal(mem.select(buffer, pos, items), 0);
|
||||
assert.equal(mem.select(buffer, pos, []), 0);
|
||||
|
||||
mem.memorize(buffer, pos, items[0]);
|
||||
mem.memorize(buffer, pos, null!);
|
||||
});
|
||||
|
||||
test('LRUMemory', () => {
|
||||
|
||||
pos = { lineNumber: 2, column: 6 };
|
||||
|
||||
const mem = new LRUMemory();
|
||||
mem.memorize(buffer, pos, items[1]);
|
||||
|
||||
assert.equal(mem.select(buffer, pos, items), 1);
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 3 }, items), 0);
|
||||
|
||||
mem.memorize(buffer, pos, items[0]);
|
||||
assert.equal(mem.select(buffer, pos, items), 0);
|
||||
|
||||
assert.equal(mem.select(buffer, pos, [
|
||||
createSuggestItem('new', 0),
|
||||
createSuggestItem('bar', 0)
|
||||
]), 1);
|
||||
|
||||
assert.equal(mem.select(buffer, pos, [
|
||||
createSuggestItem('new1', 0),
|
||||
createSuggestItem('new2', 0)
|
||||
]), 0);
|
||||
});
|
||||
|
||||
test('`"editor.suggestSelection": "recentlyUsed"` should be a little more sticky #78571', function () {
|
||||
|
||||
let item1 = createSuggestItem('gamma', 0);
|
||||
let item2 = createSuggestItem('game', 0);
|
||||
items = [item1, item2];
|
||||
|
||||
let mem = new LRUMemory();
|
||||
buffer.setValue(' foo.');
|
||||
mem.memorize(buffer, { lineNumber: 1, column: 1 }, item2);
|
||||
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 2 }, items), 0); // leading whitespace -> ignore recent items
|
||||
|
||||
mem.memorize(buffer, { lineNumber: 1, column: 9 }, item2);
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 9 }, items), 1); // foo.
|
||||
|
||||
buffer.setValue(' foo.g');
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 10 }, items), 1); // foo.g, 'gamma' and 'game' have the same score
|
||||
|
||||
item1.score = [10, 0, 0];
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 10 }, items), 0); // foo.g, 'gamma' has higher score
|
||||
|
||||
});
|
||||
|
||||
test('intellisense is not showing top options first #43429', function () {
|
||||
// ensure we don't memorize for whitespace prefixes
|
||||
|
||||
pos = { lineNumber: 2, column: 6 };
|
||||
const mem = new LRUMemory();
|
||||
|
||||
mem.memorize(buffer, pos, items[1]);
|
||||
assert.equal(mem.select(buffer, pos, items), 1);
|
||||
|
||||
assert.equal(mem.select(buffer, { lineNumber: 3, column: 5 }, items), 0); // foo: |,
|
||||
assert.equal(mem.select(buffer, { lineNumber: 3, column: 6 }, items), 1); // foo: ,|
|
||||
});
|
||||
|
||||
test('PrefixMemory', () => {
|
||||
|
||||
const mem = new PrefixMemory();
|
||||
buffer.setValue('constructor');
|
||||
const item0 = createSuggestItem('console', 0);
|
||||
const item1 = createSuggestItem('const', 0);
|
||||
const item2 = createSuggestItem('constructor', 0);
|
||||
const item3 = createSuggestItem('constant', 0);
|
||||
const items = [item0, item1, item2, item3];
|
||||
|
||||
mem.memorize(buffer, { lineNumber: 1, column: 2 }, item1); // c -> const
|
||||
mem.memorize(buffer, { lineNumber: 1, column: 3 }, item0); // co -> console
|
||||
mem.memorize(buffer, { lineNumber: 1, column: 4 }, item2); // con -> constructor
|
||||
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 1 }, items), 0);
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 2 }, items), 1);
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 3 }, items), 0);
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 4 }, items), 2);
|
||||
assert.equal(mem.select(buffer, { lineNumber: 1, column: 7 }, items), 2); // find substr
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,864 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
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 { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { TokenizationResult2 } from 'vs/editor/common/core/token';
|
||||
import { Handler } from 'vs/editor/common/editorCommon';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
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';
|
||||
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
|
||||
import { LineContext, SuggestModel } from 'vs/editor/contrib/suggest/suggestModel';
|
||||
import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget';
|
||||
import { ITestCodeEditor, 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, 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';
|
||||
import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { mock } from 'vs/base/test/common/mock';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
|
||||
|
||||
function createMockEditor(model: TextModel): ITestCodeEditor {
|
||||
let editor = createTestCodeEditor({
|
||||
model: model,
|
||||
serviceCollection: new ServiceCollection(
|
||||
[ITelemetryService, NullTelemetryService],
|
||||
[IStorageService, new InMemoryStorageService()],
|
||||
[IKeybindingService, new MockKeybindingService()],
|
||||
[ISuggestMemoryService, new class implements ISuggestMemoryService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
memorize(): void {
|
||||
}
|
||||
select(): number {
|
||||
return -1;
|
||||
}
|
||||
}],
|
||||
),
|
||||
});
|
||||
editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
|
||||
return editor;
|
||||
}
|
||||
|
||||
suite('SuggestModel - Context', function () {
|
||||
const OUTER_LANGUAGE_ID = new LanguageIdentifier('outerMode', 3);
|
||||
const INNER_LANGUAGE_ID = new LanguageIdentifier('innerMode', 4);
|
||||
|
||||
class OuterMode extends MockMode {
|
||||
constructor() {
|
||||
super(OUTER_LANGUAGE_ID);
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {}));
|
||||
|
||||
this._register(TokenizationRegistry.register(this.getLanguageIdentifier().language, {
|
||||
getInitialState: (): IState => NULL_STATE,
|
||||
tokenize: undefined!,
|
||||
tokenize2: (line: string, state: IState): TokenizationResult2 => {
|
||||
const tokensArr: number[] = [];
|
||||
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) {
|
||||
tokensArr.push(i);
|
||||
tokensArr.push((languageId.id << MetadataConsts.LANGUAGEID_OFFSET));
|
||||
}
|
||||
prevLanguageId = languageId;
|
||||
}
|
||||
|
||||
const tokens = new Uint32Array(tokensArr.length);
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
tokens[i] = tokensArr[i];
|
||||
}
|
||||
return new TokenizationResult2(tokens, state);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class InnerMode extends MockMode {
|
||||
constructor() {
|
||||
super(INNER_LANGUAGE_ID);
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {}));
|
||||
}
|
||||
}
|
||||
|
||||
const assertAutoTrigger = (model: TextModel, offset: number, expected: boolean, message?: string): void => {
|
||||
const pos = model.getPositionAt(offset);
|
||||
const editor = createMockEditor(model);
|
||||
editor.setPosition(pos);
|
||||
assert.equal(LineContext.shouldAutoTrigger(editor), expected, message);
|
||||
editor.dispose();
|
||||
};
|
||||
|
||||
let disposables: Disposable[] = [];
|
||||
|
||||
setup(() => {
|
||||
disposables = [];
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
dispose(disposables);
|
||||
disposables = [];
|
||||
});
|
||||
|
||||
test('Context - shouldAutoTrigger', function () {
|
||||
const model = createTextModel('Das Pferd frisst keinen Gurkensalat - Philipp Reis 1861.\nWer hat\'s erfunden?');
|
||||
disposables.push(model);
|
||||
|
||||
assertAutoTrigger(model, 3, true, 'end of word, Das|');
|
||||
assertAutoTrigger(model, 4, false, 'no word Das |');
|
||||
assertAutoTrigger(model, 1, false, 'middle of word D|as');
|
||||
assertAutoTrigger(model, 55, false, 'number, 1861|');
|
||||
});
|
||||
|
||||
test('shouldAutoTrigger at embedded language boundaries', () => {
|
||||
const outerMode = new OuterMode();
|
||||
const innerMode = new InnerMode();
|
||||
disposables.push(outerMode, innerMode);
|
||||
|
||||
const model = createTextModel('a<xx>a<x>', undefined, outerMode.getLanguageIdentifier());
|
||||
disposables.push(model);
|
||||
|
||||
assertAutoTrigger(model, 1, true, 'a|<x — should trigger at end of word');
|
||||
assertAutoTrigger(model, 2, false, 'a<|x — should NOT trigger at start of word');
|
||||
assertAutoTrigger(model, 3, false, 'a<x|x — should NOT trigger in middle of word');
|
||||
assertAutoTrigger(model, 4, true, 'a<xx|> — should trigger at boundary between languages');
|
||||
assertAutoTrigger(model, 5, false, 'a<xx>|a — should NOT trigger at start of word');
|
||||
assertAutoTrigger(model, 6, true, 'a<xx>a|< — should trigger at end of word');
|
||||
assertAutoTrigger(model, 8, true, 'a<xx>a<x|> — should trigger at end of word at boundary');
|
||||
});
|
||||
});
|
||||
|
||||
suite('SuggestModel - TriggerAndCancelOracle', function () {
|
||||
|
||||
|
||||
function getDefaultSuggestRange(model: ITextModel, position: Position) {
|
||||
const wordUntil = model.getWordUntilPosition(position);
|
||||
return new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
|
||||
}
|
||||
|
||||
const alwaysEmptySupport: CompletionItemProvider = {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const alwaysSomethingSupport: CompletionItemProvider = {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
label: doc.getWordUntilPosition(pos).word,
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'foofoo',
|
||||
range: getDefaultSuggestRange(doc, pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let disposables: IDisposable[] = [];
|
||||
let model: TextModel;
|
||||
|
||||
setup(function () {
|
||||
disposables = dispose(disposables);
|
||||
model = createTextModel('abc def', undefined, undefined, URI.parse('test:somefile.ttt'));
|
||||
disposables.push(model);
|
||||
});
|
||||
|
||||
function withOracle(callback: (model: SuggestModel, editor: ITestCodeEditor) => any): Promise<any> {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const editor = createMockEditor(model);
|
||||
const oracle = new SuggestModel(
|
||||
editor,
|
||||
new class extends mock<IEditorWorkerService>() {
|
||||
computeWordRanges() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
},
|
||||
new class extends mock<IClipboardService>() {
|
||||
readText() {
|
||||
return Promise.resolve('CLIPPY');
|
||||
}
|
||||
},
|
||||
NullTelemetryService,
|
||||
new NullLogService()
|
||||
);
|
||||
disposables.push(oracle, editor);
|
||||
|
||||
try {
|
||||
resolve(callback(oracle, editor));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function assertEvent<E>(event: Event<E>, action: () => any, assert: (e: E) => any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sub = event(e => {
|
||||
sub.dispose();
|
||||
try {
|
||||
resolve(assert(e));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
try {
|
||||
action();
|
||||
} catch (err) {
|
||||
sub.dispose();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('events - cancel/trigger', function () {
|
||||
return withOracle(model => {
|
||||
|
||||
return Promise.all([
|
||||
|
||||
assertEvent(model.onDidTrigger, function () {
|
||||
model.trigger({ auto: true, shy: false });
|
||||
}, function (event) {
|
||||
assert.equal(event.auto, true);
|
||||
|
||||
return assertEvent(model.onDidCancel, function () {
|
||||
model.cancel();
|
||||
}, function (event) {
|
||||
assert.equal(event.retrigger, false);
|
||||
});
|
||||
}),
|
||||
|
||||
assertEvent(model.onDidTrigger, function () {
|
||||
model.trigger({ auto: true, shy: false });
|
||||
}, function (event) {
|
||||
assert.equal(event.auto, true);
|
||||
}),
|
||||
|
||||
assertEvent(model.onDidTrigger, function () {
|
||||
model.trigger({ auto: false, shy: false });
|
||||
}, function (event) {
|
||||
assert.equal(event.auto, false);
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('events - suggest/empty', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysEmptySupport));
|
||||
|
||||
return withOracle(model => {
|
||||
return Promise.all([
|
||||
assertEvent(model.onDidCancel, function () {
|
||||
model.trigger({ auto: true, shy: false });
|
||||
}, function (event) {
|
||||
assert.equal(event.retrigger, false);
|
||||
}),
|
||||
assertEvent(model.onDidSuggest, function () {
|
||||
model.trigger({ auto: false, shy: false });
|
||||
}, function (event) {
|
||||
assert.equal(event.auto, false);
|
||||
assert.equal(event.isFrozen, false);
|
||||
assert.equal(event.completionModel.items.length, 0);
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('trigger - on type', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 4 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'd' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
|
||||
assert.equal(first.provider, alwaysSomethingSupport);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('#17400: Keep filtering suggestModel.ts after space', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
label: 'My Table',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'My Table',
|
||||
range: getDefaultSuggestRange(doc, pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
model.setValue('');
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
// make sure completionModel starts here!
|
||||
model.trigger({ auto: true, shy: false });
|
||||
}, event => {
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'My' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
assert.equal(first.completion.label, 'My Table');
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 3 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: ' ' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
assert.equal(first.completion.label, 'My Table');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('#21484: Trigger character always force a new completion session', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
label: 'foo.bar',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'foo.bar',
|
||||
range: Range.fromPositions(pos.with(undefined, 1), pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
triggerCharacters: ['.'],
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
label: 'boom',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'boom',
|
||||
range: Range.fromPositions(
|
||||
pos.delta(0, doc.getLineContent(pos.lineNumber)[pos.column - 2] === '.' ? 0 : -1),
|
||||
pos
|
||||
)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
model.setValue('');
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'foo' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
assert.equal(first.completion.label, 'foo.bar');
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.trigger('keyboard', Handler.Type, { text: '.' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 2);
|
||||
const [first, second] = event.completionModel.items;
|
||||
assert.equal(first.completion.label, 'foo.bar');
|
||||
assert.equal(second.completion.label, 'boom');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [1/2]', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
editor.getModel()!.setValue('fo');
|
||||
editor.setPosition({ lineNumber: 1, column: 3 });
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
model.trigger({ auto: false, shy: false });
|
||||
}, event => {
|
||||
assert.equal(event.auto, false);
|
||||
assert.equal(event.isFrozen, false);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
|
||||
return assertEvent(model.onDidCancel, () => {
|
||||
editor.trigger('keyboard', Handler.Type, { text: '+' });
|
||||
}, event => {
|
||||
assert.equal(event.retrigger, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [2/2]', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
editor.getModel()!.setValue('fo');
|
||||
editor.setPosition({ lineNumber: 1, column: 3 });
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
model.trigger({ auto: false, shy: false });
|
||||
}, event => {
|
||||
assert.equal(event.auto, false);
|
||||
assert.equal(event.isFrozen, false);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
|
||||
return assertEvent(model.onDidCancel, () => {
|
||||
editor.trigger('keyboard', Handler.Type, { text: ' ' });
|
||||
}, event => {
|
||||
assert.equal(event.retrigger, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (1/2)', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: true,
|
||||
suggestions: [{
|
||||
label: 'foo',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'foo',
|
||||
range: Range.fromPositions(pos.with(undefined, 1), pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
editor.getModel()!.setValue('foo');
|
||||
editor.setPosition({ lineNumber: 1, column: 4 });
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
model.trigger({ auto: false, shy: false });
|
||||
}, event => {
|
||||
assert.equal(event.auto, false);
|
||||
assert.equal(event.completionModel.incomplete.size, 1);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
|
||||
return assertEvent(model.onDidCancel, () => {
|
||||
editor.trigger('keyboard', Handler.Type, { text: ';' });
|
||||
}, event => {
|
||||
assert.equal(event.retrigger, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (2/2)', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: true,
|
||||
suggestions: [{
|
||||
label: 'foo;',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'foo',
|
||||
range: Range.fromPositions(pos.with(undefined, 1), pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
editor.getModel()!.setValue('foo');
|
||||
editor.setPosition({ lineNumber: 1, column: 4 });
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
model.trigger({ auto: false, shy: false });
|
||||
}, event => {
|
||||
assert.equal(event.auto, false);
|
||||
assert.equal(event.completionModel.incomplete.size, 1);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
// while we cancel incrementally enriching the set of
|
||||
// completions we still filter against those that we have
|
||||
// until now
|
||||
editor.trigger('keyboard', Handler.Type, { text: ';' });
|
||||
}, event => {
|
||||
assert.equal(event.auto, false);
|
||||
assert.equal(event.completionModel.incomplete.size, 1);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Trigger character is provided in suggest context', function () {
|
||||
let triggerCharacter = '';
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
triggerCharacters: ['.'],
|
||||
provideCompletionItems(doc, pos, context): CompletionList {
|
||||
assert.equal(context.triggerKind, CompletionTriggerKind.TriggerCharacter);
|
||||
triggerCharacter = context.triggerCharacter!;
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [
|
||||
{
|
||||
label: 'foo.bar',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'foo.bar',
|
||||
range: Range.fromPositions(pos.with(undefined, 1), pos)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
model.setValue('');
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'foo.' });
|
||||
}, event => {
|
||||
assert.equal(triggerCharacter, '.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Mac press and hold accent character insertion does not update suggestions, #35269', function () {
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: true,
|
||||
suggestions: [{
|
||||
label: 'abc',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'abc',
|
||||
range: Range.fromPositions(pos.with(undefined, 1), pos)
|
||||
}, {
|
||||
label: 'äbc',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'äbc',
|
||||
range: Range.fromPositions(pos.with(undefined, 1), pos)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
model.setValue('');
|
||||
return withOracle((model, editor) => {
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'a' });
|
||||
}, event => {
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
assert.equal(event.completionModel.items[0].completion.label, 'abc');
|
||||
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.executeEdits('test', [EditOperation.replace(new Range(1, 1, 1, 2), 'ä')]);
|
||||
|
||||
}, event => {
|
||||
// suggest model changed to äbc
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
assert.equal(event.completionModel.items[0].completion.label, 'äbc');
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Backspace should not always cancel code completion, #36491', function () {
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
|
||||
|
||||
return withOracle(async (model, editor) => {
|
||||
await assertEvent(model.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 4 });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'd' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
|
||||
assert.equal(first.provider, alwaysSomethingSupport);
|
||||
});
|
||||
|
||||
await assertEvent(model.onDidSuggest, () => {
|
||||
CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
|
||||
assert.equal(first.provider, alwaysSomethingSupport);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Text changes for completion CodeAction are affected by the completion #39893', function () {
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos): CompletionList {
|
||||
return {
|
||||
incomplete: true,
|
||||
suggestions: [{
|
||||
label: 'bar',
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: 'bar',
|
||||
range: Range.fromPositions(pos.delta(0, -2), pos),
|
||||
additionalTextEdits: [{
|
||||
text: ', bar',
|
||||
range: { startLineNumber: 1, endLineNumber: 1, startColumn: 17, endColumn: 17 }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
model.setValue('ba; import { foo } from "./b"');
|
||||
|
||||
return withOracle(async (sugget, editor) => {
|
||||
class TestCtrl extends SuggestController {
|
||||
_insertSuggestion(item: ISelectedSuggestion, flags: number = 0) {
|
||||
super._insertSuggestion(item, flags);
|
||||
}
|
||||
}
|
||||
const ctrl = <TestCtrl>editor.registerAndInstantiateContribution(TestCtrl.ID, TestCtrl);
|
||||
editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
|
||||
|
||||
await assertEvent(sugget.onDidSuggest, () => {
|
||||
editor.setPosition({ lineNumber: 1, column: 3 });
|
||||
sugget.trigger({ auto: false, shy: false });
|
||||
}, event => {
|
||||
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
assert.equal(first.completion.label, 'bar');
|
||||
|
||||
ctrl._insertSuggestion({ item: first, index: 0, model: event.completionModel });
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
model.getValue(),
|
||||
'bar; import { foo, bar } from "./b"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Completion unexpectedly triggers on second keypress of an edit group in a snippet #43523', function () {
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, alwaysSomethingSupport));
|
||||
|
||||
return withOracle((model, editor) => {
|
||||
return assertEvent(model.onDidSuggest, () => {
|
||||
editor.setValue('d');
|
||||
editor.setSelection(new Selection(1, 1, 1, 2));
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'e' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
const [first] = event.completionModel.items;
|
||||
|
||||
assert.equal(first.provider, alwaysSomethingSupport);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('Fails to render completion details #47988', function () {
|
||||
|
||||
let disposeA = 0;
|
||||
let disposeB = 0;
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
incomplete: true,
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Folder,
|
||||
label: 'CompleteNot',
|
||||
insertText: 'Incomplete',
|
||||
sortText: 'a',
|
||||
range: getDefaultSuggestRange(doc, pos)
|
||||
}],
|
||||
dispose() { disposeA += 1; }
|
||||
};
|
||||
}
|
||||
}));
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Folder,
|
||||
label: 'Complete',
|
||||
insertText: 'Complete',
|
||||
sortText: 'z',
|
||||
range: getDefaultSuggestRange(doc, pos)
|
||||
}],
|
||||
dispose() { disposeB += 1; }
|
||||
};
|
||||
},
|
||||
resolveCompletionItem(item) {
|
||||
return item;
|
||||
},
|
||||
}));
|
||||
|
||||
return withOracle(async (model, editor) => {
|
||||
|
||||
await assertEvent(model.onDidSuggest, () => {
|
||||
editor.setValue('');
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'c' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 2);
|
||||
assert.equal(disposeA, 0);
|
||||
assert.equal(disposeB, 0);
|
||||
});
|
||||
|
||||
await assertEvent(model.onDidSuggest, () => {
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'o' });
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 2);
|
||||
|
||||
// clean up
|
||||
model.clear();
|
||||
assert.equal(disposeA, 2); // provide got called two times!
|
||||
assert.equal(disposeB, 1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('Trigger (full) completions when (incomplete) completions are already active #99504', function () {
|
||||
|
||||
let countA = 0;
|
||||
let countB = 0;
|
||||
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
countA += 1;
|
||||
return {
|
||||
incomplete: false, // doesn't matter if incomplete or not
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Class,
|
||||
label: 'Z aaa',
|
||||
insertText: 'Z aaa',
|
||||
range: new Range(1, 1, pos.lineNumber, pos.column)
|
||||
}],
|
||||
};
|
||||
}
|
||||
}));
|
||||
disposables.push(CompletionProviderRegistry.register({ scheme: 'test' }, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
countB += 1;
|
||||
if (!doc.getWordUntilPosition(pos).word.startsWith('a')) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
incomplete: false,
|
||||
suggestions: [{
|
||||
kind: CompletionItemKind.Folder,
|
||||
label: 'aaa',
|
||||
insertText: 'aaa',
|
||||
range: getDefaultSuggestRange(doc, pos)
|
||||
}],
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
return withOracle(async (model, editor) => {
|
||||
|
||||
await assertEvent(model.onDidSuggest, () => {
|
||||
editor.setValue('');
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'Z' });
|
||||
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 1);
|
||||
assert.equal(event.completionModel.items[0].textLabel, 'Z aaa');
|
||||
});
|
||||
|
||||
await assertEvent(model.onDidSuggest, () => {
|
||||
// started another word: Z a|
|
||||
// item should be: Z aaa, aaa
|
||||
editor.trigger('keyboard', Handler.Type, { text: ' a' });
|
||||
}, event => {
|
||||
assert.equal(event.auto, true);
|
||||
assert.equal(event.completionModel.items.length, 2);
|
||||
assert.equal(event.completionModel.items[0].textLabel, 'Z aaa');
|
||||
assert.equal(event.completionModel.items[1].textLabel, 'aaa');
|
||||
|
||||
assert.equal(countA, 1); // should we keep the suggestions from the "active" provider?, Yes! See: #106573
|
||||
assert.equal(countB, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
|
||||
import { mock } from 'vs/base/test/common/mock';
|
||||
import { EditorWorkerHost, EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
|
||||
import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/model/wordHelper';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
|
||||
|
||||
suite('suggest, word distance', function () {
|
||||
|
||||
class BracketMode extends MockMode {
|
||||
|
||||
private static readonly _id = new modes.LanguageIdentifier('bracketMode', 3);
|
||||
|
||||
constructor() {
|
||||
super(BracketMode._id);
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
|
||||
brackets: [
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')'],
|
||||
]
|
||||
}));
|
||||
}
|
||||
}
|
||||
let distance: WordDistance;
|
||||
let disposables = new DisposableStore();
|
||||
|
||||
setup(async function () {
|
||||
|
||||
disposables.clear();
|
||||
let mode = new BracketMode();
|
||||
let model = createTextModel('function abc(aa, ab){\na\n}', undefined, mode.getLanguageIdentifier(), URI.parse('test:///some.path'));
|
||||
let editor = createTestCodeEditor({ model: model });
|
||||
editor.updateOptions({ suggest: { localityBonus: true } });
|
||||
editor.setPosition({ lineNumber: 2, column: 2 });
|
||||
|
||||
let modelService = new class extends mock<IModelService>() {
|
||||
onModelRemoved = Event.None;
|
||||
getModel(uri: URI) {
|
||||
return uri.toString() === model.uri.toString() ? model : null;
|
||||
}
|
||||
};
|
||||
|
||||
let service = new class extends EditorWorkerServiceImpl {
|
||||
|
||||
private _worker = new EditorSimpleWorker(new class extends mock<EditorWorkerHost>() { }, null);
|
||||
|
||||
constructor() {
|
||||
super(modelService, new class extends mock<ITextResourceConfigurationService>() { }, new NullLogService());
|
||||
this._worker.acceptNewModel({
|
||||
url: model.uri.toString(),
|
||||
lines: model.getLinesContent(),
|
||||
EOL: model.getEOL(),
|
||||
versionId: model.getVersionId()
|
||||
});
|
||||
model.onDidChangeContent(e => this._worker.acceptModelChanged(model.uri.toString(), e));
|
||||
}
|
||||
computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> {
|
||||
return this._worker.computeWordRanges(resource.toString(), range, DEFAULT_WORD_REGEXP.source, DEFAULT_WORD_REGEXP.flags);
|
||||
}
|
||||
};
|
||||
|
||||
distance = await WordDistance.create(service, editor);
|
||||
|
||||
disposables.add(service);
|
||||
disposables.add(mode);
|
||||
disposables.add(model);
|
||||
disposables.add(editor);
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
disposables.clear();
|
||||
});
|
||||
|
||||
function createSuggestItem(label: string, overwriteBefore: number, position: IPosition): CompletionItem {
|
||||
const suggestion: modes.CompletionItem = {
|
||||
label,
|
||||
range: { startLineNumber: position.lineNumber, startColumn: position.column - overwriteBefore, endLineNumber: position.lineNumber, endColumn: position.column },
|
||||
insertText: label,
|
||||
kind: 0
|
||||
};
|
||||
const container: modes.CompletionList = {
|
||||
suggestions: [suggestion]
|
||||
};
|
||||
const provider: modes.CompletionItemProvider = {
|
||||
provideCompletionItems(): any {
|
||||
return;
|
||||
}
|
||||
};
|
||||
return new CompletionItem(position, suggestion, container, provider);
|
||||
}
|
||||
|
||||
test('Suggest locality bonus can boost current word #90515', function () {
|
||||
const pos = { lineNumber: 2, column: 2 };
|
||||
const d1 = distance.distance(pos, createSuggestItem('a', 1, pos).completion);
|
||||
const d2 = distance.distance(pos, createSuggestItem('aa', 1, pos).completion);
|
||||
const d3 = distance.distance(pos, createSuggestItem('ab', 1, pos).completion);
|
||||
|
||||
assert.ok(d1 > d2);
|
||||
assert.ok(d2 === d3);
|
||||
});
|
||||
});
|
||||
68
lib/vscode/src/vs/editor/contrib/suggest/wordContextKey.ts
Normal file
68
lib/vscode/src/vs/editor/contrib/suggest/wordContextKey.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export class WordContextKey extends Disposable {
|
||||
|
||||
static readonly AtEnd = new RawContextKey<boolean>('atEndOfWord', false);
|
||||
|
||||
private readonly _ckAtEnd: IContextKey<boolean>;
|
||||
|
||||
private _enabled: boolean = false;
|
||||
private _selectionListener?: IDisposable;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super();
|
||||
this._ckAtEnd = WordContextKey.AtEnd.bindTo(contextKeyService);
|
||||
this._register(this._editor.onDidChangeConfiguration(e => e.hasChanged(EditorOption.tabCompletion) && this._update()));
|
||||
this._update();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this._selectionListener?.dispose();
|
||||
this._ckAtEnd.reset();
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
// only update this when tab completions are enabled
|
||||
const enabled = this._editor.getOption(EditorOption.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
82
lib/vscode/src/vs/editor/contrib/suggest/wordDistance.ts
Normal file
82
lib/vscode/src/vs/editor/contrib/suggest/wordDistance.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. 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 { CompletionItem, CompletionItemKind } from 'vs/editor/common/modes';
|
||||
import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export abstract class WordDistance {
|
||||
|
||||
static readonly None = new class extends WordDistance {
|
||||
distance() { return 0; }
|
||||
};
|
||||
|
||||
static async create(service: IEditorWorkerService, editor: ICodeEditor): Promise<WordDistance> {
|
||||
|
||||
if (!editor.getOption(EditorOption.suggest).localityBonus) {
|
||||
return WordDistance.None;
|
||||
}
|
||||
|
||||
if (!editor.hasModel()) {
|
||||
return WordDistance.None;
|
||||
}
|
||||
|
||||
const model = editor.getModel();
|
||||
const position = editor.getPosition();
|
||||
|
||||
if (!service.canComputeWordRanges(model.uri)) {
|
||||
return WordDistance.None;
|
||||
}
|
||||
|
||||
const [ranges] = await new BracketSelectionRangeProvider().provideSelectionRanges(model, [position]);
|
||||
if (ranges.length === 0) {
|
||||
return WordDistance.None;
|
||||
}
|
||||
|
||||
const wordRanges = await service.computeWordRanges(model.uri, ranges[0].range);
|
||||
if (!wordRanges) {
|
||||
return WordDistance.None;
|
||||
}
|
||||
|
||||
// remove current word
|
||||
const wordUntilPos = model.getWordUntilPosition(position);
|
||||
delete wordRanges[wordUntilPos.word];
|
||||
|
||||
return new class extends WordDistance {
|
||||
distance(anchor: IPosition, suggestion: CompletionItem) {
|
||||
if (!position.equals(editor.getPosition())) {
|
||||
return 0;
|
||||
}
|
||||
if (suggestion.kind === CompletionItemKind.Keyword) {
|
||||
return 2 << 20;
|
||||
}
|
||||
let word = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.name;
|
||||
let wordLines = wordRanges[word];
|
||||
if (isFalsyOrEmpty(wordLines)) {
|
||||
return 2 << 20;
|
||||
}
|
||||
let idx = binarySearch(wordLines, Range.fromPositions(anchor), Range.compareRangesUsingStarts);
|
||||
let bestWordRange = idx >= 0 ? wordLines[idx] : wordLines[Math.max(0, ~idx - 1)];
|
||||
let blockDistance = ranges.length;
|
||||
for (const range of ranges) {
|
||||
if (!Range.containsRange(range.range, bestWordRange)) {
|
||||
break;
|
||||
}
|
||||
blockDistance -= 1;
|
||||
}
|
||||
return blockDistance;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
abstract distance(anchor: IPosition, suggestion: CompletionItem): number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user