Merge from vscode 073a24de05773f2261f89172987002dc0ae2f1cd (#9711)

This commit is contained in:
Anthony Dresser
2020-03-24 00:24:15 -07:00
committed by GitHub
parent 29741d684e
commit 89ef1b0c2e
226 changed files with 6161 additions and 3288 deletions

View File

@@ -4,8 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings';
import * as platform from 'vs/base/common/platform';
import * as buffer from 'vs/base/common/buffer';
declare const TextDecoder: any; // TODO@TypeScript
declare const TextDecoder: {
prototype: TextDecoder;
new(label?: string): TextDecoder;
};
interface TextDecoder {
decode(view: Uint16Array): string;
}
@@ -18,17 +23,42 @@ export interface IStringBuilder {
appendASCIIString(str: string): void;
}
let _platformTextDecoder: TextDecoder | null;
function getPlatformTextDecoder(): TextDecoder {
if (!_platformTextDecoder) {
_platformTextDecoder = new TextDecoder(platform.isLittleEndian() ? 'UTF-16LE' : 'UTF-16BE');
}
return _platformTextDecoder;
}
export let createStringBuilder: (capacity: number) => IStringBuilder;
export let decodeUTF16LE: (source: Uint8Array, offset: number, len: number) => string;
if (typeof TextDecoder !== 'undefined') {
createStringBuilder = (capacity) => new StringBuilder(capacity);
decodeUTF16LE = standardDecodeUTF16LE;
} else {
createStringBuilder = (capacity) => new CompatStringBuilder();
decodeUTF16LE = compatDecodeUTF16LE;
}
function standardDecodeUTF16LE(source: Uint8Array, offset: number, len: number): string {
const view = new Uint16Array(source.buffer, offset, len);
return getPlatformTextDecoder().decode(view);
}
function compatDecodeUTF16LE(source: Uint8Array, offset: number, len: number): string {
let result: string[] = [];
let resultLen = 0;
for (let i = 0; i < len; i++) {
const charCode = buffer.readUInt16LE(source, offset); offset += 2;
result[resultLen++] = String.fromCharCode(charCode);
}
return result.join('');
}
class StringBuilder implements IStringBuilder {
private readonly _decoder: TextDecoder;
private readonly _capacity: number;
private readonly _buffer: Uint16Array;
@@ -36,7 +66,6 @@ class StringBuilder implements IStringBuilder {
private _bufferLength: number;
constructor(capacity: number) {
this._decoder = new TextDecoder('UTF-16LE');
this._capacity = capacity | 0;
this._buffer = new Uint16Array(this._capacity);
@@ -63,7 +92,7 @@ class StringBuilder implements IStringBuilder {
}
const view = new Uint16Array(this._buffer.buffer, 0, this._bufferLength);
return this._decoder.decode(view);
return getPlatformTextDecoder().decode(view);
}
private _flushBuffer(): void {

View File

@@ -15,6 +15,7 @@ import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
/**
* Vertical Lane in the overview ruler of the editor.
@@ -373,21 +374,13 @@ export interface IValidEditOperation {
*/
range: Range;
/**
* The text to replace with. This can be null to emulate a simple delete.
* The text to replace with. This can be empty to emulate a simple delete.
*/
text: string | null;
text: string;
/**
* This indicates that this operation has "insert" semantics.
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
* @internal
*/
forceMoveMarkers: boolean;
}
/**
* @internal
*/
export interface IValidEditOperations {
operations: IValidEditOperation[];
textChange: TextChange;
}
/**
@@ -1099,9 +1092,11 @@ export interface ITextModel {
* Edit the model without adding the edits to the undo stack.
* This can have dire consequences on the undo stack! See @pushEditOperations for the preferred way.
* @param operations The edit operations.
* @return The inverse edit operations, that, when applied, will bring the model back to the previous state.
* @return If desired, the inverse edit operations, that, when applied, will bring the model back to the previous state.
*/
applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[];
applyEdits(operations: IIdentifiedSingleEditOperation[]): void;
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
/**
* Change the end of line sequence without recording in the undo stack.
@@ -1112,7 +1107,12 @@ export interface ITextModel {
/**
* @internal
*/
_applyUndoRedoEdits(edits: IValidEditOperations[], eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[];
_applyUndo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
/**
* @internal
*/
_applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
/**
* Undo edit operations until the first previous stop point created by `pushStackElement`.
@@ -1291,7 +1291,7 @@ export interface ITextBuffer {
getLineLastNonWhitespaceColumn(lineNumber: number): number;
setEOL(newEOL: '\r\n' | '\n'): void;
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult;
applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult;
findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[];
}
@@ -1301,7 +1301,7 @@ export interface ITextBuffer {
export class ApplyEditsResult {
constructor(
public readonly reverseEdits: IValidEditOperation[],
public readonly reverseEdits: IValidEditOperation[] | null,
public readonly changes: IInternalModelContentChange[],
public readonly trimAutoWhitespaceLineNumbers: number[] | null
) { }

View File

@@ -6,69 +6,209 @@
import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Selection } from 'vs/editor/common/core/selection';
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model';
import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
import * as buffer from 'vs/base/common/buffer';
export class EditStackElement implements IResourceUndoRedoElement {
class SingleModelEditStackData {
public readonly type = UndoRedoElementType.Resource;
public readonly label: string;
private _isOpen: boolean;
public readonly model: ITextModel;
private readonly _beforeVersionId: number;
private readonly _beforeEOL: EndOfLineSequence;
private readonly _beforeCursorState: Selection[] | null;
private _afterVersionId: number;
private _afterEOL: EndOfLineSequence;
private _afterCursorState: Selection[] | null;
private _edits: IValidEditOperations[];
public get resource(): URI {
return this.model.uri;
public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData {
const alternativeVersionId = model.getAlternativeVersionId();
const eol = getModelEOL(model);
return new SingleModelEditStackData(
alternativeVersionId,
alternativeVersionId,
eol,
eol,
beforeCursorState,
beforeCursorState,
[]
);
}
constructor(model: ITextModel, beforeCursorState: Selection[] | null) {
this.label = nls.localize('edit', "Typing");
this._isOpen = true;
this.model = model;
this._beforeVersionId = this.model.getAlternativeVersionId();
this._beforeEOL = getModelEOL(this.model);
this._beforeCursorState = beforeCursorState;
this._afterVersionId = this._beforeVersionId;
this._afterEOL = this._beforeEOL;
this._afterCursorState = this._beforeCursorState;
this._edits = [];
}
public canAppend(model: ITextModel): boolean {
return (this._isOpen && this.model === model);
}
constructor(
public readonly beforeVersionId: number,
public afterVersionId: number,
public readonly beforeEOL: EndOfLineSequence,
public afterEOL: EndOfLineSequence,
public readonly beforeCursorState: Selection[] | null,
public afterCursorState: Selection[] | null,
public changes: TextChange[]
) { }
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
if (operations.length > 0) {
this._edits.push({ operations: operations });
this.changes = compressConsecutiveTextChanges(this.changes, operations.map(op => op.textChange));
}
this.afterEOL = afterEOL;
this.afterVersionId = afterVersionId;
this.afterCursorState = afterCursorState;
}
private static _writeSelectionsSize(selections: Selection[] | null): number {
return 4 + 4 * 4 * (selections ? selections.length : 0);
}
private static _writeSelections(b: Uint8Array, selections: Selection[] | null, offset: number): number {
buffer.writeUInt32BE(b, (selections ? selections.length : 0), offset); offset += 4;
if (selections) {
for (const selection of selections) {
buffer.writeUInt32BE(b, selection.selectionStartLineNumber, offset); offset += 4;
buffer.writeUInt32BE(b, selection.selectionStartColumn, offset); offset += 4;
buffer.writeUInt32BE(b, selection.positionLineNumber, offset); offset += 4;
buffer.writeUInt32BE(b, selection.positionColumn, offset); offset += 4;
}
}
return offset;
}
private static _readSelections(b: Uint8Array, offset: number, dest: Selection[]): number {
const count = buffer.readUInt32BE(b, offset); offset += 4;
for (let i = 0; i < count; i++) {
const selectionStartLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
const selectionStartColumn = buffer.readUInt32BE(b, offset); offset += 4;
const positionLineNumber = buffer.readUInt32BE(b, offset); offset += 4;
const positionColumn = buffer.readUInt32BE(b, offset); offset += 4;
dest.push(new Selection(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn));
}
return offset;
}
public serialize(): ArrayBuffer {
let necessarySize = (
+ 4 // beforeVersionId
+ 4 // afterVersionId
+ 1 // beforeEOL
+ 1 // afterEOL
+ SingleModelEditStackData._writeSelectionsSize(this.beforeCursorState)
+ SingleModelEditStackData._writeSelectionsSize(this.afterCursorState)
+ 4 // change count
);
for (const change of this.changes) {
necessarySize += change.writeSize();
}
const b = new Uint8Array(necessarySize);
let offset = 0;
buffer.writeUInt32BE(b, this.beforeVersionId, offset); offset += 4;
buffer.writeUInt32BE(b, this.afterVersionId, offset); offset += 4;
buffer.writeUInt8(b, this.beforeEOL, offset); offset += 1;
buffer.writeUInt8(b, this.afterEOL, offset); offset += 1;
offset = SingleModelEditStackData._writeSelections(b, this.beforeCursorState, offset);
offset = SingleModelEditStackData._writeSelections(b, this.afterCursorState, offset);
buffer.writeUInt32BE(b, this.changes.length, offset); offset += 4;
for (const change of this.changes) {
offset = change.write(b, offset);
}
return b.buffer;
}
public static deserialize(source: ArrayBuffer): SingleModelEditStackData {
const b = new Uint8Array(source);
let offset = 0;
const beforeVersionId = buffer.readUInt32BE(b, offset); offset += 4;
const afterVersionId = buffer.readUInt32BE(b, offset); offset += 4;
const beforeEOL = buffer.readUInt8(b, offset); offset += 1;
const afterEOL = buffer.readUInt8(b, offset); offset += 1;
const beforeCursorState: Selection[] = [];
offset = SingleModelEditStackData._readSelections(b, offset, beforeCursorState);
const afterCursorState: Selection[] = [];
offset = SingleModelEditStackData._readSelections(b, offset, afterCursorState);
const changeCount = buffer.readUInt32BE(b, offset); offset += 4;
const changes: TextChange[] = [];
for (let i = 0; i < changeCount; i++) {
offset = TextChange.read(b, offset, changes);
}
return new SingleModelEditStackData(
beforeVersionId,
afterVersionId,
beforeEOL,
afterEOL,
beforeCursorState,
afterCursorState,
changes
);
}
}
export class SingleModelEditStackElement implements IResourceUndoRedoElement {
public model: ITextModel | URI;
private _data: SingleModelEditStackData | ArrayBuffer;
public get type(): UndoRedoElementType.Resource {
return UndoRedoElementType.Resource;
}
public get resource(): URI {
if (URI.isUri(this.model)) {
return this.model;
}
return this.model.uri;
}
public get label(): string {
return nls.localize('edit', "Typing");
}
constructor(model: ITextModel, beforeCursorState: Selection[] | null) {
this.model = model;
this._data = SingleModelEditStackData.create(model, beforeCursorState);
}
public setModel(model: ITextModel | URI): void {
this.model = model;
}
public canAppend(model: ITextModel): boolean {
return (this.model === model && this._data instanceof SingleModelEditStackData);
}
public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void {
if (this._data instanceof SingleModelEditStackData) {
this._data.append(model, operations, afterEOL, afterVersionId, afterCursorState);
}
this._afterEOL = afterEOL;
this._afterVersionId = afterVersionId;
this._afterCursorState = afterCursorState;
}
public close(): void {
this._isOpen = false;
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
}
public undo(): void {
this._isOpen = false;
this._edits.reverse();
this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState);
if (URI.isUri(this.model)) {
// don't have a model
throw new Error(`Invalid SingleModelEditStackElement`);
}
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
const data = SingleModelEditStackData.deserialize(this._data);
this.model._applyUndo(data.changes, data.beforeEOL, data.beforeVersionId, data.beforeCursorState);
}
public redo(): void {
this._edits.reverse();
this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState);
if (URI.isUri(this.model)) {
// don't have a model
throw new Error(`Invalid SingleModelEditStackElement`);
}
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
const data = SingleModelEditStackData.deserialize(this._data);
this.model._applyRedo(data.changes, data.afterEOL, data.afterVersionId, data.afterCursorState);
}
public heapSize(): number {
if (this._data instanceof SingleModelEditStackData) {
this._data = this._data.serialize();
}
return this._data.byteLength + 168/*heap overhead*/;
}
}
@@ -78,27 +218,34 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
public readonly label: string;
private _isOpen: boolean;
private readonly _editStackElementsArr: EditStackElement[];
private readonly _editStackElementsMap: Map<string, EditStackElement>;
private readonly _editStackElementsArr: SingleModelEditStackElement[];
private readonly _editStackElementsMap: Map<string, SingleModelEditStackElement>;
public get resources(): readonly URI[] {
return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri);
return this._editStackElementsArr.map(editStackElement => editStackElement.resource);
}
constructor(
label: string,
editStackElements: EditStackElement[]
editStackElements: SingleModelEditStackElement[]
) {
this.label = label;
this._isOpen = true;
this._editStackElementsArr = editStackElements.slice(0);
this._editStackElementsMap = new Map<string, EditStackElement>();
this._editStackElementsMap = new Map<string, SingleModelEditStackElement>();
for (const editStackElement of this._editStackElementsArr) {
const key = uriGetComparisonKey(editStackElement.model.uri);
const key = uriGetComparisonKey(editStackElement.resource);
this._editStackElementsMap.set(key, editStackElement);
}
}
public setModel(model: ITextModel | URI): void {
const key = uriGetComparisonKey(URI.isUri(model) ? model : model.uri);
if (this._editStackElementsMap.has(key)) {
this._editStackElementsMap.get(key)!.setModel(model);
}
}
public canAppend(model: ITextModel): boolean {
if (!this._isOpen) {
return false;
@@ -135,11 +282,22 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement {
}
}
public heapSize(resource: URI): number {
const key = uriGetComparisonKey(resource);
if (this._editStackElementsMap.has(key)) {
const editStackElement = this._editStackElementsMap.get(key)!;
return editStackElement.heapSize();
}
return 0;
}
public split(): IResourceUndoRedoElement[] {
return this._editStackElementsArr;
}
}
export type EditStackElement = SingleModelEditStackElement | MultiModelEditStackElement;
function getModelEOL(model: ITextModel): EndOfLineSequence {
const eol = model.getEOL();
if (eol === '\n') {
@@ -149,11 +307,11 @@ function getModelEOL(model: ITextModel): EndOfLineSequence {
}
}
function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement {
function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement {
if (!element) {
return false;
}
return ((element instanceof EditStackElement) || (element instanceof MultiModelEditStackElement));
return ((element instanceof SingleModelEditStackElement) || (element instanceof MultiModelEditStackElement));
}
export class EditStack {
@@ -177,12 +335,12 @@ export class EditStack {
this._undoRedoService.removeElements(this._model.uri);
}
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement | MultiModelEditStackElement {
private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement {
const lastElement = this._undoRedoService.getLastElement(this._model.uri);
if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) {
return lastElement;
}
const newElement = new EditStackElement(this._model, beforeCursorState);
const newElement = new SingleModelEditStackElement(this._model, beforeCursorState);
this._undoRedoService.pushElement(newElement);
return newElement;
}
@@ -195,7 +353,7 @@ export class EditStack {
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null {
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState);
const inverseEditOperations = this._model.applyEdits(editOperations);
const inverseEditOperations = this._model.applyEdits(editOperations, true);
const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);
editStackElement.append(this._model, inverseEditOperations, getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState);
return afterCursorState;

View File

@@ -269,7 +269,7 @@ export class PieceTreeBase {
protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer.
protected _lineCnt!: number;
protected _length!: number;
protected _EOL!: string;
protected _EOL!: '\r\n' | '\n';
protected _EOLLength!: number;
protected _EOLNormalized!: boolean;
private _lastChangeBufferPos!: BufferCursor;
@@ -351,7 +351,7 @@ export class PieceTreeBase {
}
// #region Buffer API
public getEOL(): string {
public getEOL(): '\r\n' | '\n' {
return this._EOL;
}

View File

@@ -9,6 +9,8 @@ import { Range } from 'vs/editor/common/core/range';
import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot, ValidAnnotatedEditOperation, IValidEditOperation } from 'vs/editor/common/model';
import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase';
import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { countEOL, StringEOL } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
export interface IValidatedEditOperation {
sortIndex: number;
@@ -16,7 +18,10 @@ export interface IValidatedEditOperation {
range: Range;
rangeOffset: number;
rangeLength: number;
lines: string[] | null;
text: string;
eolCount: number;
firstLineLength: number;
lastLineLength: number;
forceMoveMarkers: boolean;
isAutoWhitespaceEdit: boolean;
}
@@ -60,7 +65,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
public getBOM(): string {
return this._BOM;
}
public getEOL(): string {
public getEOL(): '\r\n' | '\n' {
return this._pieceTree.getEOL();
}
@@ -201,7 +206,7 @@ export class PieceTreeTextBuffer implements ITextBuffer {
this._pieceTree.setEOL(newEOL);
}
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult {
public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult {
let mightContainRTL = this._mightContainRTL;
let mightContainNonBasicASCII = this._mightContainNonBasicASCII;
let canReduceOperations = true;
@@ -220,13 +225,34 @@ export class PieceTreeTextBuffer implements ITextBuffer {
if (!mightContainNonBasicASCII && op.text) {
mightContainNonBasicASCII = !strings.isBasicASCII(op.text);
}
let validText = '';
let eolCount = 0;
let firstLineLength = 0;
let lastLineLength = 0;
if (op.text) {
let strEOL: StringEOL;
[eolCount, firstLineLength, lastLineLength, strEOL] = countEOL(op.text);
const bufferEOL = this.getEOL();
const expectedStrEOL = (bufferEOL === '\r\n' ? StringEOL.CRLF : StringEOL.LF);
if (strEOL === StringEOL.Unknown || strEOL === expectedStrEOL) {
validText = op.text;
} else {
validText = op.text.replace(/\r\n|\r|\n/g, bufferEOL);
}
}
operations[i] = {
sortIndex: i,
identifier: op.identifier || null,
range: validatedRange,
rangeOffset: this.getOffsetAt(validatedRange.startLineNumber, validatedRange.startColumn),
rangeLength: this.getValueLengthInRange(validatedRange),
lines: op.text ? op.text.split(/\r\n|\r|\n/) : null,
text: validText,
eolCount: eolCount,
firstLineLength: firstLineLength,
lastLineLength: lastLineLength,
forceMoveMarkers: Boolean(op.forceMoveMarkers),
isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false
};
@@ -254,46 +280,56 @@ export class PieceTreeTextBuffer implements ITextBuffer {
}
// Delta encode operations
let reverseRanges = PieceTreeTextBuffer._getInverseEditRanges(operations);
let reverseRanges = (computeUndoEdits || recordTrimAutoWhitespace ? PieceTreeTextBuffer._getInverseEditRanges(operations) : []);
let newTrimAutoWhitespaceCandidates: { lineNumber: number, oldContent: string }[] = [];
if (recordTrimAutoWhitespace) {
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
if (recordTrimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) {
// Record already the future line numbers that might be auto whitespace removal candidates on next edit
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
let currentLineContent = '';
if (lineNumber === reverseRange.startLineNumber) {
currentLineContent = this.getLineContent(op.range.startLineNumber);
if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
continue;
if (op.isAutoWhitespaceEdit && op.range.isEmpty()) {
// Record already the future line numbers that might be auto whitespace removal candidates on next edit
for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) {
let currentLineContent = '';
if (lineNumber === reverseRange.startLineNumber) {
currentLineContent = this.getLineContent(op.range.startLineNumber);
if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) {
continue;
}
}
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
}
newTrimAutoWhitespaceCandidates.push({ lineNumber: lineNumber, oldContent: currentLineContent });
}
}
}
let reverseOperations: IReverseSingleEditOperation[] = [];
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let reverseRange = reverseRanges[i];
let reverseOperations: IReverseSingleEditOperation[] | null = null;
if (computeUndoEdits) {
reverseOperations[i] = {
sortIndex: op.sortIndex,
identifier: op.identifier,
range: reverseRange,
text: this.getValueInRange(op.range),
forceMoveMarkers: op.forceMoveMarkers
};
let reverseRangeDeltaOffset = 0;
reverseOperations = [];
for (let i = 0; i < operations.length; i++) {
const op = operations[i];
const reverseRange = reverseRanges[i];
const bufferText = this.getValueInRange(op.range);
const reverseRangeOffset = op.rangeOffset + reverseRangeDeltaOffset;
reverseRangeDeltaOffset += (op.text.length - bufferText.length);
reverseOperations[i] = {
sortIndex: op.sortIndex,
identifier: op.identifier,
range: reverseRange,
text: bufferText,
textChange: new TextChange(op.rangeOffset, bufferText, reverseRangeOffset, op.text)
};
}
// Can only sort reverse operations when the order is not significant
if (!hasTouchingRanges) {
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
}
}
// Can only sort reverse operations when the order is not significant
if (!hasTouchingRanges) {
reverseOperations.sort((a, b) => a.sortIndex - b.sortIndex);
}
this._mightContainRTL = mightContainRTL;
this._mightContainNonBasicASCII = mightContainNonBasicASCII;
@@ -350,58 +386,45 @@ export class PieceTreeTextBuffer implements ITextBuffer {
}
_toSingleEditOperation(operations: IValidatedEditOperation[]): IValidatedEditOperation {
let forceMoveMarkers = false,
firstEditRange = operations[0].range,
lastEditRange = operations[operations.length - 1].range,
entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn),
lastEndLineNumber = firstEditRange.startLineNumber,
lastEndColumn = firstEditRange.startColumn,
result: string[] = [];
let forceMoveMarkers = false;
const firstEditRange = operations[0].range;
const lastEditRange = operations[operations.length - 1].range;
const entireEditRange = new Range(firstEditRange.startLineNumber, firstEditRange.startColumn, lastEditRange.endLineNumber, lastEditRange.endColumn);
let lastEndLineNumber = firstEditRange.startLineNumber;
let lastEndColumn = firstEditRange.startColumn;
const result: string[] = [];
for (let i = 0, len = operations.length; i < len; i++) {
let operation = operations[i],
range = operation.range;
const operation = operations[i];
const range = operation.range;
forceMoveMarkers = forceMoveMarkers || operation.forceMoveMarkers;
// (1) -- Push old text
for (let lineNumber = lastEndLineNumber; lineNumber < range.startLineNumber; lineNumber++) {
if (lineNumber === lastEndLineNumber) {
result.push(this.getLineContent(lineNumber).substring(lastEndColumn - 1));
} else {
result.push('\n');
result.push(this.getLineContent(lineNumber));
}
}
if (range.startLineNumber === lastEndLineNumber) {
result.push(this.getLineContent(range.startLineNumber).substring(lastEndColumn - 1, range.startColumn - 1));
} else {
result.push('\n');
result.push(this.getLineContent(range.startLineNumber).substring(0, range.startColumn - 1));
}
result.push(this.getValueInRange(new Range(lastEndLineNumber, lastEndColumn, range.startLineNumber, range.startColumn)));
// (2) -- Push new text
if (operation.lines) {
for (let j = 0, lenJ = operation.lines.length; j < lenJ; j++) {
if (j !== 0) {
result.push('\n');
}
result.push(operation.lines[j]);
}
if (operation.text.length > 0) {
result.push(operation.text);
}
lastEndLineNumber = operation.range.endLineNumber;
lastEndColumn = operation.range.endColumn;
lastEndLineNumber = range.endLineNumber;
lastEndColumn = range.endColumn;
}
const text = result.join('');
const [eolCount, firstLineLength, lastLineLength] = countEOL(text);
return {
sortIndex: 0,
identifier: operations[0].identifier,
range: entireEditRange,
rangeOffset: this.getOffsetAt(entireEditRange.startLineNumber, entireEditRange.startColumn),
rangeLength: this.getValueLengthInRange(entireEditRange, EndOfLinePreference.TextDefined),
lines: result.join('').split('\n'),
text: text,
eolCount: eolCount,
firstLineLength: firstLineLength,
lastLineLength: lastLineLength,
forceMoveMarkers: forceMoveMarkers,
isAutoWhitespaceEdit: false
};
@@ -421,41 +444,26 @@ export class PieceTreeTextBuffer implements ITextBuffer {
const endLineNumber = op.range.endLineNumber;
const endColumn = op.range.endColumn;
if (startLineNumber === endLineNumber && startColumn === endColumn && (!op.lines || op.lines.length === 0)) {
if (startLineNumber === endLineNumber && startColumn === endColumn && op.text.length === 0) {
// no-op
continue;
}
const deletingLinesCnt = endLineNumber - startLineNumber;
const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0);
const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt);
const text = (op.lines ? op.lines.join(this.getEOL()) : '');
if (text) {
if (op.text) {
// replacement
this._pieceTree.delete(op.rangeOffset, op.rangeLength);
this._pieceTree.insert(op.rangeOffset, text, true);
this._pieceTree.insert(op.rangeOffset, op.text, true);
} else {
// deletion
this._pieceTree.delete(op.rangeOffset, op.rangeLength);
}
if (editingLinesCnt < insertingLinesCnt) {
let newLinesContent: string[] = [];
for (let j = editingLinesCnt + 1; j <= insertingLinesCnt; j++) {
newLinesContent.push(op.lines![j]);
}
newLinesContent[newLinesContent.length - 1] = this.getLineContent(startLineNumber + insertingLinesCnt - 1);
}
const contentChangeRange = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
contentChanges.push({
range: contentChangeRange,
rangeLength: op.rangeLength,
text: text,
text: op.text,
rangeOffset: op.rangeOffset,
forceMoveMarkers: op.forceMoveMarkers
});
@@ -504,18 +512,16 @@ export class PieceTreeTextBuffer implements ITextBuffer {
let resultRange: Range;
if (op.lines && op.lines.length > 0) {
if (op.text.length > 0) {
// the operation inserts something
let lineCount = op.lines.length;
let firstLine = op.lines[0];
let lastLine = op.lines[lineCount - 1];
const lineCount = op.eolCount + 1;
if (lineCount === 1) {
// single line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + firstLine.length);
resultRange = new Range(startLineNumber, startColumn, startLineNumber, startColumn + op.firstLineLength);
} else {
// multi line insert
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, lastLine.length + 1);
resultRange = new Range(startLineNumber, startColumn, startLineNumber + lineCount - 1, op.lastLineLength + 1);
}
} else {
// There is nothing to insert

View File

@@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as buffer from 'vs/base/common/buffer';
import { decodeUTF16LE } from 'vs/editor/common/core/stringBuilder';
export class TextChange {
public get oldLength(): number {
return this.oldText.length;
}
public get oldEnd(): number {
return this.oldPosition + this.oldText.length;
}
public get newLength(): number {
return this.newText.length;
}
public get newEnd(): number {
return this.newPosition + this.newText.length;
}
constructor(
public readonly oldPosition: number,
public readonly oldText: string,
public readonly newPosition: number,
public readonly newText: string
) { }
private static _writeStringSize(str: string): number {
return (
4 + 2 * str.length
);
}
private static _writeString(b: Uint8Array, str: string, offset: number): number {
const len = str.length;
buffer.writeUInt32BE(b, len, offset); offset += 4;
for (let i = 0; i < len; i++) {
buffer.writeUInt16LE(b, str.charCodeAt(i), offset); offset += 2;
}
return offset;
}
private static _readString(b: Uint8Array, offset: number): string {
const len = buffer.readUInt32BE(b, offset); offset += 4;
return decodeUTF16LE(b, offset, len);
}
public writeSize(): number {
return (
+ 4 // oldPosition
+ 4 // newPosition
+ TextChange._writeStringSize(this.oldText)
+ TextChange._writeStringSize(this.newText)
);
}
public write(b: Uint8Array, offset: number): number {
buffer.writeUInt32BE(b, this.oldPosition, offset); offset += 4;
buffer.writeUInt32BE(b, this.newPosition, offset); offset += 4;
offset = TextChange._writeString(b, this.oldText, offset);
offset = TextChange._writeString(b, this.newText, offset);
return offset;
}
public static read(b: Uint8Array, offset: number, dest: TextChange[]): number {
const oldPosition = buffer.readUInt32BE(b, offset); offset += 4;
const newPosition = buffer.readUInt32BE(b, offset); offset += 4;
const oldText = TextChange._readString(b, offset); offset += TextChange._writeStringSize(oldText);
const newText = TextChange._readString(b, offset); offset += TextChange._writeStringSize(newText);
dest.push(new TextChange(oldPosition, oldText, newPosition, newText));
return offset;
}
}
export function compressConsecutiveTextChanges(prevEdits: TextChange[] | null, currEdits: TextChange[]): TextChange[] {
if (prevEdits === null || prevEdits.length === 0) {
return currEdits;
}
const compressor = new TextChangeCompressor(prevEdits, currEdits);
return compressor.compress();
}
class TextChangeCompressor {
private _prevEdits: TextChange[];
private _currEdits: TextChange[];
private _result: TextChange[];
private _resultLen: number;
private _prevLen: number;
private _prevDeltaOffset: number;
private _currLen: number;
private _currDeltaOffset: number;
constructor(prevEdits: TextChange[], currEdits: TextChange[]) {
this._prevEdits = prevEdits;
this._currEdits = currEdits;
this._result = [];
this._resultLen = 0;
this._prevLen = this._prevEdits.length;
this._prevDeltaOffset = 0;
this._currLen = this._currEdits.length;
this._currDeltaOffset = 0;
}
public compress(): TextChange[] {
let prevIndex = 0;
let currIndex = 0;
let prevEdit = this._getPrev(prevIndex);
let currEdit = this._getCurr(currIndex);
while (prevIndex < this._prevLen || currIndex < this._currLen) {
if (prevEdit === null) {
this._acceptCurr(currEdit!);
currEdit = this._getCurr(++currIndex);
continue;
}
if (currEdit === null) {
this._acceptPrev(prevEdit);
prevEdit = this._getPrev(++prevIndex);
continue;
}
if (currEdit.oldEnd <= prevEdit.newPosition) {
this._acceptCurr(currEdit);
currEdit = this._getCurr(++currIndex);
continue;
}
if (prevEdit.newEnd <= currEdit.oldPosition) {
this._acceptPrev(prevEdit);
prevEdit = this._getPrev(++prevIndex);
continue;
}
if (currEdit.oldPosition < prevEdit.newPosition) {
const [e1, e2] = TextChangeCompressor._splitCurr(currEdit, prevEdit.newPosition - currEdit.oldPosition);
this._acceptCurr(e1);
currEdit = e2;
continue;
}
if (prevEdit.newPosition < currEdit.oldPosition) {
const [e1, e2] = TextChangeCompressor._splitPrev(prevEdit, currEdit.oldPosition - prevEdit.newPosition);
this._acceptPrev(e1);
prevEdit = e2;
continue;
}
// At this point, currEdit.oldPosition === prevEdit.newPosition
let mergePrev: TextChange;
let mergeCurr: TextChange;
if (currEdit.oldEnd === prevEdit.newEnd) {
mergePrev = prevEdit;
mergeCurr = currEdit;
prevEdit = this._getPrev(++prevIndex);
currEdit = this._getCurr(++currIndex);
} else if (currEdit.oldEnd < prevEdit.newEnd) {
const [e1, e2] = TextChangeCompressor._splitPrev(prevEdit, currEdit.oldLength);
mergePrev = e1;
mergeCurr = currEdit;
prevEdit = e2;
currEdit = this._getCurr(++currIndex);
} else {
const [e1, e2] = TextChangeCompressor._splitCurr(currEdit, prevEdit.newLength);
mergePrev = prevEdit;
mergeCurr = e1;
prevEdit = this._getPrev(++prevIndex);
currEdit = e2;
}
this._result[this._resultLen++] = new TextChange(
mergePrev.oldPosition,
mergePrev.oldText,
mergeCurr.newPosition,
mergeCurr.newText
);
this._prevDeltaOffset += mergePrev.newLength - mergePrev.oldLength;
this._currDeltaOffset += mergeCurr.newLength - mergeCurr.oldLength;
}
const merged = TextChangeCompressor._merge(this._result);
const cleaned = TextChangeCompressor._removeNoOps(merged);
return cleaned;
}
private _acceptCurr(currEdit: TextChange): void {
this._result[this._resultLen++] = TextChangeCompressor._rebaseCurr(this._prevDeltaOffset, currEdit);
this._currDeltaOffset += currEdit.newLength - currEdit.oldLength;
}
private _getCurr(currIndex: number): TextChange | null {
return (currIndex < this._currLen ? this._currEdits[currIndex] : null);
}
private _acceptPrev(prevEdit: TextChange): void {
this._result[this._resultLen++] = TextChangeCompressor._rebasePrev(this._currDeltaOffset, prevEdit);
this._prevDeltaOffset += prevEdit.newLength - prevEdit.oldLength;
}
private _getPrev(prevIndex: number): TextChange | null {
return (prevIndex < this._prevLen ? this._prevEdits[prevIndex] : null);
}
private static _rebaseCurr(prevDeltaOffset: number, currEdit: TextChange): TextChange {
return new TextChange(
currEdit.oldPosition - prevDeltaOffset,
currEdit.oldText,
currEdit.newPosition,
currEdit.newText
);
}
private static _rebasePrev(currDeltaOffset: number, prevEdit: TextChange): TextChange {
return new TextChange(
prevEdit.oldPosition,
prevEdit.oldText,
prevEdit.newPosition + currDeltaOffset,
prevEdit.newText
);
}
private static _splitPrev(edit: TextChange, offset: number): [TextChange, TextChange] {
const preText = edit.newText.substr(0, offset);
const postText = edit.newText.substr(offset);
return [
new TextChange(
edit.oldPosition,
edit.oldText,
edit.newPosition,
preText
),
new TextChange(
edit.oldEnd,
'',
edit.newPosition + offset,
postText
)
];
}
private static _splitCurr(edit: TextChange, offset: number): [TextChange, TextChange] {
const preText = edit.oldText.substr(0, offset);
const postText = edit.oldText.substr(offset);
return [
new TextChange(
edit.oldPosition,
preText,
edit.newPosition,
edit.newText
),
new TextChange(
edit.oldPosition + offset,
postText,
edit.newEnd,
''
)
];
}
private static _merge(edits: TextChange[]): TextChange[] {
if (edits.length === 0) {
return edits;
}
let result: TextChange[] = [], resultLen = 0;
let prev = edits[0];
for (let i = 1; i < edits.length; i++) {
const curr = edits[i];
if (prev.oldEnd === curr.oldPosition) {
// Merge into `prev`
prev = new TextChange(
prev.oldPosition,
prev.oldText + curr.oldText,
prev.newPosition,
prev.newText + curr.newText
);
} else {
result[resultLen++] = prev;
prev = curr;
}
}
result[resultLen++] = prev;
return result;
}
private static _removeNoOps(edits: TextChange[]): TextChange[] {
if (edits.length === 0) {
return edits;
}
let result: TextChange[] = [], resultLen = 0;
for (let i = 0; i < edits.length; i++) {
const edit = edits[i];
if (edit.oldText === edit.newText) {
continue;
}
result[resultLen++] = edit;
}
return result;
}
}

View File

@@ -37,6 +37,7 @@ import { Color } from 'vs/base/common/color';
import { Constants } from 'vs/base/common/uint';
import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { TextChange } from 'vs/editor/common/model/textChange';
function createTextBufferBuilder() {
return new PieceTreeTextBufferBuilder();
@@ -367,7 +368,6 @@ export class TextModel extends Disposable implements model.ITextModel {
this._onWillDispose.fire();
this._languageRegistryListener.dispose();
this._tokenization.dispose();
this._undoRedoService.removeElements(this.uri);
this._isDisposed = true;
super.dispose();
this._isDisposing = false;
@@ -711,7 +711,11 @@ export class TextModel extends Disposable implements model.ITextModel {
this._alternativeVersionId = this._versionId;
}
private _overwriteAlternativeVersionId(newAlternativeVersionId: number): void {
public _overwriteVersionId(versionId: number): void {
this._versionId = versionId;
}
public _overwriteAlternativeVersionId(newAlternativeVersionId: number): void {
this._alternativeVersionId = newAlternativeVersionId;
}
@@ -1285,19 +1289,39 @@ export class TextModel extends Disposable implements model.ITextModel {
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer);
}
_applyUndoRedoEdits(edits: model.IValidEditOperations[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): model.IValidEditOperations[] {
_applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
const edits = changes.map<model.IIdentifiedSingleEditOperation>((change) => {
const rangeStart = this.getPositionAt(change.newPosition);
const rangeEnd = this.getPositionAt(change.newEnd);
return {
range: new Range(rangeStart.lineNumber, rangeStart.column, rangeEnd.lineNumber, rangeEnd.column),
text: change.oldText
};
});
this._applyUndoRedoEdits(edits, eol, true, false, resultingAlternativeVersionId, resultingSelection);
}
_applyRedo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
const edits = changes.map<model.IIdentifiedSingleEditOperation>((change) => {
const rangeStart = this.getPositionAt(change.oldPosition);
const rangeEnd = this.getPositionAt(change.oldEnd);
return {
range: new Range(rangeStart.lineNumber, rangeStart.column, rangeEnd.lineNumber, rangeEnd.column),
text: change.newText
};
});
this._applyUndoRedoEdits(edits, eol, false, true, resultingAlternativeVersionId, resultingSelection);
}
private _applyUndoRedoEdits(edits: model.IIdentifiedSingleEditOperation[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
try {
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
this._isUndoing = isUndoing;
this._isRedoing = isRedoing;
let reverseEdits: model.IValidEditOperations[] = [];
for (let i = 0, len = edits.length; i < len; i++) {
reverseEdits[i] = { operations: this.applyEdits(edits[i].operations) };
}
this.applyEdits(edits, false);
this.setEOL(eol);
this._overwriteAlternativeVersionId(resultingAlternativeVersionId);
return reverseEdits;
} finally {
this._isUndoing = false;
this._isRedoing = false;
@@ -1306,21 +1330,25 @@ export class TextModel extends Disposable implements model.ITextModel {
}
}
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IValidEditOperation[] {
public applyEdits(operations: model.IIdentifiedSingleEditOperation[]): void;
public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
public applyEdits(operations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[];
public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] {
try {
this._onDidChangeDecorations.beginDeferredEmit();
this._eventEmitter.beginDeferredEmit();
return this._doApplyEdits(this._validateEditOperations(rawOperations));
const operations = this._validateEditOperations(rawOperations);
return this._doApplyEdits(operations, computeUndoEdits);
} finally {
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();
}
}
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[]): model.IValidEditOperation[] {
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean): void | model.IValidEditOperation[] {
const oldLineCount = this._buffer.getLineCount();
const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace);
const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace, computeUndoEdits);
const newLineCount = this._buffer.getLineCount();
const contentChanges = result.changes;
@@ -1395,7 +1423,7 @@ export class TextModel extends Disposable implements model.ITextModel {
);
}
return result.reverseEdits;
return (result.reverseEdits === null ? undefined : result.reverseEdits);
}
public undo(): void {

View File

@@ -11,10 +11,18 @@ import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, Toke
import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer';
import { CharCode } from 'vs/base/common/charCode';
export function countEOL(text: string): [number, number, number] {
export const enum StringEOL {
Unknown = 0,
Invalid = 3,
LF = 1,
CRLF = 2
}
export function countEOL(text: string): [number, number, number, StringEOL] {
let eolCount = 0;
let firstLineLength = 0;
let lastLineStart = 0;
let eol: StringEOL = StringEOL.Unknown;
for (let i = 0, len = text.length; i < len; i++) {
const chr = text.charCodeAt(i);
@@ -25,12 +33,16 @@ export function countEOL(text: string): [number, number, number] {
eolCount++;
if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) {
// \r\n... case
eol |= StringEOL.CRLF;
i++; // skip \n
} else {
// \r... case
eol |= StringEOL.Invalid;
}
lastLineStart = i + 1;
} else if (chr === CharCode.LineFeed) {
// \n... case
eol |= StringEOL.LF;
if (eolCount === 0) {
firstLineLength = i;
}
@@ -41,7 +53,7 @@ export function countEOL(text: string): [number, number, number] {
if (eolCount === 0) {
firstLineLength = text.length;
}
return [eolCount, firstLineLength, text.length - lastLineStart];
return [eolCount, firstLineLength, text.length - lastLineStart, eol];
}
function getDefaultMetadata(topLevelLanguageId: LanguageId): number {

View File

@@ -1396,6 +1396,15 @@ export interface AuthenticationSession {
accountName: string;
}
/**
* @internal
*/
export interface AuthenticationSessionsChangeEvent {
added: string[];
removed: string[];
changed: string[];
}
export interface Command {
id: string;
title: string;

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
@@ -25,7 +26,14 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
import { StringSHA1 } from 'vs/base/common/hash';
import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { Schemas } from 'vs/base/common/network';
import Severity from 'vs/base/common/severity';
export const MAINTAIN_UNDO_REDO_STACK = true;
export interface IEditorSemanticHighlightingOptions {
enabled?: boolean;
@@ -35,6 +43,18 @@ function MODEL_ID(resource: URI): string {
return resource.toString();
}
function computeModelSha1(model: ITextModel): string {
// compute the sha1
const shaComputer = new StringSHA1();
const snapshot = model.createSnapshot();
let text: string | null;
while ((text = snapshot.read())) {
shaComputer.update(text);
}
return shaComputer.digest();
}
class ModelData implements IDisposable {
public readonly model: ITextModel;
@@ -98,13 +118,42 @@ interface IRawConfig {
const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF;
export class ModelServiceImpl extends Disposable implements IModelService {
public _serviceBrand: undefined;
interface EditStackPastFutureElements {
past: EditStackElement[];
future: EditStackElement[];
}
private readonly _configurationService: IConfigurationService;
private readonly _configurationServiceSubscription: IDisposable;
private readonly _resourcePropertiesService: ITextResourcePropertiesService;
private readonly _undoRedoService: IUndoRedoService;
function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements {
return (isEditStackElements(undoElements.past) && isEditStackElements(undoElements.future));
}
function isEditStackElements(elements: IUndoRedoElement[]): elements is EditStackElement[] {
for (const element of elements) {
if (element instanceof SingleModelEditStackElement) {
continue;
}
if (element instanceof MultiModelEditStackElement) {
continue;
}
return false;
}
return true;
}
class DisposedModelInfo {
constructor(
public readonly uri: URI,
public readonly sha1: string,
public readonly versionId: number,
public readonly alternativeVersionId: number,
) { }
}
export class ModelServiceImpl extends Disposable implements IModelService {
private static _PROMPT_UNDO_REDO_SIZE_LIMIT = 10 * 1024 * 1024; // 10MB
public _serviceBrand: undefined;
private readonly _onModelAdded: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());
public readonly onModelAdded: Event<ITextModel> = this._onModelAdded.event;
@@ -115,39 +164,37 @@ export class ModelServiceImpl extends Disposable implements IModelService {
private readonly _onModelModeChanged: Emitter<{ model: ITextModel; oldModeId: string; }> = this._register(new Emitter<{ model: ITextModel; oldModeId: string; }>());
public readonly onModelModeChanged: Event<{ model: ITextModel; oldModeId: string; }> = this._onModelModeChanged.event;
private _modelCreationOptionsByLanguageAndResource: {
[languageAndResource: string]: ITextModelCreationOptions;
};
private _modelCreationOptionsByLanguageAndResource: { [languageAndResource: string]: ITextModelCreationOptions; };
/**
* All the models known in the system.
*/
private readonly _models: { [modelId: string]: ModelData; };
private readonly _disposedModels: Map<string, DisposedModelInfo>;
constructor(
@IConfigurationService configurationService: IConfigurationService,
@ITextResourcePropertiesService resourcePropertiesService: ITextResourcePropertiesService,
@IThemeService themeService: IThemeService,
@ILogService logService: ILogService,
@IUndoRedoService undoRedoService: IUndoRedoService
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService,
@IThemeService private readonly _themeService: IThemeService,
@ILogService private readonly _logService: ILogService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@IDialogService private readonly _dialogService: IDialogService,
) {
super();
this._configurationService = configurationService;
this._resourcePropertiesService = resourcePropertiesService;
this._undoRedoService = undoRedoService;
this._models = {};
this._modelCreationOptionsByLanguageAndResource = Object.create(null);
this._models = {};
this._disposedModels = new Map<string, DisposedModelInfo>();
this._configurationServiceSubscription = this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions());
this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()));
this._updateModelOptions();
this._register(new SemanticColoringFeature(this, themeService, configurationService, logService));
this._register(new SemanticColoringFeature(this, this._themeService, this._configurationService, this._logService));
}
private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions {
let tabSize = EDITOR_MODEL_DEFAULTS.tabSize;
if (config.editor && typeof config.editor.tabSize !== 'undefined') {
let parsedTabSize = parseInt(config.editor.tabSize, 10);
const parsedTabSize = parseInt(config.editor.tabSize, 10);
if (!isNaN(parsedTabSize)) {
tabSize = parsedTabSize;
}
@@ -158,7 +205,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
let indentSize = tabSize;
if (config.editor && typeof config.editor.indentSize !== 'undefined' && config.editor.indentSize !== 'tabSize') {
let parsedIndentSize = parseInt(config.editor.indentSize, 10);
const parsedIndentSize = parseInt(config.editor.indentSize, 10);
if (!isNaN(parsedIndentSize)) {
indentSize = parsedIndentSize;
}
@@ -230,14 +277,14 @@ export class ModelServiceImpl extends Disposable implements IModelService {
}
private _updateModelOptions(): void {
let oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource;
const oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource;
this._modelCreationOptionsByLanguageAndResource = Object.create(null);
// Update options on all models
let keys = Object.keys(this._models);
const keys = Object.keys(this._models);
for (let i = 0, len = keys.length; i < len; i++) {
let modelId = keys[i];
let modelData = this._models[modelId];
const modelId = keys[i];
const modelData = this._models[modelId];
const language = modelData.model.getLanguageIdentifier().language;
const uri = modelData.model.uri;
const oldOptions = oldOptionsByLanguageAndResource[language + uri];
@@ -277,17 +324,30 @@ export class ModelServiceImpl extends Disposable implements IModelService {
}
}
public dispose(): void {
this._configurationServiceSubscription.dispose();
super.dispose();
}
// --- begin IModelService
private _createModelData(value: string | ITextBufferFactory, languageIdentifier: LanguageIdentifier, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {
// create & save the model
const options = this.getCreationOptions(languageIdentifier.language, resource, isForSimpleWidget);
const model: TextModel = new TextModel(value, options, languageIdentifier, resource, this._undoRedoService);
if (resource && this._disposedModels.has(MODEL_ID(resource))) {
const disposedModelData = this._disposedModels.get(MODEL_ID(resource))!;
this._disposedModels.delete(MODEL_ID(resource));
const elements = this._undoRedoService.getElements(resource);
if (computeModelSha1(model) === disposedModelData.sha1 && isEditStackPastFutureElements(elements)) {
for (const element of elements.past) {
element.setModel(model);
}
for (const element of elements.future) {
element.setModel(model);
}
this._undoRedoService.setElementsIsValid(resource, true);
model._overwriteVersionId(disposedModelData.versionId);
model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId);
} else {
this._undoRedoService.removeElements(resource);
}
}
const modelId = MODEL_ID(model.uri);
if (this._models[modelId]) {
@@ -360,7 +420,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
const commonSuffix = this._commonSuffix(model, modelLineCount - commonPrefix, commonPrefix, textBuffer, textBufferLineCount - commonPrefix, commonPrefix);
let oldRange: Range, newRange: Range;
let oldRange: Range;
let newRange: Range;
if (commonSuffix > 0) {
oldRange = new Range(commonPrefix + 1, 1, modelLineCount - commonSuffix + 1, 1);
newRange = new Range(commonPrefix + 1, 1, textBufferLineCount - commonSuffix + 1, 1);
@@ -394,7 +455,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
if (!languageSelection) {
return;
}
let modelData = this._models[MODEL_ID(model.uri)];
const modelData = this._models[MODEL_ID(model.uri)];
if (!modelData) {
return;
}
@@ -403,19 +464,69 @@ export class ModelServiceImpl extends Disposable implements IModelService {
public destroyModel(resource: URI): void {
// We need to support that not all models get disposed through this service (i.e. model.dispose() should work!)
let modelData = this._models[MODEL_ID(resource)];
const modelData = this._models[MODEL_ID(resource)];
if (!modelData) {
return;
}
const model = modelData.model;
let maintainUndoRedoStack = false;
let heapSize = 0;
if (MAINTAIN_UNDO_REDO_STACK && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote)) {
const elements = this._undoRedoService.getElements(resource);
if ((elements.past.length > 0 || elements.future.length > 0) && isEditStackPastFutureElements(elements)) {
maintainUndoRedoStack = true;
for (const element of elements.past) {
heapSize += element.heapSize(resource);
element.setModel(resource); // remove reference from text buffer instance
}
for (const element of elements.future) {
heapSize += element.heapSize(resource);
element.setModel(resource); // remove reference from text buffer instance
}
} else {
maintainUndoRedoStack = false;
}
}
if (maintainUndoRedoStack) {
// We only invalidate the elements, but they remain in the undo-redo service.
this._undoRedoService.setElementsIsValid(resource, false);
this._disposedModels.set(MODEL_ID(resource), new DisposedModelInfo(resource, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId()));
} else {
this._undoRedoService.removeElements(resource);
}
modelData.model.dispose();
// After disposing the model, prompt and ask if we should keep the undo-redo stack
if (maintainUndoRedoStack && heapSize > ModelServiceImpl._PROMPT_UNDO_REDO_SIZE_LIMIT) {
const mbSize = (heapSize / 1024 / 1024).toFixed(1);
this._dialogService.show(
Severity.Info,
nls.localize('undoRedoConfirm', "Keep the undo-redo stack for {0} in memory ({1} MB)?", (resource.scheme === Schemas.file ? resource.fsPath : resource.path), mbSize),
[
nls.localize('nok', "Discard"),
nls.localize('ok', "Keep"),
],
{
cancelId: 2
}
).then((result) => {
const discard = (result.choice === 2 || result.choice === 0);
if (discard) {
this._disposedModels.delete(MODEL_ID(resource));
this._undoRedoService.removeElements(resource);
}
});
}
}
public getModels(): ITextModel[] {
let ret: ITextModel[] = [];
const ret: ITextModel[] = [];
let keys = Object.keys(this._models);
const keys = Object.keys(this._models);
for (let i = 0, len = keys.length; i < len; i++) {
let modelId = keys[i];
const modelId = keys[i];
ret.push(this._models[modelId].model);
}
@@ -423,8 +534,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
}
public getModel(resource: URI): ITextModel | null {
let modelId = MODEL_ID(resource);
let modelData = this._models[modelId];
const modelId = MODEL_ID(resource);
const modelData = this._models[modelId];
if (!modelData) {
return null;
}
@@ -434,8 +545,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
// --- end IModelService
private _onWillDispose(model: ITextModel): void {
let modelId = MODEL_ID(model.uri);
let modelData = this._models[modelId];
const modelId = MODEL_ID(model.uri);
const modelData = this._models[modelId];
delete this._models[modelId];
modelData.dispose();

View File

@@ -36,7 +36,7 @@ export namespace InspectTokensNLS {
}
export namespace GoToLineNLS {
export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line...");
export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line/Column...");
}
export namespace QuickHelpNLS {