Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding overtype mode #233188

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
69e6bff
adding code in order to enable overtype
aiday-mar Nov 6, 2024
5b951f7
adding a setting to determine the cursor style in overtype mode
aiday-mar Nov 6, 2024
b17acbe
adding action to toggle between overtype and insert mode
aiday-mar Nov 6, 2024
1fd450b
adding a contribution which adds a statu bar icon
aiday-mar Nov 6, 2024
c3b3496
adding same as tab focus mode
aiday-mar Nov 11, 2024
f15cac5
polishing
aiday-mar Nov 11, 2024
e8fd730
polish
aiday-mar Nov 12, 2024
0d07ee0
updating by clicking on the status bar
aiday-mar Nov 12, 2024
01de87e
polish
aiday-mar Nov 12, 2024
5048d07
changing the name of the status bar
aiday-mar Nov 12, 2024
4a2b843
add some keybindings and make status bar less prominent
aiday-mar Nov 12, 2024
779035d
polishing the replacement of text
aiday-mar Nov 12, 2024
900af97
adding overtype on paste
aiday-mar Nov 12, 2024
0949847
adding pasting support
aiday-mar Nov 12, 2024
0968f6d
using paste on distributed cursors
aiday-mar Nov 12, 2024
fb0434c
fix ReplaceOvertypeCommandWithOffsetCursorState
aiday-mar Nov 12, 2024
5706284
add code
aiday-mar Nov 13, 2024
44c77db
adding support for composition
aiday-mar Nov 13, 2024
5f03a29
undoing edit context changes
aiday-mar Nov 13, 2024
669ecc1
changing the status bar entry so it disappears when moving to insert …
aiday-mar Nov 13, 2024
d848314
rerender the cursors
aiday-mar Nov 13, 2024
164c67d
allowing composition over several lines
aiday-mar Nov 13, 2024
6a6e9ae
removing log
aiday-mar Nov 13, 2024
24f1dab
polishing the code
aiday-mar Nov 13, 2024
6a5daee
adding tests for overtype mode
aiday-mar Nov 13, 2024
d78453c
reset back the input mode to insert from ovetype on suite teardown
aiday-mar Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/vs/base/common/inputMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Emitter, Event } from './event.js';

class InputModeImpl {

private _inputMode: 'overtype' | 'insert' = 'insert';
private readonly _onDidChangeInputMode = new Emitter<'overtype' | 'insert'>();
public readonly onDidChangeInputMode: Event<'overtype' | 'insert'> = this._onDidChangeInputMode.event;

public getInputMode(): 'overtype' | 'insert' {
return this._inputMode;
}

public setInputMode(inputMode: 'overtype' | 'insert'): void {
this._inputMode = inputMode;
this._onDidChangeInputMode.fire(this._inputMode);
}
}

/**
* Controls the type mode, whether insert or overtype
*/
export const InputMode = new InputModeImpl();
12 changes: 10 additions & 2 deletions src/vs/editor/browser/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { OverviewRuler } from './viewParts/overviewRuler/overviewRuler.js';
import { Rulers } from './viewParts/rulers/rulers.js';
import { ScrollDecorationViewPart } from './viewParts/scrollDecoration/scrollDecoration.js';
import { SelectionsOverlay } from './viewParts/selections/selections.js';
import { ViewCursors } from './viewParts/viewCursors/viewCursors.js';
import { IViewCursorsHelper, ViewCursors } from './viewParts/viewCursors/viewCursors.js';
import { ViewZones } from './viewParts/viewZones/viewZones.js';
import { WhitespaceOverlay } from './viewParts/whitespace/whitespace.js';
import { IEditorConfiguration } from '../common/config/editorConfiguration.js';
Expand Down Expand Up @@ -209,7 +209,7 @@ export class View extends ViewEventHandler {
this._contentWidgets = new ViewContentWidgets(this._context, this.domNode);
this._viewParts.push(this._contentWidgets);

this._viewCursors = new ViewCursors(this._context);
this._viewCursors = new ViewCursors(this._context, this._createViewCursorsHelper());
this._viewParts.push(this._viewCursors);

// Overlay widgets
Expand Down Expand Up @@ -369,6 +369,14 @@ export class View extends ViewEventHandler {
};
}

private _createViewCursorsHelper(): IViewCursorsHelper {
return {
renderNow: (): void => {
this.render(true, false);
}
};
}

