mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 01:25:38 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
1792
src/vs/editor/common/controller/coreCommands.ts
Normal file
1792
src/vs/editor/common/controller/coreCommands.ts
Normal file
File diff suppressed because it is too large
Load Diff
861
src/vs/editor/common/controller/cursor.ts
Normal file
861
src/vs/editor/common/controller/cursor.ts
Normal file
@@ -0,0 +1,861 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { CursorCollection } from 'vs/editor/common/controller/cursorCollection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection, SelectionDirection, ISelection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { CursorColumns, CursorConfiguration, EditOperationResult, CursorContext, CursorState, RevealTarget, IColumnSelectData, ICursors } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations';
|
||||
import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations';
|
||||
import { TextModelEventType, ModelRawContentChangedEvent, RawContentChangedType } from 'vs/editor/common/model/textModelEvents';
|
||||
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
// import { ScreenReaderMessageGenerator } from "vs/editor/common/controller/accGenerator";
|
||||
|
||||
function containsLineMappingChanged(events: viewEvents.ViewEvent[]): boolean {
|
||||
for (let i = 0, len = events.length; i < len; i++) {
|
||||
if (events[i].type === viewEvents.ViewEventType.ViewLineMappingChanged) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export class CursorStateChangedEvent {
|
||||
/**
|
||||
* The new selections.
|
||||
* The primary selection is always at index 0.
|
||||
*/
|
||||
readonly selections: Selection[];
|
||||
/**
|
||||
* Source of the call that caused the event.
|
||||
*/
|
||||
readonly source: string;
|
||||
/**
|
||||
* Reason.
|
||||
*/
|
||||
readonly reason: CursorChangeReason;
|
||||
|
||||
constructor(selections: Selection[], source: string, reason: CursorChangeReason) {
|
||||
this.selections = selections;
|
||||
this.source = source;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of the cursor and the model state
|
||||
*/
|
||||
export class CursorModelState {
|
||||
|
||||
public readonly modelVersionId: number;
|
||||
public readonly cursorState: CursorState[];
|
||||
|
||||
constructor(model: editorCommon.IModel, cursor: Cursor) {
|
||||
this.modelVersionId = model.getVersionId();
|
||||
this.cursorState = cursor.getAll();
|
||||
}
|
||||
|
||||
public equals(other: CursorModelState): boolean {
|
||||
if (!other) {
|
||||
return false;
|
||||
}
|
||||
if (this.modelVersionId !== other.modelVersionId) {
|
||||
return false;
|
||||
}
|
||||
if (this.cursorState.length !== other.cursorState.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0, len = this.cursorState.length; i < len; i++) {
|
||||
if (!this.cursorState[i].equals(other.cursorState[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
|
||||
|
||||
private readonly _onDidChange: Emitter<CursorStateChangedEvent> = this._register(new Emitter<CursorStateChangedEvent>());
|
||||
public readonly onDidChange: Event<CursorStateChangedEvent> = this._onDidChange.event;
|
||||
|
||||
private readonly _configuration: editorCommon.IConfiguration;
|
||||
private readonly _model: editorCommon.IModel;
|
||||
private readonly _viewModel: IViewModel;
|
||||
public context: CursorContext;
|
||||
private _cursors: CursorCollection;
|
||||
|
||||
private _isHandling: boolean;
|
||||
private _isDoingComposition: boolean;
|
||||
private _columnSelectData: IColumnSelectData;
|
||||
|
||||
constructor(configuration: editorCommon.IConfiguration, model: editorCommon.IModel, viewModel: IViewModel) {
|
||||
super();
|
||||
this._configuration = configuration;
|
||||
this._model = model;
|
||||
this._viewModel = viewModel;
|
||||
this.context = new CursorContext(this._configuration, this._model, this._viewModel);
|
||||
this._cursors = new CursorCollection(this.context);
|
||||
|
||||
this._isHandling = false;
|
||||
this._isDoingComposition = false;
|
||||
this._columnSelectData = null;
|
||||
|
||||
this._register(this._model.addBulkListener((events) => {
|
||||
if (this._isHandling) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hadContentChange = false;
|
||||
let hadFlushEvent = false;
|
||||
for (let i = 0, len = events.length; i < len; i++) {
|
||||
const event = events[i];
|
||||
const eventType = event.type;
|
||||
|
||||
if (eventType === TextModelEventType.ModelRawContentChanged2) {
|
||||
hadContentChange = true;
|
||||
const rawChangeEvent = <ModelRawContentChangedEvent>event.data;
|
||||
hadFlushEvent = hadFlushEvent || rawChangeEvent.containsEvent(RawContentChangedType.Flush);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hadContentChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._onModelContentChanged(hadFlushEvent);
|
||||
}));
|
||||
|
||||
this._register(viewModel.addEventListener((events: viewEvents.ViewEvent[]) => {
|
||||
if (!containsLineMappingChanged(events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure valid state
|
||||
this.setStates('viewModel', CursorChangeReason.NotSet, this.getAll());
|
||||
}));
|
||||
|
||||
const updateCursorContext = () => {
|
||||
this.context = new CursorContext(this._configuration, this._model, this._viewModel);
|
||||
this._cursors.updateContext(this.context);
|
||||
};
|
||||
this._register(this._model.onDidChangeLanguage((e) => {
|
||||
updateCursorContext();
|
||||
}));
|
||||
this._register(LanguageConfigurationRegistry.onDidChange(() => {
|
||||
// TODO@Alex: react only if certain supports changed? (and if my model's mode changed)
|
||||
updateCursorContext();
|
||||
}));
|
||||
this._register(model.onDidChangeOptions(() => {
|
||||
updateCursorContext();
|
||||
}));
|
||||
this._register(this._configuration.onDidChange((e) => {
|
||||
if (CursorConfiguration.shouldRecreate(e)) {
|
||||
updateCursorContext();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._cursors.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ------ some getters/setters
|
||||
|
||||
public getPrimaryCursor(): CursorState {
|
||||
return this._cursors.getPrimaryCursor();
|
||||
}
|
||||
|
||||
public getLastAddedCursorIndex(): number {
|
||||
return this._cursors.getLastAddedCursorIndex();
|
||||
}
|
||||
|
||||
public getAll(): CursorState[] {
|
||||
return this._cursors.getAll();
|
||||
}
|
||||
|
||||
public setStates(source: string, reason: CursorChangeReason, states: CursorState[]): void {
|
||||
const oldState = new CursorModelState(this._model, this);
|
||||
|
||||
this._cursors.setStates(states);
|
||||
this._cursors.normalize();
|
||||
this._columnSelectData = null;
|
||||
|
||||
this._emitStateChangedIfNecessary(source, reason, oldState);
|
||||
}
|
||||
|
||||
public setColumnSelectData(columnSelectData: IColumnSelectData): void {
|
||||
this._columnSelectData = columnSelectData;
|
||||
}
|
||||
|
||||
public reveal(horizontal: boolean, target: RevealTarget, scrollType: editorCommon.ScrollType): void {
|
||||
this._revealRange(target, viewEvents.VerticalRevealType.Simple, horizontal, scrollType);
|
||||
}
|
||||
|
||||
public revealRange(revealHorizontal: boolean, viewRange: Range, verticalType: viewEvents.VerticalRevealType, scrollType: editorCommon.ScrollType) {
|
||||
this.emitCursorRevealRange(viewRange, verticalType, revealHorizontal, scrollType);
|
||||
}
|
||||
|
||||
public scrollTo(desiredScrollTop: number): void {
|
||||
this._viewModel.viewLayout.setScrollPositionSmooth({
|
||||
scrollTop: desiredScrollTop
|
||||
});
|
||||
}
|
||||
|
||||
public saveState(): editorCommon.ICursorState[] {
|
||||
|
||||
let result: editorCommon.ICursorState[] = [];
|
||||
|
||||
const selections = this._cursors.getSelections();
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
result.push({
|
||||
inSelectionMode: !selection.isEmpty(),
|
||||
selectionStart: {
|
||||
lineNumber: selection.selectionStartLineNumber,
|
||||
column: selection.selectionStartColumn,
|
||||
},
|
||||
position: {
|
||||
lineNumber: selection.positionLineNumber,
|
||||
column: selection.positionColumn,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public restoreState(states: editorCommon.ICursorState[]): void {
|
||||
|
||||
let desiredSelections: ISelection[] = [];
|
||||
|
||||
for (let i = 0, len = states.length; i < len; i++) {
|
||||
const state = states[i];
|
||||
|
||||
let positionLineNumber = 1;
|
||||
let positionColumn = 1;
|
||||
|
||||
// Avoid missing properties on the literal
|
||||
if (state.position && state.position.lineNumber) {
|
||||
positionLineNumber = state.position.lineNumber;
|
||||
}
|
||||
if (state.position && state.position.column) {
|
||||
positionColumn = state.position.column;
|
||||
}
|
||||
|
||||
let selectionStartLineNumber = positionLineNumber;
|
||||
let selectionStartColumn = positionColumn;
|
||||
|
||||
// Avoid missing properties on the literal
|
||||
if (state.selectionStart && state.selectionStart.lineNumber) {
|
||||
selectionStartLineNumber = state.selectionStart.lineNumber;
|
||||
}
|
||||
if (state.selectionStart && state.selectionStart.column) {
|
||||
selectionStartColumn = state.selectionStart.column;
|
||||
}
|
||||
|
||||
desiredSelections.push({
|
||||
selectionStartLineNumber: selectionStartLineNumber,
|
||||
selectionStartColumn: selectionStartColumn,
|
||||
positionLineNumber: positionLineNumber,
|
||||
positionColumn: positionColumn
|
||||
});
|
||||
}
|
||||
|
||||
this.setStates('restoreState', CursorChangeReason.NotSet, CursorState.fromModelSelections(desiredSelections));
|
||||
this.reveal(true, RevealTarget.Primary, editorCommon.ScrollType.Immediate);
|
||||
}
|
||||
|
||||
private _onModelContentChanged(hadFlushEvent: boolean): void {
|
||||
if (hadFlushEvent) {
|
||||
// a model.setValue() was called
|
||||
this._cursors.dispose();
|
||||
this._cursors = new CursorCollection(this.context);
|
||||
|
||||
this._emitStateChangedIfNecessary('model', CursorChangeReason.ContentFlush, null);
|
||||
} else {
|
||||
const selectionsFromMarkers = this._cursors.readSelectionFromMarkers();
|
||||
this.setStates('modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers));
|
||||
}
|
||||
}
|
||||
|
||||
public getSelection(): Selection {
|
||||
return this._cursors.getPrimaryCursor().modelState.selection;
|
||||
}
|
||||
|
||||
public getColumnSelectData(): IColumnSelectData {
|
||||
if (this._columnSelectData) {
|
||||
return this._columnSelectData;
|
||||
}
|
||||
const primaryCursor = this._cursors.getPrimaryCursor();
|
||||
const primaryPos = primaryCursor.viewState.position;
|
||||
return {
|
||||
toViewLineNumber: primaryPos.lineNumber,
|
||||
toViewVisualColumn: CursorColumns.visibleColumnFromColumn2(this.context.config, this.context.viewModel, primaryPos)
|
||||
};
|
||||
}
|
||||
|
||||
public getSelections(): Selection[] {
|
||||
return this._cursors.getSelections();
|
||||
}
|
||||
|
||||
public getViewSelections(): Selection[] {
|
||||
return this._cursors.getViewSelections();
|
||||
}
|
||||
|
||||
public getPosition(): Position {
|
||||
return this._cursors.getPrimaryCursor().modelState.position;
|
||||
}
|
||||
|
||||
public setSelections(source: string, selections: ISelection[]): void {
|
||||
this.setStates(source, CursorChangeReason.NotSet, CursorState.fromModelSelections(selections));
|
||||
}
|
||||
|
||||
// ------ auxiliary handling logic
|
||||
|
||||
private _executeEditOperation(opResult: EditOperationResult): void {
|
||||
|
||||
if (!opResult) {
|
||||
// Nothing to execute
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._configuration.editor.readOnly) {
|
||||
// Cannot execute when read only
|
||||
return;
|
||||
}
|
||||
|
||||
if (opResult.shouldPushStackElementBefore) {
|
||||
this._model.pushStackElement();
|
||||
}
|
||||
|
||||
const result = CommandExecutor.executeCommands(this._model, this._cursors.getSelections(), opResult.commands);
|
||||
if (result) {
|
||||
// The commands were applied correctly
|
||||
this._interpretCommandResult(result);
|
||||
}
|
||||
|
||||
if (opResult.shouldPushStackElementAfter) {
|
||||
this._model.pushStackElement();
|
||||
}
|
||||
}
|
||||
|
||||
private _interpretCommandResult(cursorState: Selection[]): void {
|
||||
if (!cursorState || cursorState.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._columnSelectData = null;
|
||||
this._cursors.setSelections(cursorState);
|
||||
this._cursors.normalize();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
// ----- emitting events
|
||||
|
||||
private _emitStateChangedIfNecessary(source: string, reason: CursorChangeReason, oldState: CursorModelState): boolean {
|
||||
const newState = new CursorModelState(this._model, this);
|
||||
if (newState.equals(oldState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let isInEditableRange: boolean = true;
|
||||
if (this._model.hasEditableRange()) {
|
||||
const editableRange = this._model.getEditableRange();
|
||||
if (!editableRange.containsPosition(newState.cursorState[0].modelState.position)) {
|
||||
isInEditableRange = false;
|
||||
}
|
||||
}
|
||||
|
||||
const selections = this._cursors.getSelections();
|
||||
const viewSelections = this._cursors.getViewSelections();
|
||||
|
||||
// Let the view get the event first.
|
||||
this._emit([new viewEvents.ViewCursorStateChangedEvent(viewSelections, isInEditableRange)]);
|
||||
|
||||
// Only after the view has been notified, let the rest of the world know...
|
||||
if (!oldState
|
||||
|| oldState.cursorState.length !== newState.cursorState.length
|
||||
|| newState.cursorState.some((newCursorState, i) => !newCursorState.modelState.equals(oldState.cursorState[i].modelState))
|
||||
) {
|
||||
this._onDidChange.fire(new CursorStateChangedEvent(selections, source || 'keyboard', reason));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _revealRange(revealTarget: RevealTarget, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void {
|
||||
const viewPositions = this._cursors.getViewPositions();
|
||||
|
||||
let viewPosition = viewPositions[0];
|
||||
|
||||
if (revealTarget === RevealTarget.TopMost) {
|
||||
for (let i = 1; i < viewPositions.length; i++) {
|
||||
if (viewPositions[i].isBefore(viewPosition)) {
|
||||
viewPosition = viewPositions[i];
|
||||
}
|
||||
}
|
||||
} else if (revealTarget === RevealTarget.BottomMost) {
|
||||
for (let i = 1; i < viewPositions.length; i++) {
|
||||
if (viewPosition.isBeforeOrEqual(viewPositions[i])) {
|
||||
viewPosition = viewPositions[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (viewPositions.length > 1) {
|
||||
// no revealing!
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column);
|
||||
this.emitCursorRevealRange(viewRange, verticalType, revealHorizontal, scrollType);
|
||||
}
|
||||
|
||||
public emitCursorRevealRange(viewRange: Range, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) {
|
||||
this._emit([new viewEvents.ViewRevealRangeRequestEvent(viewRange, verticalType, revealHorizontal, scrollType)]);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
// ----- handlers beyond this point
|
||||
|
||||
public trigger(source: string, handlerId: string, payload: any): void {
|
||||
const H = editorCommon.Handler;
|
||||
|
||||
if (handlerId === H.CompositionStart) {
|
||||
this._isDoingComposition = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (handlerId === H.CompositionEnd) {
|
||||
this._isDoingComposition = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = new CursorModelState(this._model, this);
|
||||
let cursorChangeReason = CursorChangeReason.NotSet;
|
||||
|
||||
// ensure valid state on all cursors
|
||||
this._cursors.ensureValidState();
|
||||
|
||||
this._isHandling = true;
|
||||
|
||||
try {
|
||||
switch (handlerId) {
|
||||
case H.Type:
|
||||
this._type(source, <string>payload.text);
|
||||
break;
|
||||
|
||||
case H.ReplacePreviousChar:
|
||||
this._replacePreviousChar(<string>payload.text, <number>payload.replaceCharCnt);
|
||||
break;
|
||||
|
||||
case H.Paste:
|
||||
cursorChangeReason = CursorChangeReason.Paste;
|
||||
this._paste(<string>payload.text, <boolean>payload.pasteOnNewLine);
|
||||
break;
|
||||
|
||||
case H.Cut:
|
||||
this._cut();
|
||||
break;
|
||||
|
||||
case H.Undo:
|
||||
cursorChangeReason = CursorChangeReason.Undo;
|
||||
this._interpretCommandResult(this._model.undo());
|
||||
break;
|
||||
|
||||
case H.Redo:
|
||||
cursorChangeReason = CursorChangeReason.Redo;
|
||||
this._interpretCommandResult(this._model.redo());
|
||||
break;
|
||||
|
||||
case H.ExecuteCommand:
|
||||
this._externalExecuteCommand(<editorCommon.ICommand>payload);
|
||||
break;
|
||||
|
||||
case H.ExecuteCommands:
|
||||
this._externalExecuteCommands(<editorCommon.ICommand[]>payload);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
|
||||
this._isHandling = false;
|
||||
|
||||
if (this._emitStateChangedIfNecessary(source, cursorChangeReason, oldState)) {
|
||||
this._revealRange(RevealTarget.Primary, viewEvents.VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
}
|
||||
|
||||
private _type(source: string, text: string): void {
|
||||
if (!this._isDoingComposition && source === 'keyboard') {
|
||||
// If this event is coming straight from the keyboard, look for electric characters and enter
|
||||
|
||||
for (let i = 0, len = text.length; i < len; i++) {
|
||||
let charCode = text.charCodeAt(i);
|
||||
let chr: string;
|
||||
if (strings.isHighSurrogate(charCode) && i + 1 < len) {
|
||||
chr = text.charAt(i) + text.charAt(i + 1);
|
||||
i++;
|
||||
} else {
|
||||
chr = text.charAt(i);
|
||||
}
|
||||
|
||||
// Here we must interpret each typed character individually, that's why we create a new context
|
||||
this._executeEditOperation(TypeOperations.typeWithInterceptors(this.context.config, this.context.model, this.getSelections(), chr));
|
||||
}
|
||||
|
||||
} else {
|
||||
this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this.context.config, this.context.model, this.getSelections(), text));
|
||||
}
|
||||
}
|
||||
|
||||
private _replacePreviousChar(text: string, replaceCharCnt: number): void {
|
||||
this._executeEditOperation(TypeOperations.replacePreviousChar(this.context.config, this.context.model, this.getSelections(), text, replaceCharCnt));
|
||||
}
|
||||
|
||||
private _paste(text: string, pasteOnNewLine: boolean): void {
|
||||
this._executeEditOperation(TypeOperations.paste(this.context.config, this.context.model, this.getSelections(), pasteOnNewLine, text));
|
||||
}
|
||||
|
||||
private _cut(): void {
|
||||
this._executeEditOperation(DeleteOperations.cut(this.context.config, this.context.model, this.getSelections()));
|
||||
}
|
||||
|
||||
private _externalExecuteCommand(command: editorCommon.ICommand): void {
|
||||
this._cursors.killSecondaryCursors();
|
||||
|
||||
this._executeEditOperation(new EditOperationResult([command], {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
}));
|
||||
}
|
||||
|
||||
private _externalExecuteCommands(commands: editorCommon.ICommand[]): void {
|
||||
this._executeEditOperation(new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
interface IExecContext {
|
||||
readonly model: editorCommon.IModel;
|
||||
readonly selectionsBefore: Selection[];
|
||||
readonly selectionStartMarkers: string[];
|
||||
readonly positionMarkers: string[];
|
||||
}
|
||||
|
||||
interface ICommandData {
|
||||
operations: editorCommon.IIdentifiedSingleEditOperation[];
|
||||
hadTrackedEditOperation: boolean;
|
||||
}
|
||||
|
||||
interface ICommandsData {
|
||||
operations: editorCommon.IIdentifiedSingleEditOperation[];
|
||||
hadTrackedEditOperation: boolean;
|
||||
}
|
||||
|
||||
class CommandExecutor {
|
||||
|
||||
public static executeCommands(model: editorCommon.IModel, selectionsBefore: Selection[], commands: editorCommon.ICommand[]): Selection[] {
|
||||
|
||||
const ctx: IExecContext = {
|
||||
model: model,
|
||||
selectionsBefore: selectionsBefore,
|
||||
selectionStartMarkers: [],
|
||||
positionMarkers: []
|
||||
};
|
||||
|
||||
const result = this._innerExecuteCommands(ctx, commands);
|
||||
|
||||
for (let i = 0; i < ctx.selectionStartMarkers.length; i++) {
|
||||
ctx.model._removeMarker(ctx.selectionStartMarkers[i]);
|
||||
ctx.model._removeMarker(ctx.positionMarkers[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _innerExecuteCommands(ctx: IExecContext, commands: editorCommon.ICommand[]): Selection[] {
|
||||
|
||||
if (this._arrayIsEmpty(commands)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commandsData = this._getEditOperations(ctx, commands);
|
||||
if (commandsData.operations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawOperations = commandsData.operations;
|
||||
|
||||
const editableRange = ctx.model.getEditableRange();
|
||||
const editableRangeStart = editableRange.getStartPosition();
|
||||
const editableRangeEnd = editableRange.getEndPosition();
|
||||
for (let i = 0, len = rawOperations.length; i < len; i++) {
|
||||
const operationRange = rawOperations[i].range;
|
||||
if (!editableRangeStart.isBeforeOrEqual(operationRange.getStartPosition()) || !operationRange.getEndPosition().isBeforeOrEqual(editableRangeEnd)) {
|
||||
// These commands are outside of the editable range
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const loserCursorsMap = this._getLoserCursorMap(rawOperations);
|
||||
if (loserCursorsMap.hasOwnProperty('0')) {
|
||||
// These commands are very messed up
|
||||
console.warn('Ignoring commands');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove operations belonging to losing cursors
|
||||
let filteredOperations: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
for (let i = 0, len = rawOperations.length; i < len; i++) {
|
||||
if (!loserCursorsMap.hasOwnProperty(rawOperations[i].identifier.major.toString())) {
|
||||
filteredOperations.push(rawOperations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO@Alex: find a better way to do this.
|
||||
// give the hint that edit operations are tracked to the model
|
||||
if (commandsData.hadTrackedEditOperation && filteredOperations.length > 0) {
|
||||
filteredOperations[0]._isTracked = true;
|
||||
}
|
||||
const selectionsAfter = ctx.model.pushEditOperations(ctx.selectionsBefore, filteredOperations, (inverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[]): Selection[] => {
|
||||
let groupedInverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[][] = [];
|
||||
for (let i = 0; i < ctx.selectionsBefore.length; i++) {
|
||||
groupedInverseEditOperations[i] = [];
|
||||
}
|
||||
for (let i = 0; i < inverseEditOperations.length; i++) {
|
||||
const op = inverseEditOperations[i];
|
||||
if (!op.identifier) {
|
||||
// perhaps auto whitespace trim edits
|
||||
continue;
|
||||
}
|
||||
groupedInverseEditOperations[op.identifier.major].push(op);
|
||||
}
|
||||
const minorBasedSorter = (a: editorCommon.IIdentifiedSingleEditOperation, b: editorCommon.IIdentifiedSingleEditOperation) => {
|
||||
return a.identifier.minor - b.identifier.minor;
|
||||
};
|
||||
let cursorSelections: Selection[] = [];
|
||||
for (let i = 0; i < ctx.selectionsBefore.length; i++) {
|
||||
if (groupedInverseEditOperations[i].length > 0) {
|
||||
groupedInverseEditOperations[i].sort(minorBasedSorter);
|
||||
cursorSelections[i] = commands[i].computeCursorState(ctx.model, {
|
||||
getInverseEditOperations: () => {
|
||||
return groupedInverseEditOperations[i];
|
||||
},
|
||||
|
||||
getTrackedSelection: (id: string) => {
|
||||
const idx = parseInt(id, 10);
|
||||
const selectionStartMarker = ctx.model._getMarker(ctx.selectionStartMarkers[idx]);
|
||||
const positionMarker = ctx.model._getMarker(ctx.positionMarkers[idx]);
|
||||
return new Selection(selectionStartMarker.lineNumber, selectionStartMarker.column, positionMarker.lineNumber, positionMarker.column);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cursorSelections[i] = ctx.selectionsBefore[i];
|
||||
}
|
||||
}
|
||||
return cursorSelections;
|
||||
});
|
||||
|
||||
// Extract losing cursors
|
||||
let losingCursors: number[] = [];
|
||||
for (let losingCursorIndex in loserCursorsMap) {
|
||||
if (loserCursorsMap.hasOwnProperty(losingCursorIndex)) {
|
||||
losingCursors.push(parseInt(losingCursorIndex, 10));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort losing cursors descending
|
||||
losingCursors.sort((a: number, b: number): number => {
|
||||
return b - a;
|
||||
});
|
||||
|
||||
// Remove losing cursors
|
||||
for (let i = 0; i < losingCursors.length; i++) {
|
||||
selectionsAfter.splice(losingCursors[i], 1);
|
||||
}
|
||||
|
||||
return selectionsAfter;
|
||||
}
|
||||
|
||||
private static _arrayIsEmpty(commands: editorCommon.ICommand[]): boolean {
|
||||
for (let i = 0, len = commands.length; i < len; i++) {
|
||||
if (commands[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _getEditOperations(ctx: IExecContext, commands: editorCommon.ICommand[]): ICommandsData {
|
||||
let operations: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
let hadTrackedEditOperation: boolean = false;
|
||||
|
||||
for (let i = 0, len = commands.length; i < len; i++) {
|
||||
if (commands[i]) {
|
||||
const r = this._getEditOperationsFromCommand(ctx, i, commands[i]);
|
||||
operations = operations.concat(r.operations);
|
||||
hadTrackedEditOperation = hadTrackedEditOperation || r.hadTrackedEditOperation;
|
||||
}
|
||||
}
|
||||
return {
|
||||
operations: operations,
|
||||
hadTrackedEditOperation: hadTrackedEditOperation
|
||||
};
|
||||
}
|
||||
|
||||
private static _getEditOperationsFromCommand(ctx: IExecContext, majorIdentifier: number, command: editorCommon.ICommand): ICommandData {
|
||||
// This method acts as a transaction, if the command fails
|
||||
// everything it has done is ignored
|
||||
let operations: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
let operationMinor = 0;
|
||||
|
||||
const addEditOperation = (selection: Range, text: string) => {
|
||||
if (selection.isEmpty() && text === '') {
|
||||
// This command wants to add a no-op => no thank you
|
||||
return;
|
||||
}
|
||||
operations.push({
|
||||
identifier: {
|
||||
major: majorIdentifier,
|
||||
minor: operationMinor++
|
||||
},
|
||||
range: selection,
|
||||
text: text,
|
||||
forceMoveMarkers: false,
|
||||
isAutoWhitespaceEdit: command.insertsAutoWhitespace
|
||||
});
|
||||
};
|
||||
|
||||
let hadTrackedEditOperation = false;
|
||||
const addTrackedEditOperation = (selection: Range, text: string) => {
|
||||
hadTrackedEditOperation = true;
|
||||
addEditOperation(selection, text);
|
||||
};
|
||||
|
||||
const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => {
|
||||
let selectionMarkerStickToPreviousCharacter: boolean;
|
||||
let positionMarkerStickToPreviousCharacter: boolean;
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
// Try to lock it with surrounding text
|
||||
if (typeof trackPreviousOnEmpty === 'boolean') {
|
||||
selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty;
|
||||
positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty;
|
||||
} else {
|
||||
const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber);
|
||||
if (selection.startColumn === maxLineColumn) {
|
||||
selectionMarkerStickToPreviousCharacter = true;
|
||||
positionMarkerStickToPreviousCharacter = true;
|
||||
} else {
|
||||
selectionMarkerStickToPreviousCharacter = false;
|
||||
positionMarkerStickToPreviousCharacter = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selection.getDirection() === SelectionDirection.LTR) {
|
||||
selectionMarkerStickToPreviousCharacter = false;
|
||||
positionMarkerStickToPreviousCharacter = true;
|
||||
} else {
|
||||
selectionMarkerStickToPreviousCharacter = true;
|
||||
positionMarkerStickToPreviousCharacter = false;
|
||||
}
|
||||
}
|
||||
|
||||
const l = ctx.selectionStartMarkers.length;
|
||||
ctx.selectionStartMarkers[l] = ctx.model._addMarker(0, selection.selectionStartLineNumber, selection.selectionStartColumn, selectionMarkerStickToPreviousCharacter);
|
||||
ctx.positionMarkers[l] = ctx.model._addMarker(0, selection.positionLineNumber, selection.positionColumn, positionMarkerStickToPreviousCharacter);
|
||||
return l.toString();
|
||||
};
|
||||
|
||||
const editOperationBuilder: editorCommon.IEditOperationBuilder = {
|
||||
addEditOperation: addEditOperation,
|
||||
addTrackedEditOperation: addTrackedEditOperation,
|
||||
trackSelection: trackSelection
|
||||
};
|
||||
|
||||
try {
|
||||
command.getEditOperations(ctx.model, editOperationBuilder);
|
||||
} catch (e) {
|
||||
e.friendlyMessage = nls.localize('corrupt.commands', "Unexpected exception while executing command.");
|
||||
onUnexpectedError(e);
|
||||
return {
|
||||
operations: [],
|
||||
hadTrackedEditOperation: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
operations: operations,
|
||||
hadTrackedEditOperation: hadTrackedEditOperation
|
||||
};
|
||||
}
|
||||
|
||||
private static _getLoserCursorMap(operations: editorCommon.IIdentifiedSingleEditOperation[]): { [index: string]: boolean; } {
|
||||
// This is destructive on the array
|
||||
operations = operations.slice(0);
|
||||
|
||||
// Sort operations with last one first
|
||||
operations.sort((a: editorCommon.IIdentifiedSingleEditOperation, b: editorCommon.IIdentifiedSingleEditOperation): number => {
|
||||
// Note the minus!
|
||||
return -(Range.compareRangesUsingEnds(a.range, b.range));
|
||||
});
|
||||
|
||||
// Operations can not overlap!
|
||||
let loserCursorsMap: { [index: string]: boolean; } = {};
|
||||
|
||||
for (let i = 1; i < operations.length; i++) {
|
||||
const previousOp = operations[i - 1];
|
||||
const currentOp = operations[i];
|
||||
|
||||
if (previousOp.range.getStartPosition().isBefore(currentOp.range.getEndPosition())) {
|
||||
|
||||
let loserMajor: number;
|
||||
|
||||
if (previousOp.identifier.major > currentOp.identifier.major) {
|
||||
// previousOp loses the battle
|
||||
loserMajor = previousOp.identifier.major;
|
||||
} else {
|
||||
loserMajor = currentOp.identifier.major;
|
||||
}
|
||||
|
||||
loserCursorsMap[loserMajor.toString()] = true;
|
||||
|
||||
for (let j = 0; j < operations.length; j++) {
|
||||
if (operations[j].identifier.major === loserMajor) {
|
||||
operations.splice(j, 1);
|
||||
if (j < i) {
|
||||
i--;
|
||||
}
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return loserCursorsMap;
|
||||
}
|
||||
}
|
||||
256
src/vs/editor/common/controller/cursorCollection.ts
Normal file
256
src/vs/editor/common/controller/cursorCollection.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { OneCursor } from 'vs/editor/common/controller/oneCursor';
|
||||
import { Selection, ISelection } from 'vs/editor/common/core/selection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { CursorState, CursorContext } from 'vs/editor/common/controller/cursorCommon';
|
||||
|
||||
export class CursorCollection {
|
||||
|
||||
private context: CursorContext;
|
||||
|
||||
private primaryCursor: OneCursor;
|
||||
private secondaryCursors: OneCursor[];
|
||||
|
||||
// An index which identifies the last cursor that was added / moved (think Ctrl+drag)
|
||||
private lastAddedCursorIndex: number;
|
||||
|
||||
constructor(context: CursorContext) {
|
||||
this.context = context;
|
||||
this.primaryCursor = new OneCursor(context);
|
||||
this.secondaryCursors = [];
|
||||
this.lastAddedCursorIndex = 0;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.primaryCursor.dispose(this.context);
|
||||
this.killSecondaryCursors();
|
||||
}
|
||||
|
||||
public updateContext(context: CursorContext): void {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ensureValidState(): void {
|
||||
this.primaryCursor.ensureValidState(this.context);
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
this.secondaryCursors[i].ensureValidState(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
public readSelectionFromMarkers(): Selection[] {
|
||||
let result: Selection[] = [];
|
||||
result[0] = this.primaryCursor.readSelectionFromMarkers(this.context);
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
result[i + 1] = this.secondaryCursors[i].readSelectionFromMarkers(this.context);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getAll(): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
result[0] = this.primaryCursor.asCursorState();
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
result[i + 1] = this.secondaryCursors[i].asCursorState();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getViewPositions(): Position[] {
|
||||
let result: Position[] = [];
|
||||
result[0] = this.primaryCursor.viewState.position;
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
result[i + 1] = this.secondaryCursors[i].viewState.position;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getSelections(): Selection[] {
|
||||
let result: Selection[] = [];
|
||||
result[0] = this.primaryCursor.modelState.selection;
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
result[i + 1] = this.secondaryCursors[i].modelState.selection;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getViewSelections(): Selection[] {
|
||||
let result: Selection[] = [];
|
||||
result[0] = this.primaryCursor.viewState.selection;
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
result[i + 1] = this.secondaryCursors[i].viewState.selection;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public setSelections(selections: ISelection[]): void {
|
||||
this.setStates(CursorState.fromModelSelections(selections));
|
||||
}
|
||||
|
||||
public getPrimaryCursor(): CursorState {
|
||||
return this.primaryCursor.asCursorState();
|
||||
}
|
||||
|
||||
public setStates(states: CursorState[]): void {
|
||||
if (states === null) {
|
||||
return;
|
||||
}
|
||||
this.primaryCursor.setState(this.context, states[0].modelState, states[0].viewState);
|
||||
this._setSecondaryStates(states.slice(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or disposes secondary cursors as necessary to match the number of `secondarySelections`.
|
||||
*/
|
||||
private _setSecondaryStates(secondaryStates: CursorState[]): void {
|
||||
const secondaryCursorsLength = this.secondaryCursors.length;
|
||||
const secondaryStatesLength = secondaryStates.length;
|
||||
|
||||
if (secondaryCursorsLength < secondaryStatesLength) {
|
||||
let createCnt = secondaryStatesLength - secondaryCursorsLength;
|
||||
for (let i = 0; i < createCnt; i++) {
|
||||
this._addSecondaryCursor();
|
||||
}
|
||||
} else if (secondaryCursorsLength > secondaryStatesLength) {
|
||||
let removeCnt = secondaryCursorsLength - secondaryStatesLength;
|
||||
for (let i = 0; i < removeCnt; i++) {
|
||||
this._removeSecondaryCursor(this.secondaryCursors.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < secondaryStatesLength; i++) {
|
||||
this.secondaryCursors[i].setState(this.context, secondaryStates[i].modelState, secondaryStates[i].viewState);
|
||||
}
|
||||
}
|
||||
|
||||
public killSecondaryCursors(): void {
|
||||
this._setSecondaryStates([]);
|
||||
}
|
||||
|
||||
private _addSecondaryCursor(): void {
|
||||
this.secondaryCursors.push(new OneCursor(this.context));
|
||||
this.lastAddedCursorIndex = this.secondaryCursors.length;
|
||||
}
|
||||
|
||||
public getLastAddedCursorIndex(): number {
|
||||
if (this.secondaryCursors.length === 0 || this.lastAddedCursorIndex === 0) {
|
||||
return 0;
|
||||
}
|
||||
return this.lastAddedCursorIndex;
|
||||
}
|
||||
|
||||
private _removeSecondaryCursor(removeIndex: number): void {
|
||||
if (this.lastAddedCursorIndex >= removeIndex + 1) {
|
||||
this.lastAddedCursorIndex--;
|
||||
}
|
||||
this.secondaryCursors[removeIndex].dispose(this.context);
|
||||
this.secondaryCursors.splice(removeIndex, 1);
|
||||
}
|
||||
|
||||
private _getAll(): OneCursor[] {
|
||||
let result: OneCursor[] = [];
|
||||
result[0] = this.primaryCursor;
|
||||
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
|
||||
result[i + 1] = this.secondaryCursors[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public normalize(): void {
|
||||
if (this.secondaryCursors.length === 0) {
|
||||
return;
|
||||
}
|
||||
let cursors = this._getAll();
|
||||
|
||||
interface SortedCursor {
|
||||
index: number;
|
||||
selection: Selection;
|
||||
viewSelection: Selection;
|
||||
}
|
||||
let sortedCursors: SortedCursor[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
sortedCursors.push({
|
||||
index: i,
|
||||
selection: cursors[i].modelState.selection,
|
||||
viewSelection: cursors[i].viewState.selection
|
||||
});
|
||||
}
|
||||
sortedCursors.sort((a, b) => {
|
||||
if (a.viewSelection.startLineNumber === b.viewSelection.startLineNumber) {
|
||||
return a.viewSelection.startColumn - b.viewSelection.startColumn;
|
||||
}
|
||||
return a.viewSelection.startLineNumber - b.viewSelection.startLineNumber;
|
||||
});
|
||||
|
||||
for (let sortedCursorIndex = 0; sortedCursorIndex < sortedCursors.length - 1; sortedCursorIndex++) {
|
||||
const current = sortedCursors[sortedCursorIndex];
|
||||
const next = sortedCursors[sortedCursorIndex + 1];
|
||||
|
||||
const currentViewSelection = current.viewSelection;
|
||||
const nextViewSelection = next.viewSelection;
|
||||
|
||||
let shouldMergeCursors: boolean;
|
||||
if (nextViewSelection.isEmpty() || currentViewSelection.isEmpty()) {
|
||||
// Merge touching cursors if one of them is collapsed
|
||||
shouldMergeCursors = nextViewSelection.getStartPosition().isBeforeOrEqual(currentViewSelection.getEndPosition());
|
||||
} else {
|
||||
// Merge only overlapping cursors (i.e. allow touching ranges)
|
||||
shouldMergeCursors = nextViewSelection.getStartPosition().isBefore(currentViewSelection.getEndPosition());
|
||||
}
|
||||
|
||||
if (shouldMergeCursors) {
|
||||
const winnerSortedCursorIndex = current.index < next.index ? sortedCursorIndex : sortedCursorIndex + 1;
|
||||
const looserSortedCursorIndex = current.index < next.index ? sortedCursorIndex + 1 : sortedCursorIndex;
|
||||
|
||||
const looserIndex = sortedCursors[looserSortedCursorIndex].index;
|
||||
const winnerIndex = sortedCursors[winnerSortedCursorIndex].index;
|
||||
|
||||
const looserSelection = sortedCursors[looserSortedCursorIndex].selection;
|
||||
const winnerSelection = sortedCursors[winnerSortedCursorIndex].selection;
|
||||
|
||||
if (!looserSelection.equalsSelection(winnerSelection)) {
|
||||
const resultingRange = looserSelection.plusRange(winnerSelection);
|
||||
const looserSelectionIsLTR = (looserSelection.selectionStartLineNumber === looserSelection.startLineNumber && looserSelection.selectionStartColumn === looserSelection.startColumn);
|
||||
const winnerSelectionIsLTR = (winnerSelection.selectionStartLineNumber === winnerSelection.startLineNumber && winnerSelection.selectionStartColumn === winnerSelection.startColumn);
|
||||
|
||||
// Give more importance to the last added cursor (think Ctrl-dragging + hitting another cursor)
|
||||
let resultingSelectionIsLTR: boolean;
|
||||
if (looserIndex === this.lastAddedCursorIndex) {
|
||||
resultingSelectionIsLTR = looserSelectionIsLTR;
|
||||
this.lastAddedCursorIndex = winnerIndex;
|
||||
} else {
|
||||
// Winner takes it all
|
||||
resultingSelectionIsLTR = winnerSelectionIsLTR;
|
||||
}
|
||||
|
||||
let resultingSelection: Selection;
|
||||
if (resultingSelectionIsLTR) {
|
||||
resultingSelection = new Selection(resultingRange.startLineNumber, resultingRange.startColumn, resultingRange.endLineNumber, resultingRange.endColumn);
|
||||
} else {
|
||||
resultingSelection = new Selection(resultingRange.endLineNumber, resultingRange.endColumn, resultingRange.startLineNumber, resultingRange.startColumn);
|
||||
}
|
||||
|
||||
sortedCursors[winnerSortedCursorIndex].selection = resultingSelection;
|
||||
const resultingState = CursorState.fromModelSelection(resultingSelection);
|
||||
cursors[winnerIndex].setState(this.context, resultingState.modelState, resultingState.viewState);
|
||||
}
|
||||
|
||||
for (let j = 0; j < sortedCursors.length; j++) {
|
||||
if (sortedCursors[j].index > looserIndex) {
|
||||
sortedCursors[j].index--;
|
||||
}
|
||||
}
|
||||
|
||||
cursors.splice(looserIndex, 1);
|
||||
sortedCursors.splice(looserSortedCursorIndex, 1);
|
||||
this._removeSecondaryCursor(looserIndex - 1);
|
||||
|
||||
sortedCursorIndex--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/vs/editor/common/controller/cursorColumnSelection.ts
Normal file
125
src/vs/editor/common/controller/cursorColumnSelection.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { SingleCursorState, CursorColumns, CursorConfiguration, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon';
|
||||
|
||||
export interface IColumnSelectResult {
|
||||
viewStates: SingleCursorState[];
|
||||
reversed: boolean;
|
||||
toLineNumber: number;
|
||||
toVisualColumn: number;
|
||||
}
|
||||
|
||||
export class ColumnSelection {
|
||||
|
||||
private static _columnSelect(config: CursorConfiguration, model: ICursorSimpleModel, fromLineNumber: number, fromVisibleColumn: number, toLineNumber: number, toVisibleColumn: number): IColumnSelectResult {
|
||||
let lineCount = Math.abs(toLineNumber - fromLineNumber) + 1;
|
||||
let reversed = (fromLineNumber > toLineNumber);
|
||||
let isRTL = (fromVisibleColumn > toVisibleColumn);
|
||||
let isLTR = (fromVisibleColumn < toVisibleColumn);
|
||||
|
||||
let result: SingleCursorState[] = [];
|
||||
|
||||
// console.log(`fromVisibleColumn: ${fromVisibleColumn}, toVisibleColumn: ${toVisibleColumn}`);
|
||||
|
||||
for (let i = 0; i < lineCount; i++) {
|
||||
let lineNumber = fromLineNumber + (reversed ? -i : i);
|
||||
|
||||
let startColumn = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, fromVisibleColumn);
|
||||
let endColumn = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, toVisibleColumn);
|
||||
let visibleStartColumn = CursorColumns.visibleColumnFromColumn2(config, model, new Position(lineNumber, startColumn));
|
||||
let visibleEndColumn = CursorColumns.visibleColumnFromColumn2(config, model, new Position(lineNumber, endColumn));
|
||||
|
||||
// console.log(`lineNumber: ${lineNumber}: visibleStartColumn: ${visibleStartColumn}, visibleEndColumn: ${visibleEndColumn}`);
|
||||
|
||||
if (isLTR) {
|
||||
if (visibleStartColumn > toVisibleColumn) {
|
||||
continue;
|
||||
}
|
||||
if (visibleEndColumn < fromVisibleColumn) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRTL) {
|
||||
if (visibleEndColumn > fromVisibleColumn) {
|
||||
continue;
|
||||
}
|
||||
if (visibleStartColumn < toVisibleColumn) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(new SingleCursorState(
|
||||
new Range(lineNumber, startColumn, lineNumber, startColumn), 0,
|
||||
new Position(lineNumber, endColumn), 0
|
||||
));
|
||||
}
|
||||
|
||||
return {
|
||||
viewStates: result,
|
||||
reversed: reversed,
|
||||
toLineNumber: toLineNumber,
|
||||
toVisualColumn: toVisibleColumn
|
||||
};
|
||||
}
|
||||
|
||||
public static columnSelect(config: CursorConfiguration, model: ICursorSimpleModel, fromViewSelection: Selection, toViewLineNumber: number, toViewVisualColumn: number): IColumnSelectResult {
|
||||
const fromViewPosition = new Position(fromViewSelection.selectionStartLineNumber, fromViewSelection.selectionStartColumn);
|
||||
const fromViewVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, fromViewPosition);
|
||||
return ColumnSelection._columnSelect(config, model, fromViewPosition.lineNumber, fromViewVisibleColumn, toViewLineNumber, toViewVisualColumn);
|
||||
}
|
||||
|
||||
public static columnSelectLeft(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, toViewLineNumber: number, toViewVisualColumn: number): IColumnSelectResult {
|
||||
if (toViewVisualColumn > 1) {
|
||||
toViewVisualColumn--;
|
||||
}
|
||||
|
||||
return this.columnSelect(config, model, cursor.selection, toViewLineNumber, toViewVisualColumn);
|
||||
}
|
||||
|
||||
public static columnSelectRight(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, toViewLineNumber: number, toViewVisualColumn: number): IColumnSelectResult {
|
||||
let maxVisualViewColumn = 0;
|
||||
let minViewLineNumber = Math.min(cursor.position.lineNumber, toViewLineNumber);
|
||||
let maxViewLineNumber = Math.max(cursor.position.lineNumber, toViewLineNumber);
|
||||
for (let lineNumber = minViewLineNumber; lineNumber <= maxViewLineNumber; lineNumber++) {
|
||||
let lineMaxViewColumn = model.getLineMaxColumn(lineNumber);
|
||||
let lineMaxVisualViewColumn = CursorColumns.visibleColumnFromColumn2(config, model, new Position(lineNumber, lineMaxViewColumn));
|
||||
maxVisualViewColumn = Math.max(maxVisualViewColumn, lineMaxVisualViewColumn);
|
||||
}
|
||||
|
||||
if (toViewVisualColumn < maxVisualViewColumn) {
|
||||
toViewVisualColumn++;
|
||||
}
|
||||
|
||||
return this.columnSelect(config, model, cursor.selection, toViewLineNumber, toViewVisualColumn);
|
||||
}
|
||||
|
||||
public static columnSelectUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, isPaged: boolean, toViewLineNumber: number, toViewVisualColumn: number): IColumnSelectResult {
|
||||
let linesCount = isPaged ? config.pageSize : 1;
|
||||
|
||||
toViewLineNumber -= linesCount;
|
||||
if (toViewLineNumber < 1) {
|
||||
toViewLineNumber = 1;
|
||||
}
|
||||
|
||||
return this.columnSelect(config, model, cursor.selection, toViewLineNumber, toViewVisualColumn);
|
||||
}
|
||||
|
||||
public static columnSelectDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, isPaged: boolean, toViewLineNumber: number, toViewVisualColumn: number): IColumnSelectResult {
|
||||
let linesCount = isPaged ? config.pageSize : 1;
|
||||
|
||||
toViewLineNumber += linesCount;
|
||||
if (toViewLineNumber > model.getLineCount()) {
|
||||
toViewLineNumber = model.getLineCount();
|
||||
}
|
||||
|
||||
return this.columnSelect(config, model, cursor.selection, toViewLineNumber, toViewVisualColumn);
|
||||
}
|
||||
}
|
||||
553
src/vs/editor/common/controller/cursorCommon.ts
Normal file
553
src/vs/editor/common/controller/cursorCommon.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Position } from 'vs/editor/common/core/position';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ICommand, TextModelResolvedOptions, IConfiguration, IModel, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { Selection, ISelection } from 'vs/editor/common/core/selection';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
|
||||
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
|
||||
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { VerticalRevealType } from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export interface IColumnSelectData {
|
||||
toViewLineNumber: number;
|
||||
toViewVisualColumn: number;
|
||||
}
|
||||
|
||||
export const enum RevealTarget {
|
||||
Primary = 0,
|
||||
TopMost = 1,
|
||||
BottomMost = 2
|
||||
}
|
||||
|
||||
export interface ICursors {
|
||||
readonly context: CursorContext;
|
||||
getPrimaryCursor(): CursorState;
|
||||
getLastAddedCursorIndex(): number;
|
||||
getAll(): CursorState[];
|
||||
|
||||
getColumnSelectData(): IColumnSelectData;
|
||||
setColumnSelectData(columnSelectData: IColumnSelectData): void;
|
||||
|
||||
setStates(source: string, reason: CursorChangeReason, states: CursorState[]): void;
|
||||
reveal(horizontal: boolean, target: RevealTarget, scrollType: ScrollType): void;
|
||||
revealRange(revealHorizontal: boolean, viewRange: Range, verticalType: VerticalRevealType, scrollType: ScrollType): void;
|
||||
|
||||
scrollTo(desiredScrollTop: number): void;
|
||||
}
|
||||
|
||||
export interface CharacterMap {
|
||||
[char: string]: string;
|
||||
}
|
||||
|
||||
export class CursorConfiguration {
|
||||
_cursorMoveConfigurationBrand: void;
|
||||
|
||||
public readonly readOnly: boolean;
|
||||
public readonly tabSize: number;
|
||||
public readonly insertSpaces: boolean;
|
||||
public readonly oneIndent: string;
|
||||
public readonly pageSize: number;
|
||||
public readonly lineHeight: number;
|
||||
public readonly useTabStops: boolean;
|
||||
public readonly wordSeparators: string;
|
||||
public readonly emptySelectionClipboard: boolean;
|
||||
public readonly autoClosingBrackets: boolean;
|
||||
public readonly autoIndent: boolean;
|
||||
public readonly autoClosingPairsOpen: CharacterMap;
|
||||
public readonly autoClosingPairsClose: CharacterMap;
|
||||
public readonly surroundingPairs: CharacterMap;
|
||||
public readonly electricChars: { [key: string]: boolean; };
|
||||
|
||||
public static shouldRecreate(e: IConfigurationChangedEvent): boolean {
|
||||
return (
|
||||
e.layoutInfo
|
||||
|| e.wordSeparators
|
||||
|| e.emptySelectionClipboard
|
||||
|| e.autoClosingBrackets
|
||||
|| e.useTabStops
|
||||
|| e.lineHeight
|
||||
|| e.readOnly
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
languageIdentifier: LanguageIdentifier,
|
||||
oneIndent: string,
|
||||
modelOptions: TextModelResolvedOptions,
|
||||
configuration: IConfiguration
|
||||
) {
|
||||
let c = configuration.editor;
|
||||
|
||||
this.readOnly = c.readOnly;
|
||||
this.tabSize = modelOptions.tabSize;
|
||||
this.insertSpaces = modelOptions.insertSpaces;
|
||||
this.oneIndent = oneIndent;
|
||||
this.pageSize = Math.floor(c.layoutInfo.height / c.fontInfo.lineHeight) - 2;
|
||||
this.lineHeight = c.lineHeight;
|
||||
this.useTabStops = c.useTabStops;
|
||||
this.wordSeparators = c.wordSeparators;
|
||||
this.emptySelectionClipboard = c.emptySelectionClipboard;
|
||||
this.autoClosingBrackets = c.autoClosingBrackets;
|
||||
this.autoIndent = c.autoIndent;
|
||||
|
||||
this.autoClosingPairsOpen = {};
|
||||
this.autoClosingPairsClose = {};
|
||||
this.surroundingPairs = {};
|
||||
this.electricChars = {};
|
||||
|
||||
let electricChars = CursorConfiguration._getElectricCharacters(languageIdentifier);
|
||||
if (electricChars) {
|
||||
for (let i = 0; i < electricChars.length; i++) {
|
||||
this.electricChars[electricChars[i]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
let autoClosingPairs = CursorConfiguration._getAutoClosingPairs(languageIdentifier);
|
||||
if (autoClosingPairs) {
|
||||
for (let i = 0; i < autoClosingPairs.length; i++) {
|
||||
this.autoClosingPairsOpen[autoClosingPairs[i].open] = autoClosingPairs[i].close;
|
||||
this.autoClosingPairsClose[autoClosingPairs[i].close] = autoClosingPairs[i].open;
|
||||
}
|
||||
}
|
||||
|
||||
let surroundingPairs = CursorConfiguration._getSurroundingPairs(languageIdentifier);
|
||||
if (surroundingPairs) {
|
||||
for (let i = 0; i < surroundingPairs.length; i++) {
|
||||
this.surroundingPairs[surroundingPairs[i].open] = surroundingPairs[i].close;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public normalizeIndentation(str: string): string {
|
||||
return TextModel.normalizeIndentation(str, this.tabSize, this.insertSpaces);
|
||||
}
|
||||
|
||||
private static _getElectricCharacters(languageIdentifier: LanguageIdentifier): string[] {
|
||||
try {
|
||||
return LanguageConfigurationRegistry.getElectricCharacters(languageIdentifier.id);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] {
|
||||
try {
|
||||
return LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static _getSurroundingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] {
|
||||
try {
|
||||
return LanguageConfigurationRegistry.getSurroundingPairs(languageIdentifier.id);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a simple model (either the model or the view model).
|
||||
*/
|
||||
export interface ICursorSimpleModel {
|
||||
getLineCount(): number;
|
||||
getLineContent(lineNumber: number): string;
|
||||
getLineMinColumn(lineNumber: number): number;
|
||||
getLineMaxColumn(lineNumber: number): number;
|
||||
getLineFirstNonWhitespaceColumn(lineNumber: number): number;
|
||||
getLineLastNonWhitespaceColumn(lineNumber: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cursor state on either the model or on the view model.
|
||||
*/
|
||||
export class SingleCursorState {
|
||||
_singleCursorStateBrand: void;
|
||||
|
||||
// --- selection can start as a range (think double click and drag)
|
||||
public readonly selectionStart: Range;
|
||||
public readonly selectionStartLeftoverVisibleColumns: number;
|
||||
public readonly position: Position;
|
||||
public readonly leftoverVisibleColumns: number;
|
||||
public readonly selection: Selection;
|
||||
|
||||
constructor(
|
||||
selectionStart: Range,
|
||||
selectionStartLeftoverVisibleColumns: number,
|
||||
position: Position,
|
||||
leftoverVisibleColumns: number,
|
||||
) {
|
||||
this.selectionStart = selectionStart;
|
||||
this.selectionStartLeftoverVisibleColumns = selectionStartLeftoverVisibleColumns;
|
||||
this.position = position;
|
||||
this.leftoverVisibleColumns = leftoverVisibleColumns;
|
||||
this.selection = SingleCursorState._computeSelection(this.selectionStart, this.position);
|
||||
}
|
||||
|
||||
public equals(other: SingleCursorState) {
|
||||
return (
|
||||
this.selectionStartLeftoverVisibleColumns === other.selectionStartLeftoverVisibleColumns
|
||||
&& this.leftoverVisibleColumns === other.leftoverVisibleColumns
|
||||
&& this.position.equals(other.position)
|
||||
&& this.selectionStart.equalsRange(other.selectionStart)
|
||||
);
|
||||
}
|
||||
|
||||
public hasSelection(): boolean {
|
||||
return (!this.selection.isEmpty() || !this.selectionStart.isEmpty());
|
||||
}
|
||||
|
||||
public move(inSelectionMode: boolean, lineNumber: number, column: number, leftoverVisibleColumns: number): SingleCursorState {
|
||||
if (inSelectionMode) {
|
||||
// move just position
|
||||
return new SingleCursorState(
|
||||
this.selectionStart,
|
||||
this.selectionStartLeftoverVisibleColumns,
|
||||
new Position(lineNumber, column),
|
||||
leftoverVisibleColumns
|
||||
);
|
||||
} else {
|
||||
// move everything
|
||||
return new SingleCursorState(
|
||||
new Range(lineNumber, column, lineNumber, column),
|
||||
leftoverVisibleColumns,
|
||||
new Position(lineNumber, column),
|
||||
leftoverVisibleColumns
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static _computeSelection(selectionStart: Range, position: Position): Selection {
|
||||
let startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number;
|
||||
if (selectionStart.isEmpty()) {
|
||||
startLineNumber = selectionStart.startLineNumber;
|
||||
startColumn = selectionStart.startColumn;
|
||||
endLineNumber = position.lineNumber;
|
||||
endColumn = position.column;
|
||||
} else {
|
||||
if (position.isBeforeOrEqual(selectionStart.getStartPosition())) {
|
||||
startLineNumber = selectionStart.endLineNumber;
|
||||
startColumn = selectionStart.endColumn;
|
||||
endLineNumber = position.lineNumber;
|
||||
endColumn = position.column;
|
||||
} else {
|
||||
startLineNumber = selectionStart.startLineNumber;
|
||||
startColumn = selectionStart.startColumn;
|
||||
endLineNumber = position.lineNumber;
|
||||
endColumn = position.column;
|
||||
}
|
||||
}
|
||||
return new Selection(
|
||||
startLineNumber,
|
||||
startColumn,
|
||||
endLineNumber,
|
||||
endColumn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CursorContext {
|
||||
_cursorContextBrand: void;
|
||||
|
||||
public readonly model: IModel;
|
||||
public readonly viewModel: IViewModel;
|
||||
public readonly config: CursorConfiguration;
|
||||
|
||||
constructor(configuration: IConfiguration, model: IModel, viewModel: IViewModel) {
|
||||
this.model = model;
|
||||
this.viewModel = viewModel;
|
||||
this.config = new CursorConfiguration(
|
||||
this.model.getLanguageIdentifier(),
|
||||
this.model.getOneIndent(),
|
||||
this.model.getOptions(),
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
public validateViewPosition(viewPosition: Position, modelPosition: Position): Position {
|
||||
return this.viewModel.coordinatesConverter.validateViewPosition(viewPosition, modelPosition);
|
||||
}
|
||||
|
||||
public validateViewRange(viewRange: Range, expectedModelRange: Range): Range {
|
||||
return this.viewModel.coordinatesConverter.validateViewRange(viewRange, expectedModelRange);
|
||||
}
|
||||
|
||||
public convertViewRangeToModelRange(viewRange: Range): Range {
|
||||
return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange);
|
||||
}
|
||||
|
||||
public convertViewPositionToModelPosition(lineNumber: number, column: number): Position {
|
||||
return this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, column));
|
||||
}
|
||||
|
||||
public convertModelPositionToViewPosition(modelPosition: Position): Position {
|
||||
return this.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
|
||||
}
|
||||
|
||||
public convertModelRangeToViewRange(modelRange: Range): Range {
|
||||
return this.viewModel.coordinatesConverter.convertModelRangeToViewRange(modelRange);
|
||||
}
|
||||
|
||||
public getCurrentScrollTop(): number {
|
||||
return this.viewModel.viewLayout.getCurrentScrollTop();
|
||||
}
|
||||
|
||||
public getCompletelyVisibleViewRange(): Range {
|
||||
return this.viewModel.getCompletelyVisibleViewRange();
|
||||
}
|
||||
|
||||
public getCompletelyVisibleModelRange(): Range {
|
||||
const viewRange = this.viewModel.getCompletelyVisibleViewRange();
|
||||
return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange);
|
||||
}
|
||||
|
||||
public getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range {
|
||||
return this.viewModel.getCompletelyVisibleViewRangeAtScrollTop(scrollTop);
|
||||
}
|
||||
|
||||
public getCompletelyVisibleModelRangeAtScrollTop(scrollTop: number): Range {
|
||||
const viewRange = this.viewModel.getCompletelyVisibleViewRangeAtScrollTop(scrollTop);
|
||||
return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange);
|
||||
}
|
||||
|
||||
public getVerticalOffsetForViewLine(viewLineNumber: number): number {
|
||||
return this.viewModel.viewLayout.getVerticalOffsetForLineNumber(viewLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
export class CursorState {
|
||||
_cursorStateBrand: void;
|
||||
|
||||
public static fromModelState(modelState: SingleCursorState): CursorState {
|
||||
return new CursorState(modelState, null);
|
||||
}
|
||||
|
||||
public static fromViewState(viewState: SingleCursorState): CursorState {
|
||||
return new CursorState(null, viewState);
|
||||
}
|
||||
|
||||
public static fromModelSelection(modelSelection: ISelection): CursorState {
|
||||
const selectionStartLineNumber = modelSelection.selectionStartLineNumber;
|
||||
const selectionStartColumn = modelSelection.selectionStartColumn;
|
||||
const positionLineNumber = modelSelection.positionLineNumber;
|
||||
const positionColumn = modelSelection.positionColumn;
|
||||
const modelState = new SingleCursorState(
|
||||
new Range(selectionStartLineNumber, selectionStartColumn, selectionStartLineNumber, selectionStartColumn), 0,
|
||||
new Position(positionLineNumber, positionColumn), 0
|
||||
);
|
||||
return CursorState.fromModelState(modelState);
|
||||
}
|
||||
|
||||
public static fromModelSelections(modelSelections: ISelection[]): CursorState[] {
|
||||
let states: CursorState[] = [];
|
||||
for (let i = 0, len = modelSelections.length; i < len; i++) {
|
||||
states[i] = this.fromModelSelection(modelSelections[i]);
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
public static ensureInEditableRange(context: CursorContext, states: CursorState[]): CursorState[] {
|
||||
const model = context.model;
|
||||
if (!model.hasEditableRange()) {
|
||||
return states;
|
||||
}
|
||||
|
||||
const modelEditableRange = model.getEditableRange();
|
||||
const viewEditableRange = context.convertModelRangeToViewRange(modelEditableRange);
|
||||
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = states.length; i < len; i++) {
|
||||
const state = states[i];
|
||||
|
||||
if (state.modelState) {
|
||||
const newModelState = CursorState._ensureInEditableRange(state.modelState, modelEditableRange);
|
||||
result[i] = newModelState ? CursorState.fromModelState(newModelState) : state;
|
||||
} else {
|
||||
const newViewState = CursorState._ensureInEditableRange(state.viewState, viewEditableRange);
|
||||
result[i] = newViewState ? CursorState.fromViewState(newViewState) : state;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _ensureInEditableRange(state: SingleCursorState, editableRange: Range): SingleCursorState {
|
||||
const position = state.position;
|
||||
|
||||
if (position.lineNumber < editableRange.startLineNumber || (position.lineNumber === editableRange.startLineNumber && position.column < editableRange.startColumn)) {
|
||||
return new SingleCursorState(
|
||||
state.selectionStart, state.selectionStartLeftoverVisibleColumns,
|
||||
new Position(editableRange.startLineNumber, editableRange.startColumn), 0
|
||||
);
|
||||
}
|
||||
|
||||
if (position.lineNumber > editableRange.endLineNumber || (position.lineNumber === editableRange.endLineNumber && position.column > editableRange.endColumn)) {
|
||||
return new SingleCursorState(
|
||||
state.selectionStart, state.selectionStartLeftoverVisibleColumns,
|
||||
new Position(editableRange.endLineNumber, editableRange.endColumn), 0
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
readonly modelState: SingleCursorState;
|
||||
readonly viewState: SingleCursorState;
|
||||
|
||||
constructor(modelState: SingleCursorState, viewState: SingleCursorState) {
|
||||
this.modelState = modelState;
|
||||
this.viewState = viewState;
|
||||
}
|
||||
|
||||
public equals(other: CursorState): boolean {
|
||||
return (this.viewState.equals(other.viewState) && this.modelState.equals(other.modelState));
|
||||
}
|
||||
}
|
||||
|
||||
export class EditOperationResult {
|
||||
_editOperationResultBrand: void;
|
||||
|
||||
readonly commands: ICommand[];
|
||||
readonly shouldPushStackElementBefore: boolean;
|
||||
readonly shouldPushStackElementAfter: boolean;
|
||||
|
||||
constructor(
|
||||
commands: ICommand[],
|
||||
opts: {
|
||||
shouldPushStackElementBefore: boolean;
|
||||
shouldPushStackElementAfter: boolean;
|
||||
}
|
||||
) {
|
||||
this.commands = commands;
|
||||
this.shouldPushStackElementBefore = opts.shouldPushStackElementBefore;
|
||||
this.shouldPushStackElementAfter = opts.shouldPushStackElementAfter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common operations that work and make sense both on the model and on the view model.
|
||||
*/
|
||||
export class CursorColumns {
|
||||
|
||||
public static isLowSurrogate(model: ICursorSimpleModel, lineNumber: number, charOffset: number): boolean {
|
||||
let lineContent = model.getLineContent(lineNumber);
|
||||
if (charOffset < 0 || charOffset >= lineContent.length) {
|
||||
return false;
|
||||
}
|
||||
return strings.isLowSurrogate(lineContent.charCodeAt(charOffset));
|
||||
}
|
||||
|
||||
public static isHighSurrogate(model: ICursorSimpleModel, lineNumber: number, charOffset: number): boolean {
|
||||
let lineContent = model.getLineContent(lineNumber);
|
||||
if (charOffset < 0 || charOffset >= lineContent.length) {
|
||||
return false;
|
||||
}
|
||||
return strings.isHighSurrogate(lineContent.charCodeAt(charOffset));
|
||||
}
|
||||
|
||||
public static isInsideSurrogatePair(model: ICursorSimpleModel, lineNumber: number, column: number): boolean {
|
||||
return this.isHighSurrogate(model, lineNumber, column - 2);
|
||||
}
|
||||
|
||||
public static visibleColumnFromColumn(lineContent: string, column: number, tabSize: number): number {
|
||||
let endOffset = lineContent.length;
|
||||
if (endOffset > column - 1) {
|
||||
endOffset = column - 1;
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
for (let i = 0; i < endOffset; i++) {
|
||||
let charCode = lineContent.charCodeAt(i);
|
||||
if (charCode === CharCode.Tab) {
|
||||
result = this.nextTabStop(result, tabSize);
|
||||
} else {
|
||||
result = result + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static visibleColumnFromColumn2(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): number {
|
||||
return this.visibleColumnFromColumn(model.getLineContent(position.lineNumber), position.column, config.tabSize);
|
||||
}
|
||||
|
||||
public static columnFromVisibleColumn(lineContent: string, visibleColumn: number, tabSize: number): number {
|
||||
if (visibleColumn <= 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const lineLength = lineContent.length;
|
||||
|
||||
let beforeVisibleColumn = 0;
|
||||
for (let i = 0; i < lineLength; i++) {
|
||||
let charCode = lineContent.charCodeAt(i);
|
||||
|
||||
let afterVisibleColumn: number;
|
||||
if (charCode === CharCode.Tab) {
|
||||
afterVisibleColumn = this.nextTabStop(beforeVisibleColumn, tabSize);
|
||||
} else {
|
||||
afterVisibleColumn = beforeVisibleColumn + 1;
|
||||
}
|
||||
|
||||
if (afterVisibleColumn >= visibleColumn) {
|
||||
let prevDelta = visibleColumn - beforeVisibleColumn;
|
||||
let afterDelta = afterVisibleColumn - visibleColumn;
|
||||
if (afterDelta < prevDelta) {
|
||||
return i + 2;
|
||||
} else {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
beforeVisibleColumn = afterVisibleColumn;
|
||||
}
|
||||
|
||||
// walked the entire string
|
||||
return lineLength + 1;
|
||||
}
|
||||
|
||||
public static columnFromVisibleColumn2(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, visibleColumn: number): number {
|
||||
let result = this.columnFromVisibleColumn(model.getLineContent(lineNumber), visibleColumn, config.tabSize);
|
||||
|
||||
let minColumn = model.getLineMinColumn(lineNumber);
|
||||
if (result < minColumn) {
|
||||
return minColumn;
|
||||
}
|
||||
|
||||
let maxColumn = model.getLineMaxColumn(lineNumber);
|
||||
if (result > maxColumn) {
|
||||
return maxColumn;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns)
|
||||
*/
|
||||
public static nextTabStop(visibleColumn: number, tabSize: number): number {
|
||||
return visibleColumn + tabSize - visibleColumn % tabSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns)
|
||||
*/
|
||||
public static prevTabStop(column: number, tabSize: number): number {
|
||||
return column - 1 - (column - 1) % tabSize;
|
||||
}
|
||||
}
|
||||
218
src/vs/editor/common/controller/cursorDeleteOperations.ts
Normal file
218
src/vs/editor/common/controller/cursorDeleteOperations.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand';
|
||||
import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { MoveOperations } from 'vs/editor/common/controller/cursorMoveOperations';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ICommand } from 'vs/editor/common/editorCommon';
|
||||
|
||||
export class DeleteOperations {
|
||||
|
||||
public static deleteRight(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
|
||||
let commands: ICommand[] = [];
|
||||
let shouldPushStackElementBefore = false;
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
let deleteSelection: Range = selection;
|
||||
|
||||
if (deleteSelection.isEmpty()) {
|
||||
let position = selection.getPosition();
|
||||
let rightOfPosition = MoveOperations.right(config, model, position.lineNumber, position.column);
|
||||
deleteSelection = new Range(
|
||||
rightOfPosition.lineNumber,
|
||||
rightOfPosition.column,
|
||||
position.lineNumber,
|
||||
position.column
|
||||
);
|
||||
}
|
||||
|
||||
if (deleteSelection.isEmpty()) {
|
||||
// Probably at end of file => ignore
|
||||
commands[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) {
|
||||
shouldPushStackElementBefore = true;
|
||||
}
|
||||
|
||||
commands[i] = new ReplaceCommand(deleteSelection, '');
|
||||
}
|
||||
return [shouldPushStackElementBefore, commands];
|
||||
}
|
||||
|
||||
private static _isAutoClosingPairDelete(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): boolean {
|
||||
if (!config.autoClosingBrackets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
const position = selection.getPosition();
|
||||
|
||||
if (!selection.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lineText = model.getLineContent(position.lineNumber);
|
||||
const character = lineText[position.column - 2];
|
||||
|
||||
if (!config.autoClosingPairsOpen.hasOwnProperty(character)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const afterCharacter = lineText[position.column - 1];
|
||||
const closeCharacter = config.autoClosingPairsOpen[character];
|
||||
|
||||
if (afterCharacter !== closeCharacter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _runAutoClosingPairDelete(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const position = selections[i].getPosition();
|
||||
const deleteSelection = new Range(
|
||||
position.lineNumber,
|
||||
position.column - 1,
|
||||
position.lineNumber,
|
||||
position.column + 1
|
||||
);
|
||||
commands[i] = new ReplaceCommand(deleteSelection, '');
|
||||
}
|
||||
return [true, commands];
|
||||
}
|
||||
|
||||
public static deleteLeft(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
|
||||
|
||||
if (this._isAutoClosingPairDelete(config, model, selections)) {
|
||||
return this._runAutoClosingPairDelete(config, model, selections);
|
||||
}
|
||||
|
||||
let commands: ICommand[] = [];
|
||||
let shouldPushStackElementBefore = false;
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
let deleteSelection: Range = selection;
|
||||
|
||||
if (deleteSelection.isEmpty()) {
|
||||
let position = selection.getPosition();
|
||||
|
||||
if (config.useTabStops && position.column > 1) {
|
||||
let lineContent = model.getLineContent(position.lineNumber);
|
||||
|
||||
let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
|
||||
let lastIndentationColumn = (
|
||||
firstNonWhitespaceIndex === -1
|
||||
? /* entire string is whitespace */lineContent.length + 1
|
||||
: firstNonWhitespaceIndex + 1
|
||||
);
|
||||
|
||||
if (position.column <= lastIndentationColumn) {
|
||||
let fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position);
|
||||
let toVisibleColumn = CursorColumns.prevTabStop(fromVisibleColumn, config.tabSize);
|
||||
let toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn);
|
||||
deleteSelection = new Range(position.lineNumber, toColumn, position.lineNumber, position.column);
|
||||
} else {
|
||||
deleteSelection = new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column);
|
||||
}
|
||||
} else {
|
||||
let leftOfPosition = MoveOperations.left(config, model, position.lineNumber, position.column);
|
||||
deleteSelection = new Range(
|
||||
leftOfPosition.lineNumber,
|
||||
leftOfPosition.column,
|
||||
position.lineNumber,
|
||||
position.column
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteSelection.isEmpty()) {
|
||||
// Probably at beginning of file => ignore
|
||||
commands[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) {
|
||||
shouldPushStackElementBefore = true;
|
||||
}
|
||||
|
||||
commands[i] = new ReplaceCommand(deleteSelection, '');
|
||||
}
|
||||
return [shouldPushStackElementBefore, commands];
|
||||
}
|
||||
|
||||
public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
if (config.emptySelectionClipboard) {
|
||||
// This is a full line cut
|
||||
|
||||
let position = selection.getPosition();
|
||||
|
||||
let startLineNumber: number,
|
||||
startColumn: number,
|
||||
endLineNumber: number,
|
||||
endColumn: number;
|
||||
|
||||
if (position.lineNumber < model.getLineCount()) {
|
||||
// Cutting a line in the middle of the model
|
||||
startLineNumber = position.lineNumber;
|
||||
startColumn = 1;
|
||||
endLineNumber = position.lineNumber + 1;
|
||||
endColumn = 1;
|
||||
} else if (position.lineNumber > 1) {
|
||||
// Cutting the last line & there are more than 1 lines in the model
|
||||
startLineNumber = position.lineNumber - 1;
|
||||
startColumn = model.getLineMaxColumn(position.lineNumber - 1);
|
||||
endLineNumber = position.lineNumber;
|
||||
endColumn = model.getLineMaxColumn(position.lineNumber);
|
||||
} else {
|
||||
// Cutting the single line that the model contains
|
||||
startLineNumber = position.lineNumber;
|
||||
startColumn = 1;
|
||||
endLineNumber = position.lineNumber;
|
||||
endColumn = model.getLineMaxColumn(position.lineNumber);
|
||||
}
|
||||
|
||||
let deleteSelection = new Range(
|
||||
startLineNumber,
|
||||
startColumn,
|
||||
endLineNumber,
|
||||
endColumn
|
||||
);
|
||||
|
||||
if (!deleteSelection.isEmpty()) {
|
||||
commands[i] = new ReplaceCommand(deleteSelection, '');
|
||||
} else {
|
||||
commands[i] = null;
|
||||
}
|
||||
} else {
|
||||
// Cannot cut empty selection
|
||||
commands[i] = null;
|
||||
}
|
||||
} else {
|
||||
commands[i] = new ReplaceCommand(selection, '');
|
||||
}
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
}
|
||||
85
src/vs/editor/common/controller/cursorEvents.ts
Normal file
85
src/vs/editor/common/controller/cursorEvents.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
|
||||
/**
|
||||
* Describes the reason the cursor has changed its position.
|
||||
*/
|
||||
export enum CursorChangeReason {
|
||||
/**
|
||||
* Unknown or not set.
|
||||
*/
|
||||
NotSet = 0,
|
||||
/**
|
||||
* A `model.setValue()` was called.
|
||||
*/
|
||||
ContentFlush = 1,
|
||||
/**
|
||||
* The `model` has been changed outside of this cursor and the cursor recovers its position from associated markers.
|
||||
*/
|
||||
RecoverFromMarkers = 2,
|
||||
/**
|
||||
* There was an explicit user gesture.
|
||||
*/
|
||||
Explicit = 3,
|
||||
/**
|
||||
* There was a Paste.
|
||||
*/
|
||||
Paste = 4,
|
||||
/**
|
||||
* There was an Undo.
|
||||
*/
|
||||
Undo = 5,
|
||||
/**
|
||||
* There was a Redo.
|
||||
*/
|
||||
Redo = 6,
|
||||
}
|
||||
/**
|
||||
* An event describing that the cursor position has changed.
|
||||
*/
|
||||
export interface ICursorPositionChangedEvent {
|
||||
/**
|
||||
* Primary cursor's position.
|
||||
*/
|
||||
readonly position: Position;
|
||||
/**
|
||||
* Secondary cursors' position.
|
||||
*/
|
||||
readonly secondaryPositions: Position[];
|
||||
/**
|
||||
* Reason.
|
||||
*/
|
||||
readonly reason: CursorChangeReason;
|
||||
/**
|
||||
* Source of the call that caused the event.
|
||||
*/
|
||||
readonly source: string;
|
||||
}
|
||||
/**
|
||||
* An event describing that the cursor selection has changed.
|
||||
*/
|
||||
export interface ICursorSelectionChangedEvent {
|
||||
/**
|
||||
* The primary selection.
|
||||
*/
|
||||
readonly selection: Selection;
|
||||
/**
|
||||
* The secondary selections.
|
||||
*/
|
||||
readonly secondarySelections: Selection[];
|
||||
/**
|
||||
* Source of the call that caused the event.
|
||||
*/
|
||||
readonly source: string;
|
||||
/**
|
||||
* Reason.
|
||||
*/
|
||||
readonly reason: CursorChangeReason;
|
||||
}
|
||||
762
src/vs/editor/common/controller/cursorMoveCommands.ts
Normal file
762
src/vs/editor/common/controller/cursorMoveCommands.ts
Normal file
@@ -0,0 +1,762 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { SingleCursorState, ICursorSimpleModel, CursorState, CursorContext } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { Position, IPosition } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { MoveOperations } from 'vs/editor/common/controller/cursorMoveOperations';
|
||||
import { WordOperations } from 'vs/editor/common/controller/cursorWordOperations';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
|
||||
|
||||
export class CursorMoveCommands {
|
||||
|
||||
public static addCursorDown(context: CursorContext, cursors: CursorState[]): CursorState[] {
|
||||
let result: CursorState[] = [], resultLen = 0;
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[resultLen++] = new CursorState(cursor.modelState, cursor.viewState);
|
||||
result[resultLen++] = CursorState.fromViewState(MoveOperations.translateDown(context.config, context.viewModel, cursor.viewState));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static addCursorUp(context: CursorContext, cursors: CursorState[]): CursorState[] {
|
||||
let result: CursorState[] = [], resultLen = 0;
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[resultLen++] = new CursorState(cursor.modelState, cursor.viewState);
|
||||
result[resultLen++] = CursorState.fromViewState(MoveOperations.translateUp(context.config, context.viewModel, cursor.viewState));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static moveToBeginningOfLine(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = this._moveToLineStart(context, cursor, inSelectionMode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToLineStart(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): CursorState {
|
||||
const currentViewStateColumn = cursor.viewState.position.column;
|
||||
const currentModelStateColumn = cursor.modelState.position.column;
|
||||
const isFirstLineOfWrappedLine = currentViewStateColumn === currentModelStateColumn;
|
||||
|
||||
const currentViewStatelineNumber = cursor.viewState.position.lineNumber;
|
||||
const firstNonBlankColumn = context.viewModel.getLineFirstNonWhitespaceColumn(currentViewStatelineNumber);
|
||||
const isBeginningOfViewLine = currentViewStateColumn === firstNonBlankColumn;
|
||||
|
||||
if (!isFirstLineOfWrappedLine && !isBeginningOfViewLine) {
|
||||
return this._moveToLineStartByView(context, cursor, inSelectionMode);
|
||||
} else {
|
||||
return this._moveToLineStartByModel(context, cursor, inSelectionMode);
|
||||
}
|
||||
}
|
||||
|
||||
private static _moveToLineStartByView(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): CursorState {
|
||||
return CursorState.fromViewState(
|
||||
MoveOperations.moveToBeginningOfLine(context.config, context.viewModel, cursor.viewState, inSelectionMode)
|
||||
);
|
||||
}
|
||||
|
||||
private static _moveToLineStartByModel(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): CursorState {
|
||||
return CursorState.fromModelState(
|
||||
MoveOperations.moveToBeginningOfLine(context.config, context.model, cursor.modelState, inSelectionMode)
|
||||
);
|
||||
}
|
||||
|
||||
public static moveToEndOfLine(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = this._moveToLineEnd(context, cursor, inSelectionMode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToLineEnd(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): CursorState {
|
||||
const viewStatePosition = cursor.viewState.position;
|
||||
const viewModelMaxColumn = context.viewModel.getLineMaxColumn(viewStatePosition.lineNumber);
|
||||
const isEndOfViewLine = viewStatePosition.column === viewModelMaxColumn;
|
||||
|
||||
const modelStatePosition = cursor.modelState.position;
|
||||
const modelMaxColumn = context.model.getLineMaxColumn(modelStatePosition.lineNumber);
|
||||
const isEndLineOfWrappedLine = viewModelMaxColumn - viewStatePosition.column === modelMaxColumn - modelStatePosition.column;
|
||||
|
||||
if (isEndOfViewLine || isEndLineOfWrappedLine) {
|
||||
return this._moveToLineEndByModel(context, cursor, inSelectionMode);
|
||||
} else {
|
||||
return this._moveToLineEndByView(context, cursor, inSelectionMode);
|
||||
}
|
||||
}
|
||||
|
||||
private static _moveToLineEndByView(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): CursorState {
|
||||
return CursorState.fromViewState(
|
||||
MoveOperations.moveToEndOfLine(context.config, context.viewModel, cursor.viewState, inSelectionMode)
|
||||
);
|
||||
}
|
||||
|
||||
private static _moveToLineEndByModel(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): CursorState {
|
||||
return CursorState.fromModelState(
|
||||
MoveOperations.moveToEndOfLine(context.config, context.model, cursor.modelState, inSelectionMode)
|
||||
);
|
||||
}
|
||||
|
||||
public static expandLineSelection(context: CursorContext, cursors: CursorState[]): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
|
||||
const viewSelection = cursor.viewState.selection;
|
||||
const startLineNumber = viewSelection.startLineNumber;
|
||||
const lineCount = context.viewModel.getLineCount();
|
||||
|
||||
let endLineNumber = viewSelection.endLineNumber;
|
||||
let endColumn: number;
|
||||
if (endLineNumber === lineCount) {
|
||||
endColumn = context.viewModel.getLineMaxColumn(lineCount);
|
||||
} else {
|
||||
endLineNumber++;
|
||||
endColumn = 1;
|
||||
}
|
||||
|
||||
result[i] = CursorState.fromViewState(new SingleCursorState(
|
||||
new Range(startLineNumber, 1, startLineNumber, 1), 0,
|
||||
new Position(endLineNumber, endColumn), 0
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static moveToBeginningOfBuffer(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromModelState(MoveOperations.moveToBeginningOfBuffer(context.config, context.model, cursor.modelState, inSelectionMode));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static moveToEndOfBuffer(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromModelState(MoveOperations.moveToEndOfBuffer(context.config, context.model, cursor.modelState, inSelectionMode));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static selectAll(context: CursorContext, cursor: CursorState): CursorState {
|
||||
|
||||
if (context.model.hasEditableRange()) {
|
||||
// Toggle between selecting editable range and selecting the entire buffer
|
||||
|
||||
const editableRange = context.model.getEditableRange();
|
||||
const selection = cursor.modelState.selection;
|
||||
|
||||
if (!selection.equalsRange(editableRange)) {
|
||||
// Selection is not editable range => select editable range
|
||||
return CursorState.fromModelState(new SingleCursorState(
|
||||
new Range(editableRange.startLineNumber, editableRange.startColumn, editableRange.startLineNumber, editableRange.startColumn), 0,
|
||||
new Position(editableRange.endLineNumber, editableRange.endColumn), 0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const lineCount = context.model.getLineCount();
|
||||
const maxColumn = context.model.getLineMaxColumn(lineCount);
|
||||
|
||||
return CursorState.fromModelState(new SingleCursorState(
|
||||
new Range(1, 1, 1, 1), 0,
|
||||
new Position(lineCount, maxColumn), 0
|
||||
));
|
||||
}
|
||||
|
||||
public static line(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): CursorState {
|
||||
const position = context.model.validatePosition(_position);
|
||||
const viewPosition = (
|
||||
_viewPosition
|
||||
? context.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position)
|
||||
: context.convertModelPositionToViewPosition(position)
|
||||
);
|
||||
|
||||
if (!inSelectionMode || !cursor.modelState.hasSelection()) {
|
||||
// Entering line selection for the first time
|
||||
const lineCount = context.model.getLineCount();
|
||||
|
||||
let selectToLineNumber = position.lineNumber + 1;
|
||||
let selectToColumn = 1;
|
||||
if (selectToLineNumber > lineCount) {
|
||||
selectToLineNumber = lineCount;
|
||||
selectToColumn = context.model.getLineMaxColumn(selectToLineNumber);
|
||||
}
|
||||
|
||||
return CursorState.fromModelState(new SingleCursorState(
|
||||
new Range(position.lineNumber, 1, selectToLineNumber, selectToColumn), 0,
|
||||
new Position(selectToLineNumber, selectToColumn), 0
|
||||
));
|
||||
}
|
||||
|
||||
// Continuing line selection
|
||||
const enteringLineNumber = cursor.modelState.selectionStart.getStartPosition().lineNumber;
|
||||
|
||||
if (position.lineNumber < enteringLineNumber) {
|
||||
|
||||
return CursorState.fromViewState(cursor.viewState.move(
|
||||
cursor.modelState.hasSelection(), viewPosition.lineNumber, 1, 0
|
||||
));
|
||||
|
||||
} else if (position.lineNumber > enteringLineNumber) {
|
||||
|
||||
const lineCount = context.viewModel.getLineCount();
|
||||
|
||||
let selectToViewLineNumber = viewPosition.lineNumber + 1;
|
||||
let selectToViewColumn = 1;
|
||||
if (selectToViewLineNumber > lineCount) {
|
||||
selectToViewLineNumber = lineCount;
|
||||
selectToViewColumn = context.viewModel.getLineMaxColumn(selectToViewLineNumber);
|
||||
}
|
||||
|
||||
return CursorState.fromViewState(cursor.viewState.move(
|
||||
cursor.modelState.hasSelection(), selectToViewLineNumber, selectToViewColumn, 0
|
||||
));
|
||||
|
||||
} else {
|
||||
|
||||
const endPositionOfSelectionStart = cursor.modelState.selectionStart.getEndPosition();
|
||||
return CursorState.fromModelState(cursor.modelState.move(
|
||||
cursor.modelState.hasSelection(), endPositionOfSelectionStart.lineNumber, endPositionOfSelectionStart.column, 0
|
||||
));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static word(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, _position: IPosition): CursorState {
|
||||
const position = context.model.validatePosition(_position);
|
||||
return CursorState.fromModelState(WordOperations.word(context.config, context.model, cursor.modelState, inSelectionMode, position));
|
||||
}
|
||||
|
||||
public static cancelSelection(context: CursorContext, cursor: CursorState): CursorState {
|
||||
if (!cursor.modelState.hasSelection()) {
|
||||
return new CursorState(cursor.modelState, cursor.viewState);
|
||||
}
|
||||
|
||||
const lineNumber = cursor.viewState.position.lineNumber;
|
||||
const column = cursor.viewState.position.column;
|
||||
|
||||
return CursorState.fromViewState(new SingleCursorState(
|
||||
new Range(lineNumber, column, lineNumber, column), 0,
|
||||
new Position(lineNumber, column), 0
|
||||
));
|
||||
}
|
||||
|
||||
public static moveTo(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): CursorState {
|
||||
const position = context.model.validatePosition(_position);
|
||||
const viewPosition = (
|
||||
_viewPosition
|
||||
? context.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position)
|
||||
: context.convertModelPositionToViewPosition(position)
|
||||
);
|
||||
return CursorState.fromViewState(cursor.viewState.move(inSelectionMode, viewPosition.lineNumber, viewPosition.column, 0));
|
||||
}
|
||||
|
||||
public static move(context: CursorContext, cursors: CursorState[], args: CursorMove.ParsedArguments): CursorState[] {
|
||||
const inSelectionMode = args.select;
|
||||
const value = args.value;
|
||||
|
||||
switch (args.direction) {
|
||||
case CursorMove.Direction.Left: {
|
||||
if (args.unit === CursorMove.Unit.HalfLine) {
|
||||
// Move left by half the current line length
|
||||
return this._moveHalfLineLeft(context, cursors, inSelectionMode);
|
||||
} else {
|
||||
// Move left by `moveParams.value` columns
|
||||
return this._moveLeft(context, cursors, inSelectionMode, value);
|
||||
}
|
||||
}
|
||||
case CursorMove.Direction.Right: {
|
||||
if (args.unit === CursorMove.Unit.HalfLine) {
|
||||
// Move right by half the current line length
|
||||
return this._moveHalfLineRight(context, cursors, inSelectionMode);
|
||||
} else {
|
||||
// Move right by `moveParams.value` columns
|
||||
return this._moveRight(context, cursors, inSelectionMode, value);
|
||||
}
|
||||
}
|
||||
case CursorMove.Direction.Up: {
|
||||
if (args.unit === CursorMove.Unit.WrappedLine) {
|
||||
// Move up by view lines
|
||||
return this._moveUpByViewLines(context, cursors, inSelectionMode, value);
|
||||
} else {
|
||||
// Move up by model lines
|
||||
return this._moveUpByModelLines(context, cursors, inSelectionMode, value);
|
||||
}
|
||||
}
|
||||
case CursorMove.Direction.Down: {
|
||||
if (args.unit === CursorMove.Unit.WrappedLine) {
|
||||
// Move down by view lines
|
||||
return this._moveDownByViewLines(context, cursors, inSelectionMode, value);
|
||||
} else {
|
||||
// Move down by model lines
|
||||
return this._moveDownByModelLines(context, cursors, inSelectionMode, value);
|
||||
}
|
||||
}
|
||||
case CursorMove.Direction.WrappedLineStart: {
|
||||
// Move to the beginning of the current view line
|
||||
return this._moveToViewMinColumn(context, cursors, inSelectionMode);
|
||||
}
|
||||
case CursorMove.Direction.WrappedLineFirstNonWhitespaceCharacter: {
|
||||
// Move to the first non-whitespace column of the current view line
|
||||
return this._moveToViewFirstNonWhitespaceColumn(context, cursors, inSelectionMode);
|
||||
}
|
||||
case CursorMove.Direction.WrappedLineColumnCenter: {
|
||||
// Move to the "center" of the current view line
|
||||
return this._moveToViewCenterColumn(context, cursors, inSelectionMode);
|
||||
}
|
||||
case CursorMove.Direction.WrappedLineEnd: {
|
||||
// Move to the end of the current view line
|
||||
return this._moveToViewMaxColumn(context, cursors, inSelectionMode);
|
||||
}
|
||||
case CursorMove.Direction.WrappedLineLastNonWhitespaceCharacter: {
|
||||
// Move to the last non-whitespace column of the current view line
|
||||
return this._moveToViewLastNonWhitespaceColumn(context, cursors, inSelectionMode);
|
||||
}
|
||||
case CursorMove.Direction.ViewPortTop: {
|
||||
// Move to the nth line start in the viewport (from the top)
|
||||
const cursor = cursors[0];
|
||||
const visibleModelRange = context.getCompletelyVisibleModelRange();
|
||||
const modelLineNumber = this._firstLineNumberInRange(context.model, visibleModelRange, value);
|
||||
const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber);
|
||||
return [this._moveToModelPosition(context, cursor, inSelectionMode, modelLineNumber, modelColumn)];
|
||||
}
|
||||
case CursorMove.Direction.ViewPortBottom: {
|
||||
// Move to the nth line start in the viewport (from the bottom)
|
||||
const cursor = cursors[0];
|
||||
const visibleModelRange = context.getCompletelyVisibleModelRange();
|
||||
const modelLineNumber = this._lastLineNumberInRange(context.model, visibleModelRange, value);
|
||||
const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber);
|
||||
return [this._moveToModelPosition(context, cursor, inSelectionMode, modelLineNumber, modelColumn)];
|
||||
}
|
||||
case CursorMove.Direction.ViewPortCenter: {
|
||||
// Move to the line start in the viewport center
|
||||
const cursor = cursors[0];
|
||||
const visibleModelRange = context.getCompletelyVisibleModelRange();
|
||||
const modelLineNumber = Math.round((visibleModelRange.startLineNumber + visibleModelRange.endLineNumber) / 2);
|
||||
const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber);
|
||||
return [this._moveToModelPosition(context, cursor, inSelectionMode, modelLineNumber, modelColumn)];
|
||||
}
|
||||
case CursorMove.Direction.ViewPortIfOutside: {
|
||||
// Move to a position inside the viewport
|
||||
const visibleViewRange = context.getCompletelyVisibleViewRange();
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = this.findPositionInViewportIfOutside(context, cursor, visibleViewRange, inSelectionMode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static findPositionInViewportIfOutside(context: CursorContext, cursor: CursorState, visibleViewRange: Range, inSelectionMode: boolean): CursorState {
|
||||
let viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
|
||||
if (visibleViewRange.startLineNumber <= viewLineNumber && viewLineNumber <= visibleViewRange.endLineNumber - 1) {
|
||||
// Nothing to do, cursor is in viewport
|
||||
return new CursorState(cursor.modelState, cursor.viewState);
|
||||
|
||||
} else {
|
||||
if (viewLineNumber > visibleViewRange.endLineNumber - 1) {
|
||||
viewLineNumber = visibleViewRange.endLineNumber - 1;
|
||||
}
|
||||
if (viewLineNumber < visibleViewRange.startLineNumber) {
|
||||
viewLineNumber = visibleViewRange.startLineNumber;
|
||||
}
|
||||
const viewColumn = context.viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber);
|
||||
return this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nth line start included in the range (from the start).
|
||||
*/
|
||||
private static _firstLineNumberInRange(model: ICursorSimpleModel, range: Range, count: number): number {
|
||||
let startLineNumber = range.startLineNumber;
|
||||
if (range.startColumn !== model.getLineMinColumn(startLineNumber)) {
|
||||
// Move on to the second line if the first line start is not included in the range
|
||||
startLineNumber++;
|
||||
}
|
||||
|
||||
return Math.min(range.endLineNumber, startLineNumber + count - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nth line start included in the range (from the end).
|
||||
*/
|
||||
private static _lastLineNumberInRange(model: ICursorSimpleModel, range: Range, count: number): number {
|
||||
let startLineNumber = range.startLineNumber;
|
||||
if (range.startColumn !== model.getLineMinColumn(startLineNumber)) {
|
||||
// Move on to the second line if the first line start is not included in the range
|
||||
startLineNumber++;
|
||||
}
|
||||
|
||||
return Math.max(startLineNumber, range.endLineNumber - count + 1);
|
||||
}
|
||||
|
||||
private static _moveLeft(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromViewState(MoveOperations.moveLeft(context.config, context.viewModel, cursor.viewState, inSelectionMode, noOfColumns));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveHalfLineLeft(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const halfLine = Math.round(context.viewModel.getLineContent(viewLineNumber).length / 2);
|
||||
result[i] = CursorState.fromViewState(MoveOperations.moveLeft(context.config, context.viewModel, cursor.viewState, inSelectionMode, halfLine));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveRight(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromViewState(MoveOperations.moveRight(context.config, context.viewModel, cursor.viewState, inSelectionMode, noOfColumns));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveHalfLineRight(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const halfLine = Math.round(context.viewModel.getLineContent(viewLineNumber).length / 2);
|
||||
result[i] = CursorState.fromViewState(MoveOperations.moveRight(context.config, context.viewModel, cursor.viewState, inSelectionMode, halfLine));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveDownByViewLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromViewState(MoveOperations.moveDown(context.config, context.viewModel, cursor.viewState, inSelectionMode, linesCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveDownByModelLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromModelState(MoveOperations.moveDown(context.config, context.model, cursor.modelState, inSelectionMode, linesCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveUpByViewLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromViewState(MoveOperations.moveUp(context.config, context.viewModel, cursor.viewState, inSelectionMode, linesCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveUpByModelLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
result[i] = CursorState.fromModelState(MoveOperations.moveUp(context.config, context.model, cursor.modelState, inSelectionMode, linesCount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToViewPosition(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, toViewLineNumber: number, toViewColumn: number): CursorState {
|
||||
return CursorState.fromViewState(cursor.viewState.move(inSelectionMode, toViewLineNumber, toViewColumn, 0));
|
||||
}
|
||||
|
||||
private static _moveToModelPosition(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, toModelLineNumber: number, toModelColumn: number): CursorState {
|
||||
return CursorState.fromModelState(cursor.modelState.move(inSelectionMode, toModelLineNumber, toModelColumn, 0));
|
||||
}
|
||||
|
||||
private static _moveToViewMinColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const viewColumn = context.viewModel.getLineMinColumn(viewLineNumber);
|
||||
result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToViewFirstNonWhitespaceColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const viewColumn = context.viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber);
|
||||
result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToViewCenterColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const viewColumn = Math.round((context.viewModel.getLineMaxColumn(viewLineNumber) + context.viewModel.getLineMinColumn(viewLineNumber)) / 2);
|
||||
result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToViewMaxColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const viewColumn = context.viewModel.getLineMaxColumn(viewLineNumber);
|
||||
result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _moveToViewLastNonWhitespaceColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): CursorState[] {
|
||||
let result: CursorState[] = [];
|
||||
for (let i = 0, len = cursors.length; i < len; i++) {
|
||||
const cursor = cursors[i];
|
||||
const viewLineNumber = cursor.viewState.position.lineNumber;
|
||||
const viewColumn = context.viewModel.getLineLastNonWhitespaceColumn(viewLineNumber);
|
||||
result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CursorMove {
|
||||
|
||||
const isCursorMoveArgs = function (arg: any): boolean {
|
||||
if (!types.isObject(arg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let cursorMoveArg: RawArguments = arg;
|
||||
|
||||
if (!types.isString(cursorMoveArg.to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!types.isUndefined(cursorMoveArg.select) && !types.isBoolean(cursorMoveArg.select)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!types.isUndefined(cursorMoveArg.by) && !types.isString(cursorMoveArg.by)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!types.isUndefined(cursorMoveArg.value) && !types.isNumber(cursorMoveArg.value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const description = <ICommandHandlerDescription>{
|
||||
description: 'Move cursor to a logical position in the view',
|
||||
args: [
|
||||
{
|
||||
name: 'Cursor move argument object',
|
||||
description: `Property-value pairs that can be passed through this argument:
|
||||
* 'to': A mandatory logical position value providing where to move the cursor.
|
||||
\`\`\`
|
||||
'left', 'right', 'up', 'down'
|
||||
'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter'
|
||||
'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter',
|
||||
'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside'
|
||||
\`\`\`
|
||||
* 'by': Unit to move. Default is computed based on 'to' value.
|
||||
\`\`\`
|
||||
'line', 'wrappedLine', 'character', 'halfLine'
|
||||
\`\`\`
|
||||
* 'value': Number of units to move. Default is '1'.
|
||||
* 'select': If 'true' makes the selection. Default is 'false'.
|
||||
`,
|
||||
constraint: isCursorMoveArgs
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Positions in the view for cursor move command.
|
||||
*/
|
||||
export const RawDirection = {
|
||||
Left: 'left',
|
||||
Right: 'right',
|
||||
Up: 'up',
|
||||
Down: 'down',
|
||||
|
||||
WrappedLineStart: 'wrappedLineStart',
|
||||
WrappedLineFirstNonWhitespaceCharacter: 'wrappedLineFirstNonWhitespaceCharacter',
|
||||
WrappedLineColumnCenter: 'wrappedLineColumnCenter',
|
||||
WrappedLineEnd: 'wrappedLineEnd',
|
||||
WrappedLineLastNonWhitespaceCharacter: 'wrappedLineLastNonWhitespaceCharacter',
|
||||
|
||||
ViewPortTop: 'viewPortTop',
|
||||
ViewPortCenter: 'viewPortCenter',
|
||||
ViewPortBottom: 'viewPortBottom',
|
||||
|
||||
ViewPortIfOutside: 'viewPortIfOutside'
|
||||
};
|
||||
|
||||
/**
|
||||
* Units for Cursor move 'by' argument
|
||||
*/
|
||||
export const RawUnit = {
|
||||
Line: 'line',
|
||||
WrappedLine: 'wrappedLine',
|
||||
Character: 'character',
|
||||
HalfLine: 'halfLine'
|
||||
};
|
||||
|
||||
/**
|
||||
* Arguments for Cursor move command
|
||||
*/
|
||||
export interface RawArguments {
|
||||
to: string;
|
||||
select?: boolean;
|
||||
by?: string;
|
||||
value?: number;
|
||||
};
|
||||
|
||||
export function parse(args: RawArguments): ParsedArguments {
|
||||
if (!args.to) {
|
||||
// illegal arguments
|
||||
return null;
|
||||
}
|
||||
|
||||
let direction: Direction;
|
||||
switch (args.to) {
|
||||
case RawDirection.Left:
|
||||
direction = Direction.Left;
|
||||
break;
|
||||
case RawDirection.Right:
|
||||
direction = Direction.Right;
|
||||
break;
|
||||
case RawDirection.Up:
|
||||
direction = Direction.Up;
|
||||
break;
|
||||
case RawDirection.Down:
|
||||
direction = Direction.Down;
|
||||
break;
|
||||
case RawDirection.WrappedLineStart:
|
||||
direction = Direction.WrappedLineStart;
|
||||
break;
|
||||
case RawDirection.WrappedLineFirstNonWhitespaceCharacter:
|
||||
direction = Direction.WrappedLineFirstNonWhitespaceCharacter;
|
||||
break;
|
||||
case RawDirection.WrappedLineColumnCenter:
|
||||
direction = Direction.WrappedLineColumnCenter;
|
||||
break;
|
||||
case RawDirection.WrappedLineEnd:
|
||||
direction = Direction.WrappedLineEnd;
|
||||
break;
|
||||
case RawDirection.WrappedLineLastNonWhitespaceCharacter:
|
||||
direction = Direction.WrappedLineLastNonWhitespaceCharacter;
|
||||
break;
|
||||
case RawDirection.ViewPortTop:
|
||||
direction = Direction.ViewPortTop;
|
||||
break;
|
||||
case RawDirection.ViewPortBottom:
|
||||
direction = Direction.ViewPortBottom;
|
||||
break;
|
||||
case RawDirection.ViewPortCenter:
|
||||
direction = Direction.ViewPortCenter;
|
||||
break;
|
||||
case RawDirection.ViewPortIfOutside:
|
||||
direction = Direction.ViewPortIfOutside;
|
||||
break;
|
||||
default:
|
||||
// illegal arguments
|
||||
return null;
|
||||
}
|
||||
|
||||
let unit = Unit.None;
|
||||
switch (args.by) {
|
||||
case RawUnit.Line:
|
||||
unit = Unit.Line;
|
||||
break;
|
||||
case RawUnit.WrappedLine:
|
||||
unit = Unit.WrappedLine;
|
||||
break;
|
||||
case RawUnit.Character:
|
||||
unit = Unit.Character;
|
||||
break;
|
||||
case RawUnit.HalfLine:
|
||||
unit = Unit.HalfLine;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
direction: direction,
|
||||
unit: unit,
|
||||
select: (!!args.select),
|
||||
value: (args.value || 1)
|
||||
};
|
||||
}
|
||||
|
||||
export interface ParsedArguments {
|
||||
direction: Direction;
|
||||
unit: Unit;
|
||||
select: boolean;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export const enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
|
||||
WrappedLineStart,
|
||||
WrappedLineFirstNonWhitespaceCharacter,
|
||||
WrappedLineColumnCenter,
|
||||
WrappedLineEnd,
|
||||
WrappedLineLastNonWhitespaceCharacter,
|
||||
|
||||
ViewPortTop,
|
||||
ViewPortCenter,
|
||||
ViewPortBottom,
|
||||
|
||||
ViewPortIfOutside,
|
||||
};
|
||||
|
||||
export const enum Unit {
|
||||
None,
|
||||
Line,
|
||||
WrappedLine,
|
||||
Character,
|
||||
HalfLine,
|
||||
};
|
||||
|
||||
}
|
||||
246
src/vs/editor/common/controller/cursorMoveOperations.ts
Normal file
246
src/vs/editor/common/controller/cursorMoveOperations.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { SingleCursorState, CursorColumns, CursorConfiguration, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
export class CursorPosition {
|
||||
_cursorPositionBrand: void;
|
||||
|
||||
public readonly lineNumber: number;
|
||||
public readonly column: number;
|
||||
public readonly leftoverVisibleColumns: number;
|
||||
|
||||
constructor(lineNumber: number, column: number, leftoverVisibleColumns: number) {
|
||||
this.lineNumber = lineNumber;
|
||||
this.column = column;
|
||||
this.leftoverVisibleColumns = leftoverVisibleColumns;
|
||||
}
|
||||
}
|
||||
|
||||
export class MoveOperations {
|
||||
|
||||
public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition {
|
||||
|
||||
if (column > model.getLineMinColumn(lineNumber)) {
|
||||
if (CursorColumns.isLowSurrogate(model, lineNumber, column - 2)) {
|
||||
// character before column is a low surrogate
|
||||
column = column - 2;
|
||||
} else {
|
||||
column = column - 1;
|
||||
}
|
||||
} else if (lineNumber > 1) {
|
||||
lineNumber = lineNumber - 1;
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
|
||||
return new CursorPosition(lineNumber, column, 0);
|
||||
}
|
||||
|
||||
public static moveLeft(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState {
|
||||
let lineNumber: number,
|
||||
column: number;
|
||||
|
||||
if (cursor.hasSelection() && !inSelectionMode) {
|
||||
// If we are in selection mode, move left without selection cancels selection and puts cursor at the beginning of the selection
|
||||
lineNumber = cursor.selection.startLineNumber;
|
||||
column = cursor.selection.startColumn;
|
||||
} else {
|
||||
let r = MoveOperations.left(config, model, cursor.position.lineNumber, cursor.position.column - (noOfColumns - 1));
|
||||
lineNumber = r.lineNumber;
|
||||
column = r.column;
|
||||
}
|
||||
|
||||
return cursor.move(inSelectionMode, lineNumber, column, 0);
|
||||
}
|
||||
|
||||
public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition {
|
||||
|
||||
if (column < model.getLineMaxColumn(lineNumber)) {
|
||||
if (CursorColumns.isHighSurrogate(model, lineNumber, column - 1)) {
|
||||
// character after column is a high surrogate
|
||||
column = column + 2;
|
||||
} else {
|
||||
column = column + 1;
|
||||
}
|
||||
} else if (lineNumber < model.getLineCount()) {
|
||||
lineNumber = lineNumber + 1;
|
||||
column = model.getLineMinColumn(lineNumber);
|
||||
}
|
||||
|
||||
return new CursorPosition(lineNumber, column, 0);
|
||||
}
|
||||
|
||||
public static moveRight(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState {
|
||||
let lineNumber: number,
|
||||
column: number;
|
||||
|
||||
if (cursor.hasSelection() && !inSelectionMode) {
|
||||
// If we are in selection mode, move right without selection cancels selection and puts cursor at the end of the selection
|
||||
lineNumber = cursor.selection.endLineNumber;
|
||||
column = cursor.selection.endColumn;
|
||||
} else {
|
||||
let r = MoveOperations.right(config, model, cursor.position.lineNumber, cursor.position.column + (noOfColumns - 1));
|
||||
lineNumber = r.lineNumber;
|
||||
column = r.column;
|
||||
}
|
||||
|
||||
return cursor.move(inSelectionMode, lineNumber, column, 0);
|
||||
}
|
||||
|
||||
public static down(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnLastLine: boolean): CursorPosition {
|
||||
const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns;
|
||||
|
||||
lineNumber = lineNumber + count;
|
||||
var lineCount = model.getLineCount();
|
||||
if (lineNumber > lineCount) {
|
||||
lineNumber = lineCount;
|
||||
if (allowMoveOnLastLine) {
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
} else {
|
||||
column = Math.min(model.getLineMaxColumn(lineNumber), column);
|
||||
if (CursorColumns.isInsideSurrogatePair(model, lineNumber, column)) {
|
||||
column = column - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
column = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, currentVisibleColumn);
|
||||
if (CursorColumns.isInsideSurrogatePair(model, lineNumber, column)) {
|
||||
column = column - 1;
|
||||
}
|
||||
}
|
||||
|
||||
leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize);
|
||||
|
||||
return new CursorPosition(lineNumber, column, leftoverVisibleColumns);
|
||||
}
|
||||
|
||||
public static moveDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState {
|
||||
let lineNumber: number,
|
||||
column: number;
|
||||
|
||||
if (cursor.hasSelection() && !inSelectionMode) {
|
||||
// If we are in selection mode, move down acts relative to the end of selection
|
||||
lineNumber = cursor.selection.endLineNumber;
|
||||
column = cursor.selection.endColumn;
|
||||
} else {
|
||||
lineNumber = cursor.position.lineNumber;
|
||||
column = cursor.position.column;
|
||||
}
|
||||
|
||||
let r = MoveOperations.down(config, model, lineNumber, column, cursor.leftoverVisibleColumns, linesCount, true);
|
||||
|
||||
return cursor.move(inSelectionMode, r.lineNumber, r.column, r.leftoverVisibleColumns);
|
||||
}
|
||||
|
||||
public static translateDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState): SingleCursorState {
|
||||
let selection = cursor.selection;
|
||||
|
||||
let selectionStart = MoveOperations.down(config, model, selection.selectionStartLineNumber, selection.selectionStartColumn, cursor.selectionStartLeftoverVisibleColumns, 1, false);
|
||||
let position = MoveOperations.down(config, model, selection.positionLineNumber, selection.positionColumn, cursor.leftoverVisibleColumns, 1, false);
|
||||
|
||||
return new SingleCursorState(
|
||||
new Range(selectionStart.lineNumber, selectionStart.column, selectionStart.lineNumber, selectionStart.column),
|
||||
selectionStart.leftoverVisibleColumns,
|
||||
new Position(position.lineNumber, position.column),
|
||||
position.leftoverVisibleColumns
|
||||
);
|
||||
}
|
||||
|
||||
public static up(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnFirstLine: boolean): CursorPosition {
|
||||
const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns;
|
||||
|
||||
lineNumber = lineNumber - count;
|
||||
if (lineNumber < 1) {
|
||||
lineNumber = 1;
|
||||
if (allowMoveOnFirstLine) {
|
||||
column = model.getLineMinColumn(lineNumber);
|
||||
} else {
|
||||
column = Math.min(model.getLineMaxColumn(lineNumber), column);
|
||||
if (CursorColumns.isInsideSurrogatePair(model, lineNumber, column)) {
|
||||
column = column - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
column = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, currentVisibleColumn);
|
||||
if (CursorColumns.isInsideSurrogatePair(model, lineNumber, column)) {
|
||||
column = column - 1;
|
||||
}
|
||||
}
|
||||
|
||||
leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize);
|
||||
|
||||
return new CursorPosition(lineNumber, column, leftoverVisibleColumns);
|
||||
}
|
||||
|
||||
public static moveUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState {
|
||||
let lineNumber: number,
|
||||
column: number;
|
||||
|
||||
if (cursor.hasSelection() && !inSelectionMode) {
|
||||
// If we are in selection mode, move up acts relative to the beginning of selection
|
||||
lineNumber = cursor.selection.startLineNumber;
|
||||
column = cursor.selection.startColumn;
|
||||
} else {
|
||||
lineNumber = cursor.position.lineNumber;
|
||||
column = cursor.position.column;
|
||||
}
|
||||
|
||||
let r = MoveOperations.up(config, model, lineNumber, column, cursor.leftoverVisibleColumns, linesCount, true);
|
||||
|
||||
return cursor.move(inSelectionMode, r.lineNumber, r.column, r.leftoverVisibleColumns);
|
||||
}
|
||||
|
||||
public static translateUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState): SingleCursorState {
|
||||
|
||||
let selection = cursor.selection;
|
||||
|
||||
let selectionStart = MoveOperations.up(config, model, selection.selectionStartLineNumber, selection.selectionStartColumn, cursor.selectionStartLeftoverVisibleColumns, 1, false);
|
||||
let position = MoveOperations.up(config, model, selection.positionLineNumber, selection.positionColumn, cursor.leftoverVisibleColumns, 1, false);
|
||||
|
||||
return new SingleCursorState(
|
||||
new Range(selectionStart.lineNumber, selectionStart.column, selectionStart.lineNumber, selectionStart.column),
|
||||
selectionStart.leftoverVisibleColumns,
|
||||
new Position(position.lineNumber, position.column),
|
||||
position.leftoverVisibleColumns
|
||||
);
|
||||
}
|
||||
|
||||
public static moveToBeginningOfLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {
|
||||
let lineNumber = cursor.position.lineNumber;
|
||||
let minColumn = model.getLineMinColumn(lineNumber);
|
||||
let firstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(lineNumber) || minColumn;
|
||||
|
||||
let column: number;
|
||||
|
||||
let relevantColumnNumber = cursor.position.column;
|
||||
if (relevantColumnNumber === firstNonBlankColumn) {
|
||||
column = minColumn;
|
||||
} else {
|
||||
column = firstNonBlankColumn;
|
||||
}
|
||||
|
||||
return cursor.move(inSelectionMode, lineNumber, column, 0);
|
||||
}
|
||||
|
||||
public static moveToEndOfLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {
|
||||
let lineNumber = cursor.position.lineNumber;
|
||||
let maxColumn = model.getLineMaxColumn(lineNumber);
|
||||
return cursor.move(inSelectionMode, lineNumber, maxColumn, 0);
|
||||
}
|
||||
|
||||
public static moveToBeginningOfBuffer(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {
|
||||
return cursor.move(inSelectionMode, 1, 1, 0);
|
||||
}
|
||||
|
||||
public static moveToEndOfBuffer(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {
|
||||
let lastLineNumber = model.getLineCount();
|
||||
let lastColumn = model.getLineMaxColumn(lastLineNumber);
|
||||
|
||||
return cursor.move(inSelectionMode, lastLineNumber, lastColumn, 0);
|
||||
}
|
||||
}
|
||||
775
src/vs/editor/common/controller/cursorTypeOperations.ts
Normal file
775
src/vs/editor/common/controller/cursorTypeOperations.ts
Normal file
@@ -0,0 +1,775 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ReplaceCommand, ReplaceCommandWithoutChangingPosition, ReplaceCommandWithOffsetCursorState } from 'vs/editor/common/commands/replaceCommand';
|
||||
import { CursorColumns, CursorConfiguration, ICursorSimpleModel, EditOperationResult } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICommand, ITokenizedModel } from 'vs/editor/common/editorCommon';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { IndentAction } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { SurroundSelectionCommand } from 'vs/editor/common/commands/surroundSelectionCommand';
|
||||
import { IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter';
|
||||
import { getMapForWordSeparators, WordCharacterClass } from 'vs/editor/common/controller/wordCharacterClassifier';
|
||||
|
||||
export class TypeOperations {
|
||||
|
||||
public static indent(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): ICommand[] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
commands[i] = new ShiftCommand(selections[i], {
|
||||
isUnshift: false,
|
||||
tabSize: config.tabSize,
|
||||
oneIndent: config.oneIndent,
|
||||
useTabStops: config.useTabStops
|
||||
});
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
public static outdent(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): ICommand[] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
commands[i] = new ShiftCommand(selections[i], {
|
||||
isUnshift: true,
|
||||
tabSize: config.tabSize,
|
||||
oneIndent: config.oneIndent,
|
||||
useTabStops: config.useTabStops
|
||||
});
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
public static shiftIndent(config: CursorConfiguration, indentation: string, count?: number): string {
|
||||
count = count || 1;
|
||||
let desiredIndentCount = ShiftCommand.shiftIndentCount(indentation, indentation.length + count, config.tabSize);
|
||||
let newIndentation = '';
|
||||
for (let i = 0; i < desiredIndentCount; i++) {
|
||||
newIndentation += '\t';
|
||||
}
|
||||
|
||||
return newIndentation;
|
||||
}
|
||||
|
||||
public static unshiftIndent(config: CursorConfiguration, indentation: string, count?: number): string {
|
||||
count = count || 1;
|
||||
let desiredIndentCount = ShiftCommand.unshiftIndentCount(indentation, indentation.length + count, config.tabSize);
|
||||
let newIndentation = '';
|
||||
for (let i = 0; i < desiredIndentCount; i++) {
|
||||
newIndentation += '\t';
|
||||
}
|
||||
|
||||
return newIndentation;
|
||||
}
|
||||
|
||||
private static _distributedPaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string[]): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
commands[i] = new ReplaceCommand(selections[i], text[i]);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
|
||||
private static _simplePaste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], text: string, pasteOnNewLine: boolean): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
let position = selection.getPosition();
|
||||
|
||||
if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) {
|
||||
pasteOnNewLine = false;
|
||||
}
|
||||
if (pasteOnNewLine && selection.startLineNumber !== selection.endLineNumber) {
|
||||
pasteOnNewLine = false;
|
||||
}
|
||||
if (pasteOnNewLine && selection.startColumn === model.getLineMinColumn(selection.startLineNumber) && selection.endColumn === model.getLineMaxColumn(selection.startLineNumber)) {
|
||||
pasteOnNewLine = false;
|
||||
}
|
||||
|
||||
if (pasteOnNewLine) {
|
||||
// Paste entire line at the beginning of line
|
||||
let typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1);
|
||||
commands[i] = new ReplaceCommand(typeSelection, text);
|
||||
} else {
|
||||
commands[i] = new ReplaceCommand(selection, text);
|
||||
}
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
|
||||
private static _distributePasteToCursors(selections: Selection[], pasteOnNewLine: boolean, text: string): string[] {
|
||||
if (pasteOnNewLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selections.length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
if (selections[i].startLineNumber !== selections[i].endLineNumber) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let pastePieces = text.split(/\r\n|\r|\n/);
|
||||
if (pastePieces.length !== selections.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return pastePieces;
|
||||
}
|
||||
|
||||
public static paste(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], pasteOnNewLine: boolean, text: string): EditOperationResult {
|
||||
const distributedPaste = this._distributePasteToCursors(selections, pasteOnNewLine, text);
|
||||
|
||||
if (distributedPaste) {
|
||||
selections = selections.sort(Range.compareRangesUsingStarts);
|
||||
return this._distributedPaste(config, model, selections, distributedPaste);
|
||||
} else {
|
||||
return this._simplePaste(config, model, selections, text, pasteOnNewLine);
|
||||
}
|
||||
}
|
||||
|
||||
private static _goodIndentForLine(config: CursorConfiguration, model: ITokenizedModel, lineNumber: number): string {
|
||||
let action;
|
||||
let indentation;
|
||||
let expectedIndentAction = LanguageConfigurationRegistry.getInheritIndentForLine(model, lineNumber, false);
|
||||
|
||||
if (expectedIndentAction) {
|
||||
action = expectedIndentAction.action;
|
||||
indentation = expectedIndentAction.indentation;
|
||||
} else if (lineNumber > 1) {
|
||||
let lastLineNumber = lineNumber - 1;
|
||||
for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) {
|
||||
let lineText = model.getLineContent(lastLineNumber);
|
||||
let nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText);
|
||||
if (nonWhitespaceIdx >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastLineNumber < 1) {
|
||||
// No previous line with content found
|
||||
return null;
|
||||
}
|
||||
|
||||
let maxColumn = model.getLineMaxColumn(lastLineNumber);
|
||||
let expectedEnterAction = LanguageConfigurationRegistry.getEnterAction(model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn));
|
||||
if (expectedEnterAction) {
|
||||
indentation = expectedEnterAction.indentation;
|
||||
action = expectedEnterAction.enterAction;
|
||||
if (action) {
|
||||
indentation += action.appendText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action) {
|
||||
if (action === IndentAction.Indent) {
|
||||
indentation = TypeOperations.shiftIndent(config, indentation);
|
||||
}
|
||||
|
||||
if (action === IndentAction.Outdent) {
|
||||
indentation = TypeOperations.unshiftIndent(config, indentation);
|
||||
}
|
||||
|
||||
indentation = config.normalizeIndentation(indentation);
|
||||
}
|
||||
|
||||
if (!indentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indentation;
|
||||
}
|
||||
|
||||
private static _replaceJumpToNextIndent(config: CursorConfiguration, model: ICursorSimpleModel, selection: Selection, insertsAutoWhitespace: boolean): ReplaceCommand {
|
||||
let typeText = '';
|
||||
|
||||
let position = selection.getStartPosition();
|
||||
if (config.insertSpaces) {
|
||||
let visibleColumnFromColumn = CursorColumns.visibleColumnFromColumn2(config, model, position);
|
||||
let tabSize = config.tabSize;
|
||||
let spacesCnt = tabSize - (visibleColumnFromColumn % tabSize);
|
||||
for (let i = 0; i < spacesCnt; i++) {
|
||||
typeText += ' ';
|
||||
}
|
||||
} else {
|
||||
typeText = '\t';
|
||||
}
|
||||
|
||||
return new ReplaceCommand(selection, typeText, insertsAutoWhitespace);
|
||||
}
|
||||
|
||||
public static tab(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[]): ICommand[] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
|
||||
let lineText = model.getLineContent(selection.startLineNumber);
|
||||
|
||||
if (/^\s*$/.test(lineText) && model.isCheapToTokenize(selection.startLineNumber)) {
|
||||
let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber);
|
||||
goodIndent = goodIndent || '\t';
|
||||
let possibleTypeText = config.normalizeIndentation(goodIndent);
|
||||
if (!strings.startsWith(lineText, possibleTypeText)) {
|
||||
commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
commands[i] = this._replaceJumpToNextIndent(config, model, selection, true);
|
||||
} else {
|
||||
if (selection.startLineNumber === selection.endLineNumber) {
|
||||
let lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber);
|
||||
if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) {
|
||||
// This is a single line selection that is not the entire line
|
||||
commands[i] = this._replaceJumpToNextIndent(config, model, selection, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
commands[i] = new ShiftCommand(selection, {
|
||||
isUnshift: false,
|
||||
tabSize: config.tabSize,
|
||||
oneIndent: config.oneIndent,
|
||||
useTabStops: config.useTabStops
|
||||
});
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
public static replacePreviousChar(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], txt: string, replaceCharCnt: number): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
if (!selection.isEmpty()) {
|
||||
// looks like https://github.com/Microsoft/vscode/issues/2773
|
||||
// where a cursor operation occurred before a canceled composition
|
||||
// => ignore composition
|
||||
commands[i] = null;
|
||||
continue;
|
||||
}
|
||||
let pos = selection.getPosition();
|
||||
let startColumn = Math.max(1, pos.column - replaceCharCnt);
|
||||
let range = new Range(pos.lineNumber, startColumn, pos.lineNumber, pos.column);
|
||||
commands[i] = new ReplaceCommand(range, txt);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
});
|
||||
}
|
||||
|
||||
private static _typeCommand(range: Range, text: string, keepPosition: boolean): ICommand {
|
||||
if (keepPosition) {
|
||||
return new ReplaceCommandWithoutChangingPosition(range, text, true);
|
||||
} else {
|
||||
return new ReplaceCommand(range, text, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static _enter(config: CursorConfiguration, model: ITokenizedModel, keepPosition: boolean, range: Range): ICommand {
|
||||
if (!model.isCheapToTokenize(range.getStartPosition().lineNumber)) {
|
||||
let lineText = model.getLineContent(range.startLineNumber);
|
||||
let indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1);
|
||||
return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition);
|
||||
}
|
||||
|
||||
let r = LanguageConfigurationRegistry.getEnterAction(model, range);
|
||||
if (r) {
|
||||
let enterAction = r.enterAction;
|
||||
let indentation = r.indentation;
|
||||
|
||||
if (enterAction.indentAction === IndentAction.None) {
|
||||
// Nothing special
|
||||
return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation + enterAction.appendText), keepPosition);
|
||||
|
||||
} else if (enterAction.indentAction === IndentAction.Indent) {
|
||||
// Indent once
|
||||
return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation + enterAction.appendText), keepPosition);
|
||||
|
||||
} else if (enterAction.indentAction === IndentAction.IndentOutdent) {
|
||||
// Ultra special
|
||||
let normalIndent = config.normalizeIndentation(indentation);
|
||||
let increasedIndent = config.normalizeIndentation(indentation + enterAction.appendText);
|
||||
|
||||
let typeText = '\n' + increasedIndent + '\n' + normalIndent;
|
||||
|
||||
if (keepPosition) {
|
||||
return new ReplaceCommandWithoutChangingPosition(range, typeText, true);
|
||||
} else {
|
||||
return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true);
|
||||
}
|
||||
} else if (enterAction.indentAction === IndentAction.Outdent) {
|
||||
let actualIndentation = TypeOperations.unshiftIndent(config, indentation);
|
||||
return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + enterAction.appendText), keepPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// no enter rules applied, we should check indentation rules then.
|
||||
let ir = LanguageConfigurationRegistry.getIndentForEnter(model, range, {
|
||||
unshiftIndent: (indent) => {
|
||||
return TypeOperations.unshiftIndent(config, indent);
|
||||
},
|
||||
shiftIndent: (indent) => {
|
||||
return TypeOperations.shiftIndent(config, indent);
|
||||
},
|
||||
normalizeIndentation: (indent) => {
|
||||
return config.normalizeIndentation(indent);
|
||||
}
|
||||
}, config.autoIndent);
|
||||
|
||||
let lineText = model.getLineContent(range.startLineNumber);
|
||||
let indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1);
|
||||
|
||||
if (ir) {
|
||||
let oldEndViewColumn = CursorColumns.visibleColumnFromColumn2(config, model, range.getEndPosition());
|
||||
let oldEndColumn = range.endColumn;
|
||||
|
||||
let beforeText = '\n';
|
||||
if (indentation !== config.normalizeIndentation(ir.beforeEnter)) {
|
||||
beforeText = config.normalizeIndentation(ir.beforeEnter) + lineText.substring(indentation.length, range.startColumn - 1) + '\n';
|
||||
range = new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn);
|
||||
}
|
||||
|
||||
let newLineContent = model.getLineContent(range.endLineNumber);
|
||||
let firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent);
|
||||
if (firstNonWhitespace >= 0) {
|
||||
range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1));
|
||||
} else {
|
||||
range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber));
|
||||
}
|
||||
|
||||
if (keepPosition) {
|
||||
return new ReplaceCommandWithoutChangingPosition(range, beforeText + config.normalizeIndentation(ir.afterEnter), true);
|
||||
} else {
|
||||
let offset = 0;
|
||||
if (oldEndColumn <= firstNonWhitespace + 1) {
|
||||
if (!config.insertSpaces) {
|
||||
oldEndViewColumn = Math.ceil(oldEndViewColumn / config.tabSize);
|
||||
}
|
||||
offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0);
|
||||
}
|
||||
return new ReplaceCommandWithOffsetCursorState(range, beforeText + config.normalizeIndentation(ir.afterEnter), 0, offset, true);
|
||||
}
|
||||
|
||||
} else {
|
||||
return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static _isAutoIndentType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[]): boolean {
|
||||
if (!config.autoIndent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
if (!model.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _runAutoIndentType(config: CursorConfiguration, model: ITokenizedModel, range: Range, ch: string): ICommand {
|
||||
let currentIndentation = LanguageConfigurationRegistry.getIndentationAtPosition(model, range.startLineNumber, range.startColumn);
|
||||
let actualIndentation = LanguageConfigurationRegistry.getIndentActionForType(model, range, ch, {
|
||||
shiftIndent: (indentation) => {
|
||||
return TypeOperations.shiftIndent(config, indentation);
|
||||
},
|
||||
unshiftIndent: (indentation) => {
|
||||
return TypeOperations.unshiftIndent(config, indentation);
|
||||
},
|
||||
});
|
||||
|
||||
if (actualIndentation === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (actualIndentation !== config.normalizeIndentation(currentIndentation)) {
|
||||
let firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(range.startLineNumber);
|
||||
if (firstNonWhitespace === 0) {
|
||||
return TypeOperations._typeCommand(
|
||||
new Range(range.startLineNumber, 0, range.endLineNumber, range.endColumn),
|
||||
config.normalizeIndentation(actualIndentation) + ch,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
return TypeOperations._typeCommand(
|
||||
new Range(range.startLineNumber, 0, range.endLineNumber, range.endColumn),
|
||||
config.normalizeIndentation(actualIndentation) +
|
||||
model.getLineContent(range.startLineNumber).substring(firstNonWhitespace - 1, range.startColumn - 1) + ch,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): boolean {
|
||||
if (!config.autoClosingBrackets || !config.autoClosingPairsClose.hasOwnProperty(ch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isEqualPair = (ch === config.autoClosingPairsClose[ch]);
|
||||
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
if (!selection.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const position = selection.getPosition();
|
||||
const lineText = model.getLineContent(position.lineNumber);
|
||||
const afterCharacter = lineText.charAt(position.column - 1);
|
||||
|
||||
if (afterCharacter !== ch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isEqualPair) {
|
||||
const lineTextBeforeCursor = lineText.substr(0, position.column - 1);
|
||||
const chCntBefore = this._countNeedlesInHaystack(lineTextBeforeCursor, ch);
|
||||
if (chCntBefore % 2 === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _countNeedlesInHaystack(haystack: string, needle: string): number {
|
||||
let cnt = 0;
|
||||
let lastIndex = -1;
|
||||
while ((lastIndex = haystack.indexOf(needle, lastIndex + 1)) !== -1) {
|
||||
cnt++;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
private static _runAutoClosingCloseCharType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
const position = selection.getPosition();
|
||||
const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1);
|
||||
commands[i] = new ReplaceCommand(typeSelection, ch);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
});
|
||||
}
|
||||
|
||||
private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): boolean {
|
||||
if (!config.autoClosingBrackets || !config.autoClosingPairsOpen.hasOwnProperty(ch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
if (!selection.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const position = selection.getPosition();
|
||||
const lineText = model.getLineContent(position.lineNumber);
|
||||
|
||||
// Do not auto-close ' or " after a word character
|
||||
if ((ch === '\'' || ch === '"') && position.column > 1) {
|
||||
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
|
||||
const characterBeforeCode = lineText.charCodeAt(position.column - 2);
|
||||
const characterBeforeType = wordSeparators.get(characterBeforeCode);
|
||||
if (characterBeforeType === WordCharacterClass.Regular) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only consider auto closing the pair if a space follows or if another autoclosed pair follows
|
||||
const characterAfter = lineText.charAt(position.column - 1);
|
||||
if (characterAfter) {
|
||||
const thisBraceIsSymmetric = (config.autoClosingPairsOpen[ch] === ch);
|
||||
|
||||
let isBeforeCloseBrace = false;
|
||||
for (let otherCloseBrace in config.autoClosingPairsClose) {
|
||||
const otherBraceIsSymmetric = (config.autoClosingPairsOpen[otherCloseBrace] === otherCloseBrace);
|
||||
if (!thisBraceIsSymmetric && otherBraceIsSymmetric) {
|
||||
continue;
|
||||
}
|
||||
if (characterAfter === otherCloseBrace) {
|
||||
isBeforeCloseBrace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isBeforeCloseBrace && !/\s/.test(characterAfter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!model.isCheapToTokenize(position.lineNumber)) {
|
||||
// Do not force tokenization
|
||||
return false;
|
||||
}
|
||||
|
||||
model.forceTokenization(position.lineNumber);
|
||||
const lineTokens = model.getLineTokens(position.lineNumber);
|
||||
|
||||
let shouldAutoClosePair = false;
|
||||
try {
|
||||
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
|
||||
if (!shouldAutoClosePair) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _runAutoClosingOpenCharType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
const closeCharacter = config.autoClosingPairsOpen[ch];
|
||||
commands[i] = new ReplaceCommandWithOffsetCursorState(selection, ch + closeCharacter, 0, -closeCharacter.length);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: false
|
||||
});
|
||||
}
|
||||
|
||||
private static _isSurroundSelectionType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): boolean {
|
||||
if (!config.autoClosingBrackets || !config.surroundingPairs.hasOwnProperty(ch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let selectionContainsOnlyWhitespace = true;
|
||||
|
||||
for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) {
|
||||
const lineText = model.getLineContent(lineNumber);
|
||||
const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0);
|
||||
const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length);
|
||||
const selectedText = lineText.substring(startIndex, endIndex);
|
||||
if (/[^ \t]/.test(selectedText)) {
|
||||
// this selected text contains something other than whitespace
|
||||
selectionContainsOnlyWhitespace = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionContainsOnlyWhitespace) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _runSurroundSelectionType(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
const closeCharacter = config.surroundingPairs[ch];
|
||||
commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
|
||||
private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[]) {
|
||||
if (selections.length === 1 && model.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static _typeInterceptorElectricChar(config: CursorConfiguration, model: ITokenizedModel, selection: Selection, ch: string): EditOperationResult {
|
||||
if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let position = selection.getPosition();
|
||||
model.forceTokenization(position.lineNumber);
|
||||
let lineTokens = model.getLineTokens(position.lineNumber);
|
||||
|
||||
let electricAction: IElectricAction;
|
||||
try {
|
||||
electricAction = LanguageConfigurationRegistry.onElectricCharacter(ch, lineTokens, position.column);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
|
||||
if (!electricAction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (electricAction.appendText) {
|
||||
const command = new ReplaceCommandWithOffsetCursorState(selection, ch + electricAction.appendText, 0, -electricAction.appendText.length);
|
||||
return new EditOperationResult([command], {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
|
||||
if (electricAction.matchOpenBracket) {
|
||||
let endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1;
|
||||
let match = model.findMatchingBracketUp(electricAction.matchOpenBracket, {
|
||||
lineNumber: position.lineNumber,
|
||||
column: endColumn
|
||||
});
|
||||
|
||||
if (match) {
|
||||
if (match.startLineNumber === position.lineNumber) {
|
||||
// matched something on the same line => no change in indentation
|
||||
return null;
|
||||
}
|
||||
let matchLine = model.getLineContent(match.startLineNumber);
|
||||
let matchLineIndentation = strings.getLeadingWhitespace(matchLine);
|
||||
let newIndentation = config.normalizeIndentation(matchLineIndentation);
|
||||
|
||||
let lineText = model.getLineContent(position.lineNumber);
|
||||
let lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column;
|
||||
|
||||
let prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1);
|
||||
let typeText = newIndentation + prefix + ch;
|
||||
|
||||
let typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column);
|
||||
|
||||
const command = new ReplaceCommand(typeSelection, typeText);
|
||||
return new EditOperationResult([command], {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static typeWithInterceptors(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], ch: string): EditOperationResult {
|
||||
|
||||
if (ch === '\n') {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
commands[i] = TypeOperations._enter(config, model, false, selections[i]);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._isAutoIndentType(config, model, selections)) {
|
||||
let indentCommand = this._runAutoIndentType(config, model, selections[0], ch);
|
||||
if (indentCommand) {
|
||||
return new EditOperationResult([indentCommand], {
|
||||
shouldPushStackElementBefore: true,
|
||||
shouldPushStackElementAfter: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._isAutoClosingCloseCharType(config, model, selections, ch)) {
|
||||
return this._runAutoClosingCloseCharType(config, model, selections, ch);
|
||||
}
|
||||
|
||||
if (this._isAutoClosingOpenCharType(config, model, selections, ch)) {
|
||||
return this._runAutoClosingOpenCharType(config, model, selections, ch);
|
||||
}
|
||||
|
||||
if (this._isSurroundSelectionType(config, model, selections, ch)) {
|
||||
return this._runSurroundSelectionType(config, model, selections, ch);
|
||||
}
|
||||
|
||||
// Electric characters make sense only when dealing with a single cursor,
|
||||
// as multiple cursors typing brackets for example would interfer with bracket matching
|
||||
if (this._isTypeInterceptorElectricChar(config, model, selections)) {
|
||||
const r = this._typeInterceptorElectricChar(config, model, selections[0], ch);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeWithoutInterceptors(config, model, selections, ch);
|
||||
}
|
||||
|
||||
public static typeWithoutInterceptors(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[], str: string): EditOperationResult {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
commands[i] = new ReplaceCommand(selections[i], str);
|
||||
}
|
||||
return new EditOperationResult(commands, {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
});
|
||||
}
|
||||
|
||||
public static lineInsertBefore(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[]): ICommand[] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
let lineNumber = selections[i].positionLineNumber;
|
||||
|
||||
if (lineNumber === 1) {
|
||||
commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n');
|
||||
} else {
|
||||
lineNumber--;
|
||||
let column = model.getLineMaxColumn(lineNumber);
|
||||
|
||||
commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column));
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
public static lineInsertAfter(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[]): ICommand[] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const lineNumber = selections[i].positionLineNumber;
|
||||
let column = model.getLineMaxColumn(lineNumber);
|
||||
commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column));
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
public static lineBreakInsert(config: CursorConfiguration, model: ITokenizedModel, selections: Selection[]): ICommand[] {
|
||||
let commands: ICommand[] = [];
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
commands[i] = this._enter(config, model, true, selections[i]);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
444
src/vs/editor/common/controller/cursorWordOperations.ts
Normal file
444
src/vs/editor/common/controller/cursorWordOperations.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { SingleCursorState, CursorConfiguration, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { WordCharacterClassifier, WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
|
||||
interface IFindWordResult {
|
||||
/**
|
||||
* The index where the word starts.
|
||||
*/
|
||||
start: number;
|
||||
/**
|
||||
* The index where the word ends.
|
||||
*/
|
||||
end: number;
|
||||
/**
|
||||
* The word type.
|
||||
*/
|
||||
wordType: WordType;
|
||||
}
|
||||
|
||||
const enum WordType {
|
||||
None = 0,
|
||||
Regular = 1,
|
||||
Separator = 2
|
||||
}
|
||||
|
||||
export const enum WordNavigationType {
|
||||
WordStart = 0,
|
||||
WordEnd = 1
|
||||
}
|
||||
|
||||
export class WordOperations {
|
||||
|
||||
private static _createWord(lineContent: string, wordType: WordType, start: number, end: number): IFindWordResult {
|
||||
// console.log('WORD ==> ' + start + ' => ' + end + ':::: <<<' + lineContent.substring(start, end) + '>>>');
|
||||
return { start: start, end: end, wordType: wordType };
|
||||
}
|
||||
|
||||
private static _findPreviousWordOnLine(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): IFindWordResult {
|
||||
let lineContent = model.getLineContent(position.lineNumber);
|
||||
return this._doFindPreviousWordOnLine(lineContent, wordSeparators, position);
|
||||
}
|
||||
|
||||
private static _doFindPreviousWordOnLine(lineContent: string, wordSeparators: WordCharacterClassifier, position: Position): IFindWordResult {
|
||||
let wordType = WordType.None;
|
||||
for (let chIndex = position.column - 2; chIndex >= 0; chIndex--) {
|
||||
let chCode = lineContent.charCodeAt(chIndex);
|
||||
let chClass = wordSeparators.get(chCode);
|
||||
|
||||
if (chClass === WordCharacterClass.Regular) {
|
||||
if (wordType === WordType.Separator) {
|
||||
return this._createWord(lineContent, wordType, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1));
|
||||
}
|
||||
wordType = WordType.Regular;
|
||||
} else if (chClass === WordCharacterClass.WordSeparator) {
|
||||
if (wordType === WordType.Regular) {
|
||||
return this._createWord(lineContent, wordType, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1));
|
||||
}
|
||||
wordType = WordType.Separator;
|
||||
} else if (chClass === WordCharacterClass.Whitespace) {
|
||||
if (wordType !== WordType.None) {
|
||||
return this._createWord(lineContent, wordType, chIndex + 1, this._findEndOfWord(lineContent, wordSeparators, wordType, chIndex + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wordType !== WordType.None) {
|
||||
return this._createWord(lineContent, wordType, 0, this._findEndOfWord(lineContent, wordSeparators, wordType, 0));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _findEndOfWord(lineContent: string, wordSeparators: WordCharacterClassifier, wordType: WordType, startIndex: number): number {
|
||||
let len = lineContent.length;
|
||||
for (let chIndex = startIndex; chIndex < len; chIndex++) {
|
||||
let chCode = lineContent.charCodeAt(chIndex);
|
||||
let chClass = wordSeparators.get(chCode);
|
||||
|
||||
if (chClass === WordCharacterClass.Whitespace) {
|
||||
return chIndex;
|
||||
}
|
||||
if (wordType === WordType.Regular && chClass === WordCharacterClass.WordSeparator) {
|
||||
return chIndex;
|
||||
}
|
||||
if (wordType === WordType.Separator && chClass === WordCharacterClass.Regular) {
|
||||
return chIndex;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
private static _findNextWordOnLine(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): IFindWordResult {
|
||||
let lineContent = model.getLineContent(position.lineNumber);
|
||||
return this._doFindNextWordOnLine(lineContent, wordSeparators, position);
|
||||
}
|
||||
|
||||
private static _doFindNextWordOnLine(lineContent: string, wordSeparators: WordCharacterClassifier, position: Position): IFindWordResult {
|
||||
let wordType = WordType.None;
|
||||
let len = lineContent.length;
|
||||
|
||||
for (let chIndex = position.column - 1; chIndex < len; chIndex++) {
|
||||
let chCode = lineContent.charCodeAt(chIndex);
|
||||
let chClass = wordSeparators.get(chCode);
|
||||
|
||||
if (chClass === WordCharacterClass.Regular) {
|
||||
if (wordType === WordType.Separator) {
|
||||
return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex);
|
||||
}
|
||||
wordType = WordType.Regular;
|
||||
} else if (chClass === WordCharacterClass.WordSeparator) {
|
||||
if (wordType === WordType.Regular) {
|
||||
return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex);
|
||||
}
|
||||
wordType = WordType.Separator;
|
||||
} else if (chClass === WordCharacterClass.Whitespace) {
|
||||
if (wordType !== WordType.None) {
|
||||
return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, chIndex - 1), chIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wordType !== WordType.None) {
|
||||
return this._createWord(lineContent, wordType, this._findStartOfWord(lineContent, wordSeparators, wordType, len - 1), len);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _findStartOfWord(lineContent: string, wordSeparators: WordCharacterClassifier, wordType: WordType, startIndex: number): number {
|
||||
for (let chIndex = startIndex; chIndex >= 0; chIndex--) {
|
||||
let chCode = lineContent.charCodeAt(chIndex);
|
||||
let chClass = wordSeparators.get(chCode);
|
||||
|
||||
if (chClass === WordCharacterClass.Whitespace) {
|
||||
return chIndex + 1;
|
||||
}
|
||||
if (wordType === WordType.Regular && chClass === WordCharacterClass.WordSeparator) {
|
||||
return chIndex + 1;
|
||||
}
|
||||
if (wordType === WordType.Separator && chClass === WordCharacterClass.Regular) {
|
||||
return chIndex + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static moveWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
|
||||
let lineNumber = position.lineNumber;
|
||||
let column = position.column;
|
||||
|
||||
if (column === 1) {
|
||||
if (lineNumber > 1) {
|
||||
lineNumber = lineNumber - 1;
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
let prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, column));
|
||||
|
||||
if (wordNavigationType === WordNavigationType.WordStart) {
|
||||
if (prevWordOnLine) {
|
||||
column = prevWordOnLine.start + 1;
|
||||
} else {
|
||||
column = 1;
|
||||
}
|
||||
} else {
|
||||
if (prevWordOnLine && column <= prevWordOnLine.end + 1) {
|
||||
prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1));
|
||||
}
|
||||
if (prevWordOnLine) {
|
||||
column = prevWordOnLine.end + 1;
|
||||
} else {
|
||||
column = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
|
||||
public static moveWordRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
|
||||
let lineNumber = position.lineNumber;
|
||||
let column = position.column;
|
||||
|
||||
if (column === model.getLineMaxColumn(lineNumber)) {
|
||||
if (lineNumber < model.getLineCount()) {
|
||||
lineNumber = lineNumber + 1;
|
||||
column = 1;
|
||||
}
|
||||
}
|
||||
|
||||
let nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, column));
|
||||
|
||||
if (wordNavigationType === WordNavigationType.WordEnd) {
|
||||
if (nextWordOnLine) {
|
||||
column = nextWordOnLine.end + 1;
|
||||
} else {
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
} else {
|
||||
if (nextWordOnLine && column >= nextWordOnLine.start + 1) {
|
||||
nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, nextWordOnLine.end + 1));
|
||||
}
|
||||
if (nextWordOnLine) {
|
||||
column = nextWordOnLine.start + 1;
|
||||
} else {
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
|
||||
private static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range {
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
const startIndex = position.column - 2;
|
||||
const lastNonWhitespace = strings.lastNonWhitespaceIndex(lineContent, startIndex);
|
||||
if (lastNonWhitespace + 1 < startIndex) {
|
||||
return new Range(position.lineNumber, lastNonWhitespace + 2, position.lineNumber, position.column);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static deleteWordLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
|
||||
if (!selection.isEmpty()) {
|
||||
return selection;
|
||||
}
|
||||
|
||||
const position = new Position(selection.positionLineNumber, selection.positionColumn);
|
||||
|
||||
let lineNumber = position.lineNumber;
|
||||
let column = position.column;
|
||||
|
||||
if (lineNumber === 1 && column === 1) {
|
||||
// Ignore deleting at beginning of file
|
||||
return null;
|
||||
}
|
||||
|
||||
if (whitespaceHeuristics) {
|
||||
let r = this._deleteWordLeftWhitespace(model, position);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
let prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, position);
|
||||
|
||||
if (wordNavigationType === WordNavigationType.WordStart) {
|
||||
if (prevWordOnLine) {
|
||||
column = prevWordOnLine.start + 1;
|
||||
} else {
|
||||
if (column > 1) {
|
||||
column = 1;
|
||||
} else {
|
||||
lineNumber--;
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (prevWordOnLine && column <= prevWordOnLine.end + 1) {
|
||||
prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, new Position(lineNumber, prevWordOnLine.start + 1));
|
||||
}
|
||||
if (prevWordOnLine) {
|
||||
column = prevWordOnLine.end + 1;
|
||||
} else {
|
||||
if (column > 1) {
|
||||
column = 1;
|
||||
} else {
|
||||
lineNumber--;
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Range(lineNumber, column, position.lineNumber, position.column);
|
||||
}
|
||||
|
||||
private static _findFirstNonWhitespaceChar(str: string, startIndex: number): number {
|
||||
let len = str.length;
|
||||
for (let chIndex = startIndex; chIndex < len; chIndex++) {
|
||||
let ch = str.charAt(chIndex);
|
||||
if (ch !== ' ' && ch !== '\t') {
|
||||
return chIndex;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
private static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range {
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
const startIndex = position.column - 1;
|
||||
const firstNonWhitespace = this._findFirstNonWhitespaceChar(lineContent, startIndex);
|
||||
if (startIndex + 1 < firstNonWhitespace) {
|
||||
// bingo
|
||||
return new Range(position.lineNumber, position.column, position.lineNumber, firstNonWhitespace + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static deleteWordRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
|
||||
if (!selection.isEmpty()) {
|
||||
return selection;
|
||||
}
|
||||
|
||||
const position = new Position(selection.positionLineNumber, selection.positionColumn);
|
||||
|
||||
let lineNumber = position.lineNumber;
|
||||
let column = position.column;
|
||||
|
||||
const lineCount = model.getLineCount();
|
||||
const maxColumn = model.getLineMaxColumn(lineNumber);
|
||||
if (lineNumber === lineCount && column === maxColumn) {
|
||||
// Ignore deleting at end of file
|
||||
return null;
|
||||
}
|
||||
|
||||
if (whitespaceHeuristics) {
|
||||
let r = this._deleteWordRightWhitespace(model, position);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
let nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, position);
|
||||
|
||||
if (wordNavigationType === WordNavigationType.WordEnd) {
|
||||
if (nextWordOnLine) {
|
||||
column = nextWordOnLine.end + 1;
|
||||
} else {
|
||||
if (column < maxColumn || lineNumber === lineCount) {
|
||||
column = maxColumn;
|
||||
} else {
|
||||
lineNumber++;
|
||||
nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, 1));
|
||||
if (nextWordOnLine) {
|
||||
column = nextWordOnLine.start + 1;
|
||||
} else {
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (nextWordOnLine && column >= nextWordOnLine.start + 1) {
|
||||
nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, nextWordOnLine.end + 1));
|
||||
}
|
||||
if (nextWordOnLine) {
|
||||
column = nextWordOnLine.start + 1;
|
||||
} else {
|
||||
if (column < maxColumn || lineNumber === lineCount) {
|
||||
column = maxColumn;
|
||||
} else {
|
||||
lineNumber++;
|
||||
nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, new Position(lineNumber, 1));
|
||||
if (nextWordOnLine) {
|
||||
column = nextWordOnLine.start + 1;
|
||||
} else {
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Range(lineNumber, column, position.lineNumber, position.column);
|
||||
}
|
||||
|
||||
public static word(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, position: Position): SingleCursorState {
|
||||
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
|
||||
let prevWord = WordOperations._findPreviousWordOnLine(wordSeparators, model, position);
|
||||
let isInPrevWord = (prevWord && prevWord.wordType === WordType.Regular && prevWord.start < position.column - 1 && position.column - 1 <= prevWord.end);
|
||||
let nextWord = WordOperations._findNextWordOnLine(wordSeparators, model, position);
|
||||
let isInNextWord = (nextWord && nextWord.wordType === WordType.Regular && nextWord.start < position.column - 1 && position.column - 1 <= nextWord.end);
|
||||
|
||||
if (!inSelectionMode || !cursor.hasSelection()) {
|
||||
// Entering word selection for the first time
|
||||
|
||||
let startColumn: number;
|
||||
let endColumn: number;
|
||||
|
||||
if (isInPrevWord) {
|
||||
startColumn = prevWord.start + 1;
|
||||
endColumn = prevWord.end + 1;
|
||||
} else if (isInNextWord) {
|
||||
startColumn = nextWord.start + 1;
|
||||
endColumn = nextWord.end + 1;
|
||||
} else {
|
||||
if (prevWord) {
|
||||
startColumn = prevWord.end + 1;
|
||||
} else {
|
||||
startColumn = 1;
|
||||
}
|
||||
if (nextWord) {
|
||||
endColumn = nextWord.start + 1;
|
||||
} else {
|
||||
endColumn = model.getLineMaxColumn(position.lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return new SingleCursorState(
|
||||
new Range(position.lineNumber, startColumn, position.lineNumber, endColumn), 0,
|
||||
new Position(position.lineNumber, endColumn), 0
|
||||
);
|
||||
}
|
||||
|
||||
let startColumn: number;
|
||||
let endColumn: number;
|
||||
|
||||
if (isInPrevWord) {
|
||||
startColumn = prevWord.start + 1;
|
||||
endColumn = prevWord.end + 1;
|
||||
} else if (isInNextWord) {
|
||||
startColumn = nextWord.start + 1;
|
||||
endColumn = nextWord.end + 1;
|
||||
} else {
|
||||
startColumn = position.column;
|
||||
endColumn = position.column;
|
||||
}
|
||||
|
||||
let lineNumber = position.lineNumber;
|
||||
let column: number;
|
||||
if (position.isBeforeOrEqual(cursor.selectionStart.getStartPosition())) {
|
||||
column = startColumn;
|
||||
let possiblePosition = new Position(lineNumber, column);
|
||||
if (cursor.selectionStart.containsPosition(possiblePosition)) {
|
||||
column = cursor.selectionStart.endColumn;
|
||||
}
|
||||
} else {
|
||||
column = endColumn;
|
||||
let possiblePosition = new Position(lineNumber, column);
|
||||
if (cursor.selectionStart.containsPosition(possiblePosition)) {
|
||||
column = cursor.selectionStart.startColumn;
|
||||
}
|
||||
}
|
||||
|
||||
return cursor.move(cursor.hasSelection(), lineNumber, column, 0);
|
||||
}
|
||||
}
|
||||
116
src/vs/editor/common/controller/oneCursor.ts
Normal file
116
src/vs/editor/common/controller/oneCursor.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { SingleCursorState, CursorContext, CursorState } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection, SelectionDirection } from 'vs/editor/common/core/selection';
|
||||
|
||||
export class OneCursor {
|
||||
|
||||
public modelState: SingleCursorState;
|
||||
public viewState: SingleCursorState;
|
||||
|
||||
private _selStartMarker: string;
|
||||
private _selEndMarker: string;
|
||||
|
||||
constructor(context: CursorContext) {
|
||||
this._setState(
|
||||
context,
|
||||
new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0),
|
||||
new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0)
|
||||
);
|
||||
}
|
||||
|
||||
public dispose(context: CursorContext): void {
|
||||
context.model._removeMarker(this._selStartMarker);
|
||||
context.model._removeMarker(this._selEndMarker);
|
||||
}
|
||||
|
||||
public asCursorState(): CursorState {
|
||||
return new CursorState(this.modelState, this.viewState);
|
||||
}
|
||||
|
||||
public readSelectionFromMarkers(context: CursorContext): Selection {
|
||||
const start = context.model._getMarker(this._selStartMarker);
|
||||
const end = context.model._getMarker(this._selEndMarker);
|
||||
|
||||
if (this.modelState.selection.getDirection() === SelectionDirection.LTR) {
|
||||
return new Selection(start.lineNumber, start.column, end.lineNumber, end.column);
|
||||
}
|
||||
|
||||
return new Selection(end.lineNumber, end.column, start.lineNumber, start.column);
|
||||
}
|
||||
|
||||
public ensureValidState(context: CursorContext): void {
|
||||
this._setState(context, this.modelState, this.viewState);
|
||||
}
|
||||
|
||||
public setState(context: CursorContext, modelState: SingleCursorState, viewState: SingleCursorState): void {
|
||||
this._setState(context, modelState, viewState);
|
||||
}
|
||||
|
||||
private _setState(context: CursorContext, modelState: SingleCursorState, viewState: SingleCursorState): void {
|
||||
if (!modelState) {
|
||||
// We only have the view state => compute the model state
|
||||
const selectionStart = context.model.validateRange(
|
||||
context.convertViewRangeToModelRange(viewState.selectionStart)
|
||||
);
|
||||
|
||||
const position = context.model.validatePosition(
|
||||
context.convertViewPositionToModelPosition(viewState.position.lineNumber, viewState.position.column)
|
||||
);
|
||||
|
||||
modelState = new SingleCursorState(selectionStart, viewState.selectionStartLeftoverVisibleColumns, position, viewState.leftoverVisibleColumns);
|
||||
} else {
|
||||
// Validate new model state
|
||||
const selectionStart = context.model.validateRange(modelState.selectionStart);
|
||||
const selectionStartLeftoverVisibleColumns = modelState.selectionStart.equalsRange(selectionStart) ? modelState.selectionStartLeftoverVisibleColumns : 0;
|
||||
|
||||
const position = context.model.validatePosition(
|
||||
modelState.position
|
||||
);
|
||||
const leftoverVisibleColumns = modelState.position.equals(position) ? modelState.leftoverVisibleColumns : 0;
|
||||
|
||||
modelState = new SingleCursorState(selectionStart, selectionStartLeftoverVisibleColumns, position, leftoverVisibleColumns);
|
||||
}
|
||||
|
||||
if (!viewState) {
|
||||
// We only have the model state => compute the view state
|
||||
const viewSelectionStart1 = context.convertModelPositionToViewPosition(new Position(modelState.selectionStart.startLineNumber, modelState.selectionStart.startColumn));
|
||||
const viewSelectionStart2 = context.convertModelPositionToViewPosition(new Position(modelState.selectionStart.endLineNumber, modelState.selectionStart.endColumn));
|
||||
const viewSelectionStart = new Range(viewSelectionStart1.lineNumber, viewSelectionStart1.column, viewSelectionStart2.lineNumber, viewSelectionStart2.column);
|
||||
const viewPosition = context.convertModelPositionToViewPosition(modelState.position);
|
||||
viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns);
|
||||
} else {
|
||||
// Validate new view state
|
||||
const viewSelectionStart = context.validateViewRange(viewState.selectionStart, modelState.selectionStart);
|
||||
const viewPosition = context.validateViewPosition(viewState.position, modelState.position);
|
||||
viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns);
|
||||
}
|
||||
|
||||
if (this.modelState && this.viewState && this.modelState.equals(modelState) && this.viewState.equals(viewState)) {
|
||||
// No-op, early return
|
||||
return;
|
||||
}
|
||||
|
||||
this.modelState = modelState;
|
||||
this.viewState = viewState;
|
||||
|
||||
this._selStartMarker = this._ensureMarker(context, this._selStartMarker, this.modelState.selection.startLineNumber, this.modelState.selection.startColumn, true);
|
||||
this._selEndMarker = this._ensureMarker(context, this._selEndMarker, this.modelState.selection.endLineNumber, this.modelState.selection.endColumn, false);
|
||||
}
|
||||
|
||||
private _ensureMarker(context: CursorContext, markerId: string, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string {
|
||||
if (!markerId) {
|
||||
return context.model._addMarker(0, lineNumber, column, stickToPreviousCharacter);
|
||||
} else {
|
||||
context.model._changeMarker(markerId, lineNumber, column);
|
||||
context.model._changeMarkerStickiness(markerId, stickToPreviousCharacter);
|
||||
return markerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/vs/editor/common/controller/wordCharacterClassifier.ts
Normal file
43
src/vs/editor/common/controller/wordCharacterClassifier.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { CharCode } from 'vs/base/common/charCode';
|
||||
import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier';
|
||||
|
||||
export const enum WordCharacterClass {
|
||||
Regular = 0,
|
||||
Whitespace = 1,
|
||||
WordSeparator = 2
|
||||
}
|
||||
|
||||
export class WordCharacterClassifier extends CharacterClassifier<WordCharacterClass> {
|
||||
|
||||
constructor(wordSeparators: string) {
|
||||
super(WordCharacterClass.Regular);
|
||||
|
||||
for (let i = 0, len = wordSeparators.length; i < len; i++) {
|
||||
this.set(wordSeparators.charCodeAt(i), WordCharacterClass.WordSeparator);
|
||||
}
|
||||
|
||||
this.set(CharCode.Space, WordCharacterClass.Whitespace);
|
||||
this.set(CharCode.Tab, WordCharacterClass.Whitespace);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function once<R>(computeFn: (input: string) => R): (input: string) => R {
|
||||
let cache: { [key: string]: R; } = {}; // TODO@Alex unbounded cache
|
||||
return (input: string): R => {
|
||||
if (!cache.hasOwnProperty(input)) {
|
||||
cache[input] = computeFn(input);
|
||||
}
|
||||
return cache[input];
|
||||
};
|
||||
}
|
||||
|
||||
export const getMapForWordSeparators = once<WordCharacterClassifier>(
|
||||
(input) => new WordCharacterClassifier(input)
|
||||
);
|
||||
Reference in New Issue
Block a user