mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-26 23:00:29 -04:00
Initial VS Code 1.19 source merge (#571)
* Initial 1.19 xcopy * Fix yarn build * Fix numerous build breaks * Next batch of build break fixes * More build break fixes * Runtime breaks * Additional post merge fixes * Fix windows setup file * Fix test failures. * Update license header blocks to refer to source eula
This commit is contained in:
478
src/vs/editor/contrib/suggest/suggestModel.ts
Normal file
478
src/vs/editor/contrib/suggest/suggestModel.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
|
||||
import { TimeoutTimer } from 'vs/base/common/async';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IModel, IWordAtPosition } from 'vs/editor/common/editorCommon';
|
||||
import { ISuggestSupport, SuggestRegistry, StandardTokenType, SuggestTriggerKind } from 'vs/editor/common/modes';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { provideSuggestionItems, getSuggestionComparator, ISuggestionItem } from './suggest';
|
||||
import { CompletionModel } from './completionModel';
|
||||
import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
|
||||
export interface ICancelEvent {
|
||||
retrigger: boolean;
|
||||
}
|
||||
|
||||
export interface ITriggerEvent {
|
||||
auto: boolean;
|
||||
}
|
||||
|
||||
export interface ISuggestEvent {
|
||||
completionModel: CompletionModel;
|
||||
isFrozen: boolean;
|
||||
auto: boolean;
|
||||
}
|
||||
|
||||
export interface SuggestTriggerContext {
|
||||
auto: boolean;
|
||||
triggerCharacter?: string;
|
||||
}
|
||||
|
||||
export class LineContext {
|
||||
|
||||
static shouldAutoTrigger(editor: ICodeEditor): boolean {
|
||||
const model = editor.getModel();
|
||||
if (!model) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
static isInEditableRange(editor: ICodeEditor): boolean {
|
||||
const model = editor.getModel();
|
||||
const position = editor.getPosition();
|
||||
if (model.hasEditableRange()) {
|
||||
const editableRange = model.getEditableRange();
|
||||
if (!editableRange.containsPosition(position)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly lineNumber: number;
|
||||
readonly column: number;
|
||||
readonly leadingLineContent: string;
|
||||
readonly leadingWord: IWordAtPosition;
|
||||
readonly auto: boolean;
|
||||
|
||||
constructor(model: IModel, position: Position, auto: 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;
|
||||
}
|
||||
}
|
||||
|
||||
export const enum State {
|
||||
Idle = 0,
|
||||
Manual = 1,
|
||||
Auto = 2
|
||||
}
|
||||
|
||||
export class SuggestModel implements IDisposable {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _toDispose: IDisposable[] = [];
|
||||
private _quickSuggestDelay: number;
|
||||
private _triggerCharacterListener: IDisposable;
|
||||
private _triggerAutoSuggestPromise: TPromise<void>;
|
||||
private _triggerRefilter = new TimeoutTimer();
|
||||
private _state: State;
|
||||
|
||||
private _requestPromise: TPromise<void>;
|
||||
private _context: LineContext;
|
||||
private _currentPosition: Position;
|
||||
|
||||
private _completionModel: CompletionModel;
|
||||
private _onDidCancel: Emitter<ICancelEvent> = new Emitter<ICancelEvent>();
|
||||
private _onDidTrigger: Emitter<ITriggerEvent> = new Emitter<ITriggerEvent>();
|
||||
private _onDidSuggest: Emitter<ISuggestEvent> = 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(editor: ICodeEditor) {
|
||||
this._editor = editor;
|
||||
this._state = State.Idle;
|
||||
this._triggerAutoSuggestPromise = null;
|
||||
this._requestPromise = null;
|
||||
this._completionModel = null;
|
||||
this._context = null;
|
||||
this._currentPosition = this._editor.getPosition() || new Position(1, 1);
|
||||
|
||||
// wire up various listeners
|
||||
this._toDispose.push(this._editor.onDidChangeModel(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this.cancel();
|
||||
}));
|
||||
this._toDispose.push(this._editor.onDidChangeModelLanguage(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this.cancel();
|
||||
}));
|
||||
this._toDispose.push(this._editor.onDidChangeConfiguration(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this._updateQuickSuggest();
|
||||
}));
|
||||
this._toDispose.push(SuggestRegistry.onDidChange(() => {
|
||||
this._updateTriggerCharacters();
|
||||
this._updateActiveSuggestSession();
|
||||
}));
|
||||
this._toDispose.push(this._editor.onDidChangeCursorSelection(e => {
|
||||
this._onCursorChange(e);
|
||||
}));
|
||||
this._toDispose.push(this._editor.onDidChangeModelContent(e => {
|
||||
this._refilterCompletionItems();
|
||||
}));
|
||||
|
||||
this._updateTriggerCharacters();
|
||||
this._updateQuickSuggest();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose([this._onDidCancel, this._onDidSuggest, this._onDidTrigger, this._triggerCharacterListener, this._triggerRefilter]);
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
dispose(this._completionModel);
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
// --- handle configuration & precondition changes
|
||||
|
||||
private _updateQuickSuggest(): void {
|
||||
this._quickSuggestDelay = this._editor.getConfiguration().contribInfo.quickSuggestionsDelay;
|
||||
|
||||
if (isNaN(this._quickSuggestDelay) || (!this._quickSuggestDelay && this._quickSuggestDelay !== 0) || this._quickSuggestDelay < 0) {
|
||||
this._quickSuggestDelay = 10;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateTriggerCharacters(): void {
|
||||
|
||||
dispose(this._triggerCharacterListener);
|
||||
|
||||
if (this._editor.getConfiguration().readOnly
|
||||
|| !this._editor.getModel()
|
||||
|| !this._editor.getConfiguration().contribInfo.suggestOnTriggerCharacters) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const supportsByTriggerCharacter: { [ch: string]: ISuggestSupport[] } = Object.create(null);
|
||||
for (const support of SuggestRegistry.all(this._editor.getModel())) {
|
||||
if (isFalsyOrEmpty(support.triggerCharacters)) {
|
||||
continue;
|
||||
}
|
||||
for (const ch of support.triggerCharacters) {
|
||||
const array = supportsByTriggerCharacter[ch];
|
||||
if (!array) {
|
||||
supportsByTriggerCharacter[ch] = [support];
|
||||
} else {
|
||||
array.push(support);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._triggerCharacterListener = this._editor.onDidType(text => {
|
||||
const lastChar = text.charAt(text.length - 1);
|
||||
const supports = supportsByTriggerCharacter[lastChar];
|
||||
|
||||
if (supports) {
|
||||
// keep existing items that where not computed by the
|
||||
// supports/providers that want to trigger now
|
||||
const items: ISuggestionItem[] = [];
|
||||
if (this._completionModel) {
|
||||
for (const item of this._completionModel.items) {
|
||||
if (supports.indexOf(item.support) < 0) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.trigger({ auto: true, triggerCharacter: lastChar }, Boolean(this._completionModel), supports, items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- trigger/retrigger/cancel suggest
|
||||
|
||||
get state(): State {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
cancel(retrigger: boolean = false): void {
|
||||
|
||||
this._triggerRefilter.cancel();
|
||||
|
||||
if (this._triggerAutoSuggestPromise) {
|
||||
this._triggerAutoSuggestPromise.cancel();
|
||||
this._triggerAutoSuggestPromise = null;
|
||||
}
|
||||
|
||||
if (this._requestPromise) {
|
||||
this._requestPromise.cancel();
|
||||
this._requestPromise = null;
|
||||
}
|
||||
|
||||
this._state = State.Idle;
|
||||
dispose(this._completionModel);
|
||||
this._completionModel = null;
|
||||
this._context = null;
|
||||
|
||||
this._onDidCancel.fire({ retrigger });
|
||||
}
|
||||
|
||||
private _updateActiveSuggestSession(): void {
|
||||
if (this._state !== State.Idle) {
|
||||
if (!SuggestRegistry.has(this._editor.getModel())) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.trigger({ auto: this._state === State.Auto }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onCursorChange(e: ICursorSelectionChangedEvent): void {
|
||||
|
||||
const prevPosition = this._currentPosition;
|
||||
this._currentPosition = this._editor.getPosition();
|
||||
|
||||
if (!e.selection.isEmpty()
|
||||
|| e.reason !== CursorChangeReason.NotSet
|
||||
|| (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 ;)
|
||||
if (this._state !== State.Idle) {
|
||||
this.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SuggestRegistry.has(this._editor.getModel())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state === State.Idle) {
|
||||
|
||||
// trigger 24x7 IntelliSense when idle, enabled, when cursor
|
||||
// moved RIGHT, and when at a good position
|
||||
if (this._editor.getConfiguration().contribInfo.quickSuggestions !== false
|
||||
&& prevPosition.isBefore(this._currentPosition)
|
||||
) {
|
||||
|
||||
this.cancel();
|
||||
|
||||
if (LineContext.shouldAutoTrigger(this._editor)) {
|
||||
this._triggerAutoSuggestPromise = TPromise.timeout(this._quickSuggestDelay);
|
||||
this._triggerAutoSuggestPromise.then(() => {
|
||||
const model = this._editor.getModel();
|
||||
const pos = this._editor.getPosition();
|
||||
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
// validate enabled now
|
||||
const { quickSuggestions } = this._editor.getConfiguration().contribInfo;
|
||||
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 { tokenType } = model
|
||||
.getLineTokens(pos.lineNumber)
|
||||
.findTokenAtOffset(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;
|
||||
}
|
||||
}
|
||||
|
||||
this._triggerAutoSuggestPromise = null;
|
||||
this.trigger({ auto: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _refilterCompletionItems(): void {
|
||||
if (this._state === State.Idle) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
if (model) {
|
||||
// refine active suggestion
|
||||
this._triggerRefilter.cancelAndSet(() => {
|
||||
const position = this._editor.getPosition();
|
||||
const ctx = new LineContext(model, position, this._state === State.Auto);
|
||||
this._onNewContext(ctx);
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
|
||||
trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: ISuggestSupport[], existingItems?: ISuggestionItem[]): void {
|
||||
|
||||
const model = this._editor.getModel();
|
||||
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
const auto = context.auto;
|
||||
const ctx = new LineContext(model, this._editor.getPosition(), auto);
|
||||
|
||||
if (!LineContext.isInEditableRange(this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous requests, change state & update UI
|
||||
this.cancel(retrigger);
|
||||
this._state = auto ? State.Auto : State.Manual;
|
||||
this._onDidTrigger.fire({ auto });
|
||||
|
||||
// Capture context when request was sent
|
||||
this._context = ctx;
|
||||
|
||||
this._requestPromise = provideSuggestionItems(model, this._editor.getPosition(),
|
||||
this._editor.getConfiguration().contribInfo.snippetSuggestions,
|
||||
onlyFrom,
|
||||
{
|
||||
triggerCharacter: context.triggerCharacter,
|
||||
triggerKind: context.triggerCharacter ? SuggestTriggerKind.TriggerCharacter : SuggestTriggerKind.Invoke
|
||||
}
|
||||
).then(items => {
|
||||
|
||||
this._requestPromise = null;
|
||||
if (this._state === State.Idle) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFalsyOrEmpty(existingItems)) {
|
||||
const cmpFn = getSuggestionComparator(this._editor.getConfiguration().contribInfo.snippetSuggestions);
|
||||
items = items.concat(existingItems).sort(cmpFn);
|
||||
}
|
||||
|
||||
const ctx = new LineContext(model, this._editor.getPosition(), auto);
|
||||
dispose(this._completionModel);
|
||||
this._completionModel = new CompletionModel(items, this._context.column, {
|
||||
leadingLineContent: ctx.leadingLineContent,
|
||||
characterCountDelta: this._context ? ctx.column - this._context.column : 0
|
||||
}, this._editor.getConfiguration().contribInfo.snippetSuggestions);
|
||||
this._onNewContext(ctx);
|
||||
|
||||
}).then(null, onUnexpectedError);
|
||||
}
|
||||
|
||||
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.column < this._context.column) {
|
||||
// typed -> moved cursor LEFT -> retrigger if still on a word
|
||||
if (ctx.leadingWord.word) {
|
||||
this.trigger({ auto: this._context.auto }, true);
|
||||
} else {
|
||||
this.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._completionModel) {
|
||||
// happens when IntelliSense is not yet computed
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.column > this._context.column && this._completionModel.incomplete && ctx.leadingWord.word.length !== 0) {
|
||||
// typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger
|
||||
const { complete, incomplete } = this._completionModel.resolveIncompleteInfo();
|
||||
this.trigger({ auto: this._state === State.Auto }, true, incomplete, complete);
|
||||
|
||||
} 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 }, 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,
|
||||
isFrozen,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user