private _createTextAreaHandlerHelper(): IVisibleRangeProvider {
return {
visibleRangeForPosition: (position: Position) => {
Expand Down
27 changes: 22 additions & 5 deletions src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import * as dom from '../../../../base/browser/dom.js';
import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import * as strings from '../../../../base/common/strings.js';
import { applyFontInfo } from '../../config/domFontInfo.js';
import { TextEditorCursorStyle, EditorOption } from '../../../common/config/editorOptions.js';
import { TextEditorCursorStyle, EditorOption, IComputedEditorOptions } from '../../../common/config/editorOptions.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';
import { ViewContext } from '../../../common/viewModel/viewContext.js';
import * as viewEvents from '../../../common/viewEvents.js';
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';
import { InputMode } from '../../../../base/common/inputMode.js';
import { Disposable } from '../../../../base/common/lifecycle.js';

export interface IViewCursorRenderData {
domNode: HTMLElement;
Expand Down Expand Up @@ -41,11 +43,11 @@ export enum CursorPlurality {
MultiSecondary,
}

export class ViewCursor {
export class ViewCursor extends Disposable {
private readonly _context: ViewContext;
private readonly _domNode: FastDomNode<HTMLElement>;

private _cursorStyle: TextEditorCursorStyle;
private _cursorStyle!: TextEditorCursorStyle;
private _lineCursorWidth: number;
private _lineHeight: number;
private _typicalHalfwidthCharacterWidth: number;
Expand All @@ -59,11 +61,11 @@ export class ViewCursor {
private _renderData: ViewCursorRenderData | null;

constructor(context: ViewContext, plurality: CursorPlurality) {
super();
this._context = context;
const options = this._context.configuration.options;
const fontInfo = options.get(EditorOption.fontInfo);

this._cursorStyle = options.get(EditorOption.cursorStyle);
this._lineHeight = options.get(EditorOption.lineHeight);
this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth);
Expand All @@ -85,6 +87,11 @@ export class ViewCursor {

this._lastRenderedContent = '';
this._renderData = null;

this._updateCursorStyle();
this._register(InputMode.onDidChangeInputMode(() => {
this._updateCursorStyle();
}));
}

public getDomNode(): FastDomNode<HTMLElement> {
Expand Down Expand Up @@ -130,7 +137,7 @@ export class ViewCursor {
const options = this._context.configuration.options;
const fontInfo = options.get(EditorOption.fontInfo);

this._cursorStyle = options.get(EditorOption.cursorStyle);
this._updateCursorStyle();
this._lineHeight = options.get(EditorOption.lineHeight);
this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
this._lineCursorWidth = Math.min(options.get(EditorOption.cursorWidth), this._typicalHalfwidthCharacterWidth);
Expand Down Expand Up @@ -234,6 +241,10 @@ export class ViewCursor {
return new ViewCursorRenderData(top, range.left, 0, width, height, textContent, textContentClassName);
}

private _updateCursorStyle(): void {
this._cursorStyle = getCursorStyle(this._context.configuration.options);
}

private _getTokenClassName(position: Position): string {
const lineData = this._context.viewModel.getViewLineData(position.lineNumber);
const tokenIndex = lineData.tokens.findTokenIndexAtOffset(position.column - 1);
Expand Down Expand Up @@ -274,3 +285,9 @@ export class ViewCursor {
};
}
}

export function getCursorStyle(options: IComputedEditorOptions) {
return InputMode.getInputMode() === 'overtype' ?
options.get(EditorOption.overtypeCursorStyle) :
options.get(EditorOption.cursorStyle);
}
32 changes: 25 additions & 7 deletions src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import './viewCursors.css';
import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import { IntervalTimer, TimeoutTimer } from '../../../../base/common/async.js';
import { ViewPart } from '../../view/viewPart.js';
import { IViewCursorRenderData, ViewCursor, CursorPlurality } from './viewCursor.js';
import { IViewCursorRenderData, ViewCursor, CursorPlurality, getCursorStyle } from './viewCursor.js';
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle, EditorOption } from '../../../common/config/editorOptions.js';
import { Position } from '../../../common/core/position.js';
import {
Expand All @@ -22,6 +22,11 @@ import { registerThemingParticipant } from '../../../../platform/theme/common/th
import { isHighContrast } from '../../../../platform/theme/common/theme.js';
import { CursorChangeReason } from '../../../common/cursorEvents.js';
import { WindowIntervalTimer, getWindow } from '../../../../base/browser/dom.js';
import { InputMode } from '../../../../base/common/inputMode.js';

export interface IViewCursorsHelper {
renderNow(): void;
}

/**
* View cursors is a view part responsible for rendering the primary cursor and
Expand All @@ -33,7 +38,7 @@ export class ViewCursors extends ViewPart {

private _readOnly: boolean;
private _cursorBlinking: TextEditorCursorBlinkingStyle;
private _cursorStyle: TextEditorCursorStyle;
private _cursorStyle!: TextEditorCursorStyle;
private _cursorSmoothCaretAnimation: 'off' | 'explicit' | 'on';
private _experimentalEditContextEnabled: boolean;
private _selectionIsEmpty: boolean;
Expand All @@ -52,22 +57,23 @@ export class ViewCursors extends ViewPart {
private readonly _primaryCursor: ViewCursor;
private readonly _secondaryCursors: ViewCursor[];
private _renderData: IViewCursorRenderData[];
private _viewHelper: IViewCursorsHelper;

constructor(context: ViewContext) {
constructor(context: ViewContext, viewHelper: IViewCursorsHelper) {
super(context);

const options = this._context.configuration.options;
this._viewHelper = viewHelper;
this._readOnly = options.get(EditorOption.readOnly);
this._cursorBlinking = options.get(EditorOption.cursorBlinking);
this._cursorStyle = options.get(EditorOption.cursorStyle);
this._cursorSmoothCaretAnimation = options.get(EditorOption.cursorSmoothCaretAnimation);
this._experimentalEditContextEnabled = options.get(EditorOption.experimentalEditContextEnabled);
this._selectionIsEmpty = true;
this._isComposingInput = false;

this._isVisible = false;

this._primaryCursor = new ViewCursor(this._context, CursorPlurality.Single);
this._primaryCursor = this._register(new ViewCursor(this._context, CursorPlurality.Single));
this._secondaryCursors = [];
this._renderData = [];

Expand All @@ -85,12 +91,17 @@ export class ViewCursors extends ViewPart {

this._editorHasFocus = false;
this._updateBlinking();
this._updateCursorStyle();
this._register(InputMode.onDidChangeInputMode(() => {
this._updateCursorStyle();
}));
}

public override dispose(): void {
super.dispose();
this._startCursorBlinkAnimation.dispose();
this._cursorFlatBlinkInterval.dispose();
this._secondaryCursors.forEach((cursor) => cursor.dispose());
}

public getDomNode(): FastDomNode<HTMLElement> {
Expand All @@ -114,12 +125,12 @@ export class ViewCursors extends ViewPart {

this._readOnly = options.get(EditorOption.readOnly);
this._cursorBlinking = options.get(EditorOption.cursorBlinking);
this._cursorStyle = options.get(EditorOption.cursorStyle);
this._cursorSmoothCaretAnimation = options.get(EditorOption.cursorSmoothCaretAnimation);
this._experimentalEditContextEnabled = options.get(EditorOption.experimentalEditContextEnabled);

this._updateBlinking();
this._updateDomClassName();
this._updateCursorStyle();

this._primaryCursor.onConfigurationChanged(e);
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
Expand Down Expand Up @@ -149,7 +160,8 @@ export class ViewCursors extends ViewPart {
const removeCnt = this._secondaryCursors.length - secondaryPositions.length;
for (let i = 0; i < removeCnt; i++) {
this._domNode.removeChild(this._secondaryCursors[0].getDomNode());
this._secondaryCursors.splice(0, 1);
const cursors = this._secondaryCursors.splice(0, 1);
cursors.forEach((cursor) => cursor.dispose());
}
}

Expand Down Expand Up @@ -339,6 +351,12 @@ export class ViewCursors extends ViewPart {
return result;
}

private _updateCursorStyle(): void {
this._cursorStyle = getCursorStyle(this._context.configuration.options);
this.forceShouldRender();
this._viewHelper.renderNow();
}

private _show(): void {
this._primaryCursor.show();
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
Expand Down
97 changes: 96 additions & 1 deletion src/vs/editor/common/commands/replaceCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Position } from '../core/position.js';
import { Range } from '../core/range.js';
import { Selection, SelectionDirection } from '../core/selection.js';
import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../editorCommon.js';
import { ITextModel } from '../model.js';
import { EndOfLineSequence, ITextModel } from '../model.js';

export class ReplaceCommand implements ICommand {

Expand All @@ -31,6 +32,73 @@ export class ReplaceCommand implements ICommand {
}
}

export class ReplaceOvertypeCommand implements ICommand {

private readonly _range: Range;
private readonly _text: string;
public readonly insertsAutoWhitespace: boolean;

constructor(range: Range, text: string, insertsAutoWhitespace: boolean = false) {
this._range = range;
this._text = text;
this.insertsAutoWhitespace = insertsAutoWhitespace;
}

public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
const startPosition = this._range.getStartPosition();
const endPosition = this._range.getEndPosition();
const rangeEndOffset = model.getOffsetAt(endPosition);
const endOffset = rangeEndOffset + this._text.length + (this._range.isEmpty() ? 0 : - 1);
const endOfLine = model.getEndOfLineSequence() === EndOfLineSequence.CRLF ? '\r\n' : '\n';
const lastCharacterRange = Range.fromPositions(model.getPositionAt(endOffset - 1), model.getPositionAt(endOffset));
const lastCharacter = model.getValueInRange(lastCharacterRange);
const newEndOffset = lastCharacter === endOfLine ? endOffset - 1 : endOffset;
const replaceRange = Range.fromPositions(startPosition, model.getPositionAt(newEndOffset));
builder.addTrackedEditOperation(replaceRange, this._text);
}

public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
const inverseEditOperations = helper.getInverseEditOperations();
const srcRange = inverseEditOperations[0].range;
return Selection.fromPositions(srcRange.getEndPosition());
}
}

export class OvertypePasteCommand implements ICommand {

private readonly _range: Range;
private readonly _text: string;
public readonly insertsAutoWhitespace: boolean;

constructor(range: Range, text: string, insertsAutoWhitespace: boolean = false) {
this._range = range;
this._text = text;
this.insertsAutoWhitespace = insertsAutoWhitespace;
}

public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
const startPosition = this._range.getStartPosition();
const endPosition = this._range.getEndPosition();
const endLineNumber = endPosition.lineNumber;
const potentialEndOffset = model.getOffsetAt(endPosition) + this._text.length + (this._range.isEmpty() ? 0 : - 1);
const potentialEndPosition = model.getPositionAt(potentialEndOffset);
let newEndPosition: Position;
if (potentialEndPosition.lineNumber > endLineNumber) {
newEndPosition = new Position(endLineNumber, model.getLineMaxColumn(endLineNumber));
} else {
newEndPosition = potentialEndPosition;
}
const replaceRange = Range.fromPositions(startPosition, newEndPosition);
builder.addTrackedEditOperation(replaceRange, this._text);
}

public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
const inverseEditOperations = helper.getInverseEditOperations();
const srcRange = inverseEditOperations[0].range;
return Selection.fromPositions(srcRange.getEndPosition());
}
}

export class ReplaceCommandThatSelectsText implements ICommand {

private readonly _range: Range;
Expand Down Expand Up @@ -102,6 +170,33 @@ export class ReplaceCommandWithOffsetCursorState implements ICommand {
}
}

export class ReplaceOvertypeCommandInComposition implements ICommand {

private readonly _range: Range;

constructor(range: Range) {
this._range = range;
}

public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
const text = model.getValueInRange(this._range);
const startPosition = this._range.getStartPosition();
const endOffset = model.getOffsetAt(startPosition) + 2 * text.length;
let endPosition = model.getPositionAt(endOffset);
if (endPosition.lineNumber > this._range.endLineNumber) {
endPosition = new Position(this._range.endLineNumber, model.getLineMaxColumn(this._range.endLineNumber));
}
const replaceRange = Range.fromPositions(startPosition, endPosition);
builder.addTrackedEditOperation(replaceRange, text);
}

public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
const inverseEditOperations = helper.getInverseEditOperations();
const srcRange = inverseEditOperations[0].range;
return Selection.fromPositions(srcRange.getEndPosition());
}
}

export class ReplaceCommandThatPreservesSelection implements ICommand {

private readonly _range: Range;
Expand Down
Loading
Loading