Skip to content

Commit 6157007

Browse files
committed
Only overtype automatically inserted characters (fixes microsoft#37315)
1 parent 076c45b commit 6157007

7 files changed

Lines changed: 302 additions & 13 deletions

File tree

src/vs/base/browser/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
122122
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
123123
export const isIPad = (userAgent.indexOf('iPad') >= 0);
124124
export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0);
125-
export const isStandalone = (window.matchMedia('(display-mode: standalone)').matches);
125+
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);
126126

127127
export function hasClipboardSupport() {
128128
if (isIE) {

src/vs/editor/browser/widget/media/editor.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,8 @@
3939
.monaco-editor .view-overlays {
4040
position: absolute;
4141
top: 0;
42-
}
42+
}
43+
44+
.monaco-editor .auto-closed-character {
45+
opacity: 0.3;
46+
}

src/vs/editor/common/controller/cursor.ts

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import { CursorCollection } from 'vs/editor/common/controller/cursorCollection';
1010
import { CursorColumns, CursorConfiguration, CursorContext, CursorState, EditOperationResult, EditOperationType, IColumnSelectData, ICursors, PartialCursorState, RevealTarget } from 'vs/editor/common/controller/cursorCommon';
1111
import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations';
1212
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
13-
import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations';
13+
import { TypeOperations, TypeWithAutoClosingCommand } from 'vs/editor/common/controller/cursorTypeOperations';
1414
import { Position } from 'vs/editor/common/core/position';
1515
import { Range } from 'vs/editor/common/core/range';
1616
import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection';
1717
import * as editorCommon from 'vs/editor/common/editorCommon';
18-
import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
18+
import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model';
1919
import { RawContentChangedType } from 'vs/editor/common/model/textModelEvents';
2020
import * as viewEvents from 'vs/editor/common/view/viewEvents';
2121
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
22+
import { dispose } from 'vs/base/common/lifecycle';
2223

2324
function containsLineMappingChanged(events: viewEvents.ViewEvent[]): boolean {
2425
for (let i = 0, len = events.length; i < len; i++) {
@@ -83,6 +84,64 @@ export class CursorModelState {
8384
}
8485
}
8586

87+
class AutoClosedAction {
88+
89+
private readonly _model: ITextModel;
90+
91+
private _autoClosedCharactersDecorations: string[];
92+
private _autoClosedEnclosingDecorations: string[];
93+
94+
constructor(model: ITextModel, autoClosedCharactersDecorations: string[], autoClosedEnclosingDecorations: string[]) {
95+
this._model = model;
96+
this._autoClosedCharactersDecorations = autoClosedCharactersDecorations;
97+
this._autoClosedEnclosingDecorations = autoClosedEnclosingDecorations;
98+
}
99+
100+
public dispose(): void {
101+
this._autoClosedCharactersDecorations = this._model.deltaDecorations(this._autoClosedCharactersDecorations, []);
102+
this._autoClosedEnclosingDecorations = this._model.deltaDecorations(this._autoClosedEnclosingDecorations, []);
103+
}
104+
105+
public getAutoClosedCharactersRanges(): Range[] {
106+
let result: Range[] = [];
107+
for (let i = 0; i < this._autoClosedCharactersDecorations.length; i++) {
108+
const decorationRange = this._model.getDecorationRange(this._autoClosedCharactersDecorations[i]);
109+
if (decorationRange) {
110+
result.push(decorationRange);
111+
}
112+
}
113+
return result;
114+
}
115+
116+
public isValid(selections: Range[]): boolean {
117+
let enclosingRanges: Range[] = [];
118+
for (let i = 0; i < this._autoClosedEnclosingDecorations.length; i++) {
119+
const decorationRange = this._model.getDecorationRange(this._autoClosedEnclosingDecorations[i]);
120+
if (decorationRange) {
121+
enclosingRanges.push(decorationRange);
122+
if (decorationRange.startLineNumber !== decorationRange.endLineNumber) {
123+
// Stop tracking if the range becomes multiline...
124+
return false;
125+
}
126+
}
127+
}
128+
enclosingRanges.sort(Range.compareRangesUsingStarts);
129+
130+
selections.sort(Range.compareRangesUsingStarts);
131+
132+
for (let i = 0; i < selections.length; i++) {
133+
if (i >= enclosingRanges.length) {
134+
return false;
135+
}
136+
if (!enclosingRanges[i].strictContainsRange(selections[i])) {
137+
return false;
138+
}
139+
}
140+
141+
return true;
142+
}
143+
}
144+
86145
export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
87146

88147
public static MAX_CURSOR_COUNT = 10000;
@@ -106,6 +165,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
106165
private _isHandling: boolean;
107166
private _isDoingComposition: boolean;
108167
private _columnSelectData: IColumnSelectData | null;
168+
private _autoClosedActions: AutoClosedAction[];
109169
private _prevEditOperationType: EditOperationType;
110170

111171
constructor(configuration: editorCommon.IConfiguration, model: ITextModel, viewModel: IViewModel) {
@@ -120,6 +180,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
120180
this._isHandling = false;
121181
this._isDoingComposition = false;
122182
this._columnSelectData = null;
183+
this._autoClosedActions = [];
123184
this._prevEditOperationType = EditOperationType.Other;
124185

125186
this._register(this._model.onDidChangeRawContent((e) => {
@@ -173,9 +234,24 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
173234

174235
public dispose(): void {
175236
this._cursors.dispose();
237+
this._autoClosedActions = dispose(this._autoClosedActions);
176238
super.dispose();
177239
}
178240

241+
private _validateAutoClosedActions(): void {
242+
if (this._autoClosedActions.length > 0) {
243+
let selections: Range[] = this._cursors.getSelections();
244+
for (let i = 0; i < this._autoClosedActions.length; i++) {
245+
const autoClosedAction = this._autoClosedActions[i];
246+
if (!autoClosedAction.isValid(selections)) {
247+
autoClosedAction.dispose();
248+
this._autoClosedActions.splice(i, 1);
249+
i--;
250+
}
251+
}
252+
}
253+
}
254+
179255
// ------ some getters/setters
180256

181257
public getPrimaryCursor(): CursorState {
@@ -202,6 +278,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
202278
this._cursors.normalize();
203279
this._columnSelectData = null;
204280

281+
this._validateAutoClosedActions();
282+
205283
this._emitStateChangedIfNecessary(source, reason, oldState);
206284
}
207285

@@ -296,7 +374,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
296374
// a model.setValue() was called
297375
this._cursors.dispose();
298376
this._cursors = new CursorCollection(this.context);
299-
377+
this._validateAutoClosedActions();
300378
this._emitStateChangedIfNecessary('model', CursorChangeReason.ContentFlush, null);
301379
} else {
302380
const selectionsFromMarkers = this._cursors.readSelectionFromMarkers();
@@ -367,6 +445,35 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
367445
// The commands were applied correctly
368446
this._interpretCommandResult(result);
369447

448+
// Check for auto-closing closed characters
449+
let autoClosedCharactersRanges: IModelDeltaDecoration[] = [];
450+
let autoClosedEnclosingRanges: IModelDeltaDecoration[] = [];
451+
452+
for (let i = 0; i < opResult.commands.length; i++) {
453+
const command = opResult.commands[i];
454+
if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) {
455+
autoClosedCharactersRanges.push({
456+
range: command.closeCharacterRange,
457+
options: {
458+
inlineClassName: 'auto-closed-character',
459+
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
460+
}
461+
});
462+
autoClosedEnclosingRanges.push({
463+
range: command.enclosingRange,
464+
options: {
465+
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
466+
}
467+
});
468+
}
469+
}
470+
471+
if (autoClosedCharactersRanges.length > 0) {
472+
const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersRanges);
473+
const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingRanges);
474+
this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations));
475+
}
476+
370477
this._prevEditOperationType = opResult.type;
371478
}
372479

@@ -540,6 +647,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
540647
this._cursors.startTrackingSelections();
541648
}
542649

650+
this._validateAutoClosedActions();
651+
543652
if (this._emitStateChangedIfNecessary(source, cursorChangeReason, oldState)) {
544653
this._revealRange(RevealTarget.Primary, viewEvents.VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth);
545654
}
@@ -566,8 +675,15 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
566675
chr = text.charAt(i);
567676
}
568677

678+
let autoClosedCharacters: Range[] = [];
679+
if (this._autoClosedActions.length > 0) {
680+
for (let i = 0, len = this._autoClosedActions.length; i < len; i++) {
681+
autoClosedCharacters = autoClosedCharacters.concat(this._autoClosedActions[i].getAutoClosedCharactersRanges());
682+
}
683+
}
684+
569685
// Here we must interpret each typed character individually, that's why we create a new context
570-
this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), chr));
686+
this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr));
571687
}
572688

573689
} else {

src/vs/editor/common/controller/cursorTypeOperations.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { CursorColumns, CursorConfiguration, EditOperationResult, EditOperationT
1313
import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier';
1414
import { Range } from 'vs/editor/common/core/range';
1515
import { Selection } from 'vs/editor/common/core/selection';
16-
import { ICommand } from 'vs/editor/common/editorCommon';
16+
import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon';
1717
import { ITextModel } from 'vs/editor/common/model';
1818
import { EnterAction, IndentAction } from 'vs/editor/common/modes/languageConfiguration';
1919
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
@@ -430,7 +430,7 @@ export class TypeOperations {
430430
return null;
431431
}
432432

433-
private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean {
433+
private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean {
434434
const autoCloseConfig = isQuote(ch) ? config.autoClosingQuotes : config.autoClosingBrackets;
435435

436436
if (autoCloseConfig === 'never' || !config.autoClosingPairsClose.hasOwnProperty(ch)) {
@@ -461,6 +461,19 @@ export class TypeOperations {
461461
return false;
462462
}
463463
}
464+
465+
// Must over-type a closing character typed by the editor
466+
let found = false;
467+
for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) {
468+
const autoClosedCharacter = autoClosedCharacters[j];
469+
if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) {
470+
found = true;
471+
break;
472+
}
473+
}
474+
if (!found) {
475+
return false;
476+
}
464477
}
465478

466479
return true;
@@ -573,7 +586,7 @@ export class TypeOperations {
573586
for (let i = 0, len = selections.length; i < len; i++) {
574587
const selection = selections[i];
575588
const closeCharacter = config.autoClosingPairsOpen[ch];
576-
commands[i] = new ReplaceCommandWithOffsetCursorState(selection, ch + closeCharacter, 0, -closeCharacter.length);
589+
commands[i] = new TypeWithAutoClosingCommand(selection, ch, closeCharacter);
577590
}
578591
return new EditOperationResult(EditOperationType.Typing, commands, {
579592
shouldPushStackElementBefore: true,
@@ -802,7 +815,7 @@ export class TypeOperations {
802815
});
803816
}
804817

805-
public static typeWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult {
818+
public static typeWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult {
806819

807820
if (ch === '\n') {
808821
let commands: ICommand[] = [];
@@ -833,7 +846,7 @@ export class TypeOperations {
833846
}
834847
}
835848

836-
if (this._isAutoClosingCloseCharType(config, model, selections, ch)) {
849+
if (this._isAutoClosingCloseCharType(config, model, selections, autoClosedCharacters, ch)) {
837850
return this._runAutoClosingCloseCharType(prevEditOperationType, config, model, selections, ch);
838851
}
839852

@@ -923,3 +936,24 @@ export class TypeOperations {
923936
return commands;
924937
}
925938
}
939+
940+
export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState {
941+
942+
private _closeCharacter: string;
943+
public closeCharacterRange: Range | null;
944+
public enclosingRange: Range | null;
945+
946+
constructor(selection: Selection, openCharacter: string, closeCharacter: string) {
947+
super(selection, openCharacter + closeCharacter, 0, -closeCharacter.length);
948+
this._closeCharacter = closeCharacter;
949+
this.closeCharacterRange = null;
950+
}
951+
952+
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
953+
let inverseEditOperations = helper.getInverseEditOperations();
954+
let range = inverseEditOperations[0].range;
955+
this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn);
956+
this.enclosingRange = range;
957+
return super.computeCursorState(model, helper);
958+
}
959+
}

src/vs/editor/common/core/range.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,32 @@ export class Range {
126126
return true;
127127
}
128128

129+
/**
130+
* Test if `range` is strictly in this range. `range` must start after and end before this range for the result to be true.
131+
*/
132+
public strictContainsRange(range: IRange): boolean {
133+
return Range.strictContainsRange(this, range);
134+
}
135+
136+
/**
137+
* Test if `otherRange` is strinctly in `range` (must start after, and end before). If the ranges are equal, will return false.
138+
*/
139+
public static strictContainsRange(range: IRange, otherRange: IRange): boolean {
140+
if (otherRange.startLineNumber < range.startLineNumber || otherRange.endLineNumber < range.startLineNumber) {
141+
return false;
142+
}
143+
if (otherRange.startLineNumber > range.endLineNumber || otherRange.endLineNumber > range.endLineNumber) {
144+
return false;
145+
}
146+
if (otherRange.startLineNumber === range.startLineNumber && otherRange.startColumn <= range.startColumn) {
147+
return false;
148+
}
149+
if (otherRange.endLineNumber === range.endLineNumber && otherRange.endColumn >= range.endColumn) {
150+
return false;
151+
}
152+
return true;
153+
}
154+
129155
/**
130156
* A reunion of the two ranges.
131157
* The smallest position will be used as the start point, and the largest one as the end point.

0 commit comments

Comments
 (0)