From f557886456051fb4f8b66bb7043ab53b4f692d99 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 19 Aug 2016 17:58:57 +0200 Subject: [PATCH] store position at which an item was requested, replace CompletionItem with lightweight alternative, #2508 --- .../suggest/browser/suggestController.ts | 6 +- .../contrib/suggest/browser/suggestWidget.ts | 40 ++++----- .../contrib/suggest/common/completionModel.ts | 90 ++++++++----------- .../editor/contrib/suggest/common/suggest.ts | 6 +- .../contrib/suggest/common/suggestModel.ts | 5 -- .../test/common/completionModel.test.ts | 5 ++ 6 files changed, 72 insertions(+), 80 deletions(-) diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index b1a92aa8ff9..9a8b1ad2cc0 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -19,7 +19,7 @@ import { CodeSnippet } from 'vs/editor/contrib/snippet/common/snippet'; import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController'; import { Context as SuggestContext } from 'vs/editor/contrib/suggest/common/suggest'; import { SuggestModel } from '../common/suggestModel'; -import { CompletionItem } from '../common/completionModel'; +import { ICompletionItem } from '../common/completionModel'; import { SuggestWidget } from './suggestWidget'; export class SuggestController implements IEditorContribution { @@ -63,10 +63,10 @@ export class SuggestController implements IEditorContribution { } } - private onDidSelectItem(item: CompletionItem): void { + private onDidSelectItem(item: ICompletionItem): void { if (item) { const {insertText, overwriteBefore, overwriteAfter, additionalTextEdits, command} = item.suggestion; - const columnDelta = this.editor.getPosition().column - this.model.getTriggerPosition().column; + const columnDelta = this.editor.getPosition().column - item.position.column; // todo@joh // * order of stuff command/extraEdit/actual edit diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index ce9c1e2ff94..d93ed462192 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -23,7 +23,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/ import { IConfigurationChangedEvent } from 'vs/editor/common/editorCommon'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { Context as SuggestContext } from '../common/suggest'; -import { CompletionItem, CompletionModel } from '../common/completionModel'; +import { ICompletionItem, CompletionModel } from '../common/completionModel'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -43,7 +43,7 @@ function matchesColor(text: string) { return text && text.match(colorRegExp) ? text : null; } -class Renderer implements IRenderer { +class Renderer implements IRenderer { private triggerKeybindingLabel: string; @@ -94,9 +94,9 @@ class Renderer implements IRenderer { return data; } - renderElement(element: CompletionItem, index: number, templateData: ISuggestionTemplateData): void { + renderElement(element: ICompletionItem, index: number, templateData: ISuggestionTemplateData): void { const data = templateData; - const suggestion = (element).suggestion; + const suggestion = (element).suggestion; if (suggestion.documentation) { data.root.setAttribute('aria-label', nls.localize('suggestionWithDetailsAriaLabel', "{0}, suggestion, has details", suggestion.label)); @@ -115,7 +115,7 @@ class Renderer implements IRenderer { } } - data.highlightedLabel.set(suggestion.label, (element).highlights); + data.highlightedLabel.set(suggestion.label, (element).highlights); data.typeLabel.textContent = suggestion.detail || ''; data.documentation.textContent = suggestion.documentation || ''; @@ -146,11 +146,11 @@ class Renderer implements IRenderer { const FocusHeight = 35; const UnfocusedHeight = 19; -class Delegate implements IDelegate { +class Delegate implements IDelegate { - constructor(private listProvider: () => List) { } + constructor(private listProvider: () => List) { } - getHeight(element: CompletionItem): number { + getHeight(element: ICompletionItem): number { const focus = this.listProvider().getFocusedElements()[0]; if (element.suggestion.documentation && element === focus) { @@ -160,7 +160,7 @@ class Delegate implements IDelegate { return UnfocusedHeight; } - getTemplateId(element: CompletionItem): string { + getTemplateId(element: ICompletionItem): string { return 'suggestion'; } } @@ -223,7 +223,7 @@ class SuggestionDetails { return this.el; } - render(item: CompletionItem): void { + render(item: ICompletionItem): void { if (!item) { this.title.textContent = ''; this.type.textContent = ''; @@ -295,21 +295,21 @@ export class SuggestWidget implements IContentWidget, IDisposable { private isAuto: boolean; private loadingTimeout: number; private currentSuggestionDetails: TPromise; - private focusedItem: CompletionItem; + private focusedItem: ICompletionItem; private completionModel: CompletionModel; private element: HTMLElement; private messageElement: HTMLElement; private listElement: HTMLElement; private details: SuggestionDetails; - private delegate: IDelegate; - private list: List; + private delegate: IDelegate; + private list: List; private suggestWidgetVisible: IContextKey; private suggestWidgetMultipleSuggestions: IContextKey; private suggestionSupportsAutoAccept: IContextKey; - private onDidSelectEmitter = new Emitter(); + private onDidSelectEmitter = new Emitter(); private editorBlurTimeout: TPromise; private showTimeout: TPromise; @@ -337,7 +337,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { this.listElement = append(this.element, $('.tree')); this.details = new SuggestionDetails(this.element, this, this.editor); - let renderer: IRenderer = instantiationService.createInstance(Renderer, this, this.editor); + let renderer: IRenderer = instantiationService.createInstance(Renderer, this, this.editor); this.delegate = new Delegate(() => this.list); this.list = new List(this.listElement, this.delegate, [renderer], { @@ -389,7 +389,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { }); } - private onListSelection(e: ISelectionChangeEvent): void { + private onListSelection(e: ISelectionChangeEvent): void { if (!e.elements.length) { return; } @@ -402,7 +402,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { this.editor.focus(); } - private _getSuggestionAriaAlertLabel(item:CompletionItem): string { + private _getSuggestionAriaAlertLabel(item:ICompletionItem): string { if (item.suggestion.documentation) { return nls.localize('ariaCurrentSuggestionWithDetails',"{0}, suggestion, has details", item.suggestion.label); } else { @@ -421,7 +421,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { } } - private onListFocus(e: IFocusChangeEvent): void { + private onListFocus(e: IFocusChangeEvent): void { if (!e.elements.length) { if (this.currentSuggestionDetails) { this.currentSuggestionDetails.cancel(); @@ -527,7 +527,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { } } - get onDidSelect():Event { + get onDidSelect():Event { return this.onDidSelectEmitter.event; } @@ -646,7 +646,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { } } - getFocusedItem(): CompletionItem { + getFocusedItem(): ICompletionItem { if (this.state !== State.Hidden && this.state !== State.Empty && this.state !== State.Loading) { diff --git a/src/vs/editor/contrib/suggest/common/completionModel.ts b/src/vs/editor/contrib/suggest/common/completionModel.ts index 7b29d43aac1..5c81cd57553 100644 --- a/src/vs/editor/contrib/suggest/common/completionModel.ts +++ b/src/vs/editor/contrib/suggest/common/completionModel.ts @@ -6,28 +6,15 @@ 'use strict'; import {isFalsyOrEmpty} from 'vs/base/common/arrays'; -import {TPromise} from 'vs/base/common/winjs.base'; -import {IFilter, IMatch, fuzzyContiguousFilter} from 'vs/base/common/filters'; -import {ISuggestion, ISuggestSupport} from 'vs/editor/common/modes'; +import {IMatch, fuzzyContiguousFilter} from 'vs/base/common/filters'; +import {ISuggestSupport} from 'vs/editor/common/modes'; import {ISuggestionItem} from './suggest'; -export class CompletionItem { - - suggestion: ISuggestion; - highlights: IMatch[]; - filter: IFilter; - - constructor(private _item: ISuggestionItem) { - this.suggestion = _item.suggestion; - this.filter = _item.support && _item.support.filter || fuzzyContiguousFilter; - } - - resolve(): TPromise { - return this._item.resolve().then(() => this); - } +export interface ICompletionItem extends ISuggestionItem { + highlights?: IMatch[]; } -export interface CompletionStats { +export interface ICompletionStats { suggestionCount: number; snippetCount: number; textCount: number; @@ -42,28 +29,16 @@ export class LineContext { export class CompletionModel { private _lineContext: LineContext; - private _items: CompletionItem[] = []; - private _incomplete: ISuggestSupport[] = []; + private _items: ICompletionItem[]; - private _filteredItems: CompletionItem[] = undefined; + private _filteredItems: ICompletionItem[]; private _topScoreIdx: number; - private _stats: CompletionStats; + private _incomplete: ISuggestSupport[]; + private _stats: ICompletionStats; constructor(items: ISuggestionItem[], lineContext: LineContext) { + this._items = items; this._lineContext = lineContext; - for (const item of items) { - this._items.push(new CompletionItem(item)); - - if (item.container.incomplete - && this._incomplete.indexOf(item.support) < 0) { - - this._incomplete.push(item.support); - } - } - } - - get incomplete(): ISuggestSupport[] { - return this._incomplete; } get lineContext(): LineContext { @@ -74,49 +49,56 @@ export class CompletionModel { if (this._lineContext.leadingLineContent !== value.leadingLineContent || this._lineContext.characterCountDelta !== value.characterCountDelta) { - this._filteredItems = undefined; this._lineContext = value; + this._filteredItems = undefined; } } - get items(): CompletionItem[] { - if (!this._filteredItems) { - this._filterAndScore(); - } + get items(): ICompletionItem[] { + this._ensureCachedState(); return this._filteredItems; } get topScoreIdx(): number { - if (!this._filteredItems) { - this._filterAndScore(); - } + this._ensureCachedState(); return this._topScoreIdx; } - get stats(): CompletionStats { + get incomplete(): ISuggestSupport[] { + this._ensureCachedState(); + return this._incomplete; + } + + get stats(): ICompletionStats { + this._ensureCachedState(); + return this._stats; + } + + private _ensureCachedState(): void { if (!this._filteredItems) { - this._filterAndScore(); + this._createCachedState(); } - return this._stats; } - private _filterAndScore(): void { + private _createCachedState(): void { this._filteredItems = []; + this._incomplete = []; this._topScoreIdx = -1; this._stats = { suggestionCount: 0, snippetCount: 0, textCount: 0 }; - const {leadingLineContent, characterCountDelta} = this._lineContext; + const {leadingLineContent, characterCountDelta} = this._lineContext; let word = ''; let topScore = -1; for (const item of this._items) { - const {filter, suggestion} = item; + const {suggestion, support, container} = item; + const filter = support && support.filter || fuzzyContiguousFilter; // 'word' is that remainder of the current line that we // filter and score against. In theory each suggestion uses a // differnet word, but in practice not - that's why we cache - const wordLen = item.suggestion.overwriteBefore + characterCountDelta; + const wordLen = suggestion.overwriteBefore + characterCountDelta; if (word.length !== wordLen) { word = leadingLineContent.slice(-wordLen); } @@ -146,9 +128,15 @@ export class CompletionModel { this._topScoreIdx = this._filteredItems.length - 1; } + // collect those supports that signaled having + // an incomplete result + if (container.incomplete && this._incomplete.indexOf(support) < 0) { + this._incomplete.push(support); + } + // update stats this._stats.suggestionCount++; - switch (item.suggestion.type) { + switch (suggestion.type) { case 'snippet': this._stats.snippetCount++; break; case 'text': this._stats.textCount++; break; } diff --git a/src/vs/editor/contrib/suggest/common/suggest.ts b/src/vs/editor/contrib/suggest/common/suggest.ts index 3963df7bcf0..b02055f12a7 100644 --- a/src/vs/editor/contrib/suggest/common/suggest.ts +++ b/src/vs/editor/contrib/suggest/common/suggest.ts @@ -10,7 +10,7 @@ import {compare} from 'vs/base/common/strings'; import {assign} from 'vs/base/common/objects'; import {onUnexpectedError} from 'vs/base/common/errors'; import {TPromise} from 'vs/base/common/winjs.base'; -import {IReadOnlyModel} from 'vs/editor/common/editorCommon'; +import {IReadOnlyModel, IPosition} from 'vs/editor/common/editorCommon'; import {CommonEditorRegistry} from 'vs/editor/common/editorCommonExtensions'; import {ISuggestResult, ISuggestSupport, ISuggestion, SuggestRegistry} from 'vs/editor/common/modes'; import {ISnippetsRegistry, Extensions} from 'vs/editor/common/modes/snippetsRegistry'; @@ -25,6 +25,7 @@ export const Context = { }; export interface ISuggestionItem { + position: IPosition; suggestion: ISuggestion; container: ISuggestResult; support: ISuggestSupport; @@ -52,6 +53,8 @@ export function provideSuggestionItems(model: IReadOnlyModel, position: Position const result: ISuggestionItem[] = []; const acceptSuggestion = createSuggesionFilter(snippetConfig); + position = position.clone(); + // get provider groups, always add snippet suggestion provider const supports = SuggestRegistry.orderedGroups(model); supports.unshift([snippetSuggestSupport]); @@ -83,6 +86,7 @@ export function provideSuggestionItems(model: IReadOnlyModel, position: Position fixOverwriteBeforeAfter(suggestion, container); result.push({ + position, container, suggestion, support, diff --git a/src/vs/editor/contrib/suggest/common/suggestModel.ts b/src/vs/editor/contrib/suggest/common/suggestModel.ts index 78736f69dbf..1f6d55d76f6 100644 --- a/src/vs/editor/contrib/suggest/common/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/common/suggestModel.ts @@ -370,11 +370,6 @@ export class SuggestModel implements IDisposable { }).then(null, onUnexpectedError); } - public getTriggerPosition(): IPosition { - const {lineNumber, column} = this.context; - return { lineNumber, column }; - } - private onNewContext(ctx: Context): void { if (this.context && this.context.isDifferentContext(ctx)) { if (this.context.shouldRetrigger(ctx)) { diff --git a/src/vs/editor/contrib/suggest/test/common/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/common/completionModel.test.ts index 791e01c59ef..a01dc318034 100644 --- a/src/vs/editor/contrib/suggest/test/common/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/common/completionModel.test.ts @@ -16,6 +16,11 @@ suite('CompletionModel', function () { return new class implements ISuggestionItem { + position = { + lineNumber: 1, + column: 1 + }; + suggestion: ISuggestion = { label, overwriteBefore, -- GitLab