提交 0f6df630 编写于 作者: J Johannes Rieken

highlight future replace range, add setting to disable, #10266

上级 6be5444b
......@@ -2354,6 +2354,10 @@ export interface ISuggestOptions {
* Overwrite word ends on accept. Default to false.
*/
overwriteOnAccept?: boolean;
/**
* Should the editor highlight what text suggest will replace.
*/
highlightReplaceRange?: boolean;
/**
* Enable graceful matching. Defaults to true.
*/
......@@ -2487,6 +2491,7 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
constructor() {
const defaults: InternalSuggestOptions = {
overwriteOnAccept: false,
highlightReplaceRange: true,
filterGraceful: true,
snippetsPreventQuickSuggestions: true,
localityBonus: false,
......@@ -2527,6 +2532,11 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
default: defaults.overwriteOnAccept,
description: nls.localize('suggest.overwriteOnAccept', "Controls whether words are overwritten when accepting completions.")
},
'editor.suggest.highlightReplaceRange': {
type: 'boolean',
default: defaults.highlightReplaceRange,
description: nls.localize('suggest.highlightReplaceRange', "Controls whether the editor highlights what text suggestions will replace.")
},
'editor.suggest.filterGraceful': {
type: 'boolean',
default: defaults.filterGraceful,
......@@ -2704,6 +2714,7 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
const input = _input as ISuggestOptions;
return {
overwriteOnAccept: EditorBooleanOption.boolean(input.overwriteOnAccept, this.defaultValue.overwriteOnAccept),
highlightReplaceRange: EditorBooleanOption.boolean(input.highlightReplaceRange, this.defaultValue.highlightReplaceRange),
filterGraceful: EditorBooleanOption.boolean(input.filterGraceful, this.defaultValue.filterGraceful),
snippetsPreventQuickSuggestions: EditorBooleanOption.boolean(input.snippetsPreventQuickSuggestions, this.defaultValue.filterGraceful),
localityBonus: EditorBooleanOption.boolean(input.localityBonus, this.defaultValue.localityBonus),
......
......@@ -31,12 +31,13 @@ import { WordContextKey } from 'vs/editor/contrib/suggest/wordContextKey';
import { Event } from 'vs/base/common/event';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IdleValue } from 'vs/base/common/async';
import { isObject } from 'vs/base/common/types';
import { isObject, assertType } from 'vs/base/common/types';
import { CommitCharacterController } from './suggestCommitCharacters';
import { IPosition } 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 { SuggestRangeHighlighter } from 'vs/editor/contrib/suggest/suggestRangeHighlighter';
/**
* Stop suggest widget from disappearing when clicking into other areas
......@@ -101,33 +102,36 @@ export class SuggestController implements IEditorContribution {
return editor.getContribution<SuggestController>(SuggestController.ID);
}
private readonly _model: SuggestModel;
private readonly _widget: IdleValue<SuggestWidget>;
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();
constructor(
private _editor: ICodeEditor,
editor: ICodeEditor,
@IEditorWorkerService editorWorker: IEditorWorkerService,
@ISuggestMemoryService private readonly _memoryService: ISuggestMemoryService,
@ICommandService private readonly _commandService: ICommandService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
this._model = new SuggestModel(this._editor, editorWorker);
this.editor = editor;
this.model = new SuggestModel(this.editor, editorWorker);
this._widget = new IdleValue(() => {
this.widget = new IdleValue(() => {
const widget = this._instantiationService.createInstance(SuggestWidget, this._editor);
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));
const commitCharacterController = new CommitCharacterController(this.editor, widget, item => this._insertSuggestion(item, InsertFlags.NoAfterUndoStop));
this._toDispose.add(commitCharacterController);
this._toDispose.add(this._model.onDidSuggest(e => {
this._toDispose.add(this.model.onDidSuggest(e => {
if (e.completionModel.items.length === 0) {
commitCharacterController.reset();
}
......@@ -137,19 +141,19 @@ export class SuggestController implements IEditorContribution {
let makesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService);
this._toDispose.add(widget.onDidFocus(({ item }) => {
const position = this._editor.getPosition()!;
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
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({
const oldText = this.editor.getModel()!.getValueInRange({
startLineNumber: position.lineNumber,
startColumn,
endLineNumber: position.lineNumber,
......@@ -161,38 +165,40 @@ export class SuggestController implements IEditorContribution {
}));
this._toDispose.add(toDisposable(() => makesTextEdit.reset()));
return widget;
});
this._alternatives = new IdleValue(() => {
return this._toDispose.add(new SuggestAlternatives(this._editor, this._contextKeyService));
return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService));
});
this._toDispose.add(_instantiationService.createInstance(WordContextKey, _editor));
this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor));
this._toDispose.add(this._model.onDidTrigger(e => {
this._widget.getValue().showTriggered(e.auto, e.shy ? 250 : 50);
this._lineSuffix.value = new LineSuffix(this._editor.getModel()!, e.position);
this._toDispose.add(this.model.onDidTrigger(e => {
this.widget.getValue().showTriggered(e.auto, e.shy ? 250 : 50);
this._lineSuffix.value = new LineSuffix(this.editor.getModel()!, e.position);
}));
this._toDispose.add(this._model.onDidSuggest(e => {
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.getValue().showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
let index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items);
this.widget.getValue().showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
}
}));
this._toDispose.add(this._model.onDidCancel(e => {
this._toDispose.add(this.model.onDidCancel(e => {
if (!e.retrigger) {
this._widget.getValue().hideWidget();
this.widget.getValue().hideWidget();
}
}));
this._toDispose.add(this._editor.onDidBlurEditorWidget(() => {
this._toDispose.add(this.editor.onDidBlurEditorWidget(() => {
if (!_sticky) {
this._model.cancel();
this._model.clear();
this.model.cancel();
this.model.clear();
}
}));
this._toDispose.add(this._widget.getValue().onDetailsKeyDown(e => {
this._toDispose.add(this.widget.getValue().onDetailsKeyDown(e => {
// cmd + c on macOS, ctrl + c on Win / Linux
if (
e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, KeyCode.KEY_C)) ||
......@@ -203,25 +209,28 @@ export class SuggestController implements IEditorContribution {
}
if (!e.toKeybinding().isModifierKey()) {
this._editor.focus();
this.editor.focus();
}
}));
// Manage the acceptSuggestionsOnEnter context key
let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService);
let updateFromConfig = () => {
const acceptSuggestionOnEnter = this._editor.getOption(EditorOption.acceptSuggestionOnEnter);
const acceptSuggestionOnEnter = this.editor.getOption(EditorOption.acceptSuggestionOnEnter);
acceptSuggestionsOnEnter.set(acceptSuggestionOnEnter === 'on' || acceptSuggestionOnEnter === 'smart');
};
this._toDispose.add(this._editor.onDidChangeConfiguration(() => updateFromConfig()));
this._toDispose.add(this.editor.onDidChangeConfiguration(() => updateFromConfig()));
updateFromConfig();
// create range highlighter
this._toDispose.add(new SuggestRangeHighlighter(this));
}
dispose(): void {
this._alternatives.dispose();
this._toDispose.dispose();
this._widget.dispose();
this._model.dispose();
this.widget.dispose();
this.model.dispose();
this._lineSuffix.dispose();
}
......@@ -231,32 +240,31 @@ export class SuggestController implements IEditorContribution {
): void {
if (!event || !event.item) {
this._alternatives.getValue().reset();
this._model.cancel();
this._model.clear();
this.model.cancel();
this.model.clear();
return;
}
if (!this._editor.hasModel()) {
if (!this.editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const model = this.editor.getModel();
const modelVersionNow = model.getAlternativeVersionId();
const { item } = event;
const { completion: suggestion, position } = item;
const columnDelta = this._editor.getPosition().column - position.column;
const { completion: suggestion } = item;
// pushing undo stops *before* additional text edits and
// *after* the main edit
if (!(flags & InsertFlags.NoBeforeUndoStop)) {
this._editor.pushUndoStop();
this.editor.pushUndoStop();
}
if (Array.isArray(suggestion.additionalTextEdits)) {
this._editor.executeEdits('suggestController.additionalTextEdits', suggestion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
this.editor.executeEdits('suggestController.additionalTextEdits', suggestion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
}
// keep item in memory
this._memoryService.memorize(model, this._editor.getPosition(), item);
this._memoryService.memorize(model, this.editor.getPosition(), item);
let { insertText } = suggestion;
if (!(suggestion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) {
......@@ -264,40 +272,38 @@ export class SuggestController implements IEditorContribution {
}
const overwriteConfig = flags & InsertFlags.AlternativeOverwriteConfig
? !this._editor.getOption(EditorOption.suggest).overwriteOnAccept
: this._editor.getOption(EditorOption.suggest).overwriteOnAccept;
? !this.editor.getOption(EditorOption.suggest).overwriteOnAccept
: this.editor.getOption(EditorOption.suggest).overwriteOnAccept;
const overwriteBefore = position.column - item.editStart.column;
const overwriteAfter = (overwriteConfig ? item.editReplaceEnd.column : item.editInsertEnd.column) - position.column;
const suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this._editor.getPosition()) : 0;
const info = this.getOverwriteInfo(item, overwriteConfig);
SnippetController2.get(this._editor).insert(insertText, {
overwriteBefore: overwriteBefore + columnDelta,
overwriteAfter: overwriteAfter + suffixDelta,
SnippetController2.get(this.editor).insert(insertText, {
overwriteBefore: info.overwriteBefore,
overwriteAfter: info.overwriteAfter,
undoStopBefore: false,
undoStopAfter: false,
adjustWhitespace: !(suggestion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace)
});
if (!(flags & InsertFlags.NoAfterUndoStop)) {
this._editor.pushUndoStop();
this.editor.pushUndoStop();
}
if (!suggestion.command) {
// done
this._model.cancel();
this._model.clear();
this.model.cancel();
this.model.clear();
} else if (suggestion.command.id === TriggerSuggestAction.id) {
// retigger
this._model.trigger({ auto: true, shy: false }, true);
this.model.trigger({ auto: true, shy: false }, true);
} else {
// exec command, done
this._commandService.executeCommand(suggestion.command.id, ...(suggestion.command.arguments ? [...suggestion.command.arguments] : []))
.catch(onUnexpectedError)
.finally(() => this._model.clear()); // <- clear only now, keep commands alive
this._model.cancel();
.finally(() => this.model.clear()); // <- clear only now, keep commands alive
this.model.cancel();
}
if (flags & InsertFlags.KeepAlternativeSuggestions) {
......@@ -321,6 +327,20 @@ export class SuggestController implements IEditorContribution {
this._alertCompletionItem(event.item);
}
getOverwriteInfo(item: CompletionItem, overwriteOnAccept: boolean): { overwriteBefore: number, overwriteAfter: number } {
assertType(this.editor.hasModel());
const overwriteBefore = item.position.column - item.editStart.column;
const overwriteAfter = (overwriteOnAccept ? 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 {
if (isNonEmptyArray(suggestion.additionalTextEdits)) {
let msg = nls.localize('arai.alert.snippet', "Accepting '{0}' made {1} additional edits", suggestion.label, suggestion.additionalTextEdits.length);
......@@ -329,22 +349,22 @@ export class SuggestController implements IEditorContribution {
}
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();
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()) {
if (!this.editor.hasModel()) {
return;
}
const positionNow = this._editor.getPosition();
const positionNow = this.editor.getPosition();
const fallback = () => {
if (positionNow.equals(this._editor.getPosition()!)) {
if (positionNow.equals(this.editor.getPosition()!)) {
this._commandService.executeCommand(arg.fallback);
}
};
......@@ -354,14 +374,14 @@ export class SuggestController implements IEditorContribution {
// snippet, other editor -> makes edit
return true;
}
const position = this._editor.getPosition()!;
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({
const textNow = this.editor.getModel()!.getValueInRange({
startLineNumber: position.lineNumber,
startColumn,
endLineNumber: position.lineNumber,
......@@ -371,41 +391,41 @@ export class SuggestController implements IEditorContribution {
return textNow !== item.completion.insertText;
};
Event.once(this._model.onDidTrigger)(_ => {
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)(() => {
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 }) => {
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 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.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();
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.getValue().getFocusedItem();
const item = this.widget.getValue().getFocusedItem();
let flags = 0;
if (keepAlternativeSuggestions) {
flags |= InsertFlags.KeepAlternativeSuggestions;
......@@ -425,45 +445,45 @@ export class SuggestController implements IEditorContribution {
}
cancelSuggestWidget(): void {
this._model.cancel();
this._model.clear();
this._widget.getValue().hideWidget();
this.model.cancel();
this.model.clear();
this.widget.getValue().hideWidget();
}
selectNextSuggestion(): void {
this._widget.getValue().selectNext();
this.widget.getValue().selectNext();
}
selectNextPageSuggestion(): void {
this._widget.getValue().selectNextPage();
this.widget.getValue().selectNextPage();
}
selectLastSuggestion(): void {
this._widget.getValue().selectLast();
this.widget.getValue().selectLast();
}
selectPrevSuggestion(): void {
this._widget.getValue().selectPrevious();
this.widget.getValue().selectPrevious();
}
selectPrevPageSuggestion(): void {
this._widget.getValue().selectPreviousPage();
this.widget.getValue().selectPreviousPage();
}
selectFirstSuggestion(): void {
this._widget.getValue().selectFirst();
this.widget.getValue().selectFirst();
}
toggleSuggestionDetails(): void {
this._widget.getValue().toggleDetails();
this.widget.getValue().toggleDetails();
}
toggleExplainMode(): void {
this._widget.getValue().toggleExplainMode();
this.widget.getValue().toggleExplainMode();
}
toggleSuggestionFocus(): void {
this._widget.getValue().toggleDetailsFocus();
this.widget.getValue().toggleDetailsFocus();
}
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorSelectionBackground, registerColor, editorSelectionHighlightBorder } from 'vs/platform/theme/common/colorRegistry';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { localize } from 'vs/nls';
const suggestReplaceBackgroundColor = registerColor(
'editor.suggestReplaceBackground',
{ light: editorSelectionBackground, dark: editorSelectionBackground, hc: editorSelectionBackground },
localize('suggestReplaceBackground', "Background color of text that suggest will replace.")
);
const suggestReplaceBorderColor = registerColor(
'editor.suggestReplaceBorder',
{ light: null, dark: null, hc: editorSelectionHighlightBorder },
localize('suggestReplaceBorder', "Border color of text that suggest will replace.")
);
registerThemingParticipant((theme, collector) => {
const suggestReplaceBackground = theme.getColor(suggestReplaceBackgroundColor);
if (suggestReplaceBackground) {
collector.addRule(`.monaco-editor .suggestReplace { background-color: ${suggestReplaceBackground}; }`);
}
const suggestReplaceBorder = theme.getColor(suggestReplaceBorderColor);
if (suggestReplaceBorder) {
collector.addRule(`.monaco-editor .suggestReplace { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${suggestReplaceBorder}; }`);
}
});
export class SuggestRangeHighlighter {
private readonly _disposables = new DisposableStore();
private _decorations: string[] = [];
private _hasWidgetListener: boolean = false;
constructor(private readonly _controller: SuggestController) {
this._disposables.add(_controller.model.onDidSuggest(e => {
if (!e.shy) {
const widget = this._controller.widget.getValue();
const focused = widget.getFocusedItem();
if (focused) {
this._highlight(focused.item);
}
if (!this._hasWidgetListener) {
this._hasWidgetListener = true;
widget.onDidFocus(e => this._highlight(e.item), undefined, this._disposables);
}
}
}));
this._disposables.add(_controller.model.onDidCancel(() => {
this._reset();
}));
}
dispose(): void {
this._reset();
this._disposables.dispose();
}
private _reset(): void {
this._decorations = this._controller.editor.deltaDecorations(this._decorations, []);
}
private _highlight(item: CompletionItem) {
const { overwriteOnAccept, highlightReplaceRange } = this._controller.editor.getOption(EditorOption.suggest);
if (highlightReplaceRange) {
const info = this._controller.getOverwriteInfo(item, overwriteOnAccept);
const position = this._controller.editor.getPosition()!;
const range = new Range(
position.lineNumber, position.column - info.overwriteBefore,
position.lineNumber, position.column + info.overwriteAfter
);
this._decorations = this._controller.editor.deltaDecorations(this._decorations, [{
range,
options: {
className: 'suggestReplace',
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges
}
}]);
}
}
}
......@@ -35,6 +35,7 @@ suite('CompletionModel', function () {
let defaultOptions = {
overwriteOnAccept: false,
highlightReplaceRange: true,
snippetsPreventQuickSuggestions: true,
filterGraceful: true,
localityBonus: false,
......
......@@ -3397,6 +3397,10 @@ declare namespace monaco.editor {
* Overwrite word ends on accept. Default to false.
*/
overwriteOnAccept?: boolean;
/**
* Should the editor highlight what text suggest will replace.
*/
highlightReplaceRange?: boolean;
/**
* Enable graceful matching. Defaults to true.
*/
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册