diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index 2cc107d4037d7..dc63ec06edc51 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -78,15 +78,15 @@ export class DecorationsOverlay extends DynamicViewOverlay { let decorations: ViewModelDecoration[] = [], decorationsLen = 0; for (let i = 0, len = _decorations.length; i < len; i++) { let d = _decorations[i]; - if (d.source.options.className) { + if (d.options.className) { decorations[decorationsLen++] = d; } } // Sort decorations for consistent render output decorations = decorations.sort((a, b) => { - let aClassName = a.source.options.className; - let bClassName = b.source.options.className; + let aClassName = a.options.className; + let bClassName = b.options.className; if (aClassName < bClassName) { return -1; @@ -120,13 +120,13 @@ export class DecorationsOverlay extends DynamicViewOverlay { for (let i = 0, lenI = decorations.length; i < lenI; i++) { let d = decorations[i]; - if (!d.source.options.isWholeLine) { + if (!d.options.isWholeLine) { continue; } let decorationOutput = ( '
' @@ -148,12 +148,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { for (let i = 0, lenI = decorations.length; i < lenI; i++) { const d = decorations[i]; - if (d.source.options.isWholeLine) { + if (d.options.isWholeLine) { continue; } - const className = d.source.options.className; - const showIfCollapsed = d.source.options.showIfCollapsed; + const className = d.options.className; + const showIfCollapsed = d.options.showIfCollapsed; let range = d.range; if (showIfCollapsed && range.endColumn === 1 && range.endLineNumber !== range.startLineNumber) { diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 33874c27b5170..757305ebc08d3 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -145,7 +145,7 @@ export class GlyphMarginOverlay extends DedupOverlay { let r: DecorationToRender[] = [], rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { let d = decorations[i]; - let glyphMarginClassName = d.source.options.glyphMarginClassName; + let glyphMarginClassName = d.options.glyphMarginClassName; if (glyphMarginClassName) { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName); } diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts index 3a143124040d2..437677880960b 100644 --- a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts +++ b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts @@ -73,7 +73,7 @@ export class LinesDecorationsOverlay extends DedupOverlay { let r: DecorationToRender[] = [], rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { let d = decorations[i]; - let linesDecorationsClassName = d.source.options.linesDecorationsClassName; + let linesDecorationsClassName = d.options.linesDecorationsClassName; if (linesDecorationsClassName) { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName); } diff --git a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts index b83c9faa123ce..75ff0a0e3ed95 100644 --- a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts +++ b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts @@ -63,7 +63,7 @@ export class MarginViewLineDecorationsOverlay extends DedupOverlay { let r: DecorationToRender[] = [], rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { let d = decorations[i]; - let marginClassName = d.source.options.marginClassName; + let marginClassName = d.options.marginClassName; if (marginClassName) { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName); } diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 91ebeaf394502..5add770087e43 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -6,262 +6,399 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { OverviewRulerImpl } from 'vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { Position } from 'vs/editor/common/core/position'; import { TokenizationRegistry } from 'vs/editor/common/modes'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry'; import { Color } from 'vs/base/common/color'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { ITheme } from 'vs/platform/theme/common/themeService'; +import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -export class DecorationsOverviewRuler extends ViewPart { +class Settings { - static MIN_DECORATION_HEIGHT = 6; - static MAX_DECORATION_HEIGHT = 60; + public readonly lineHeight: number; + public readonly pixelRatio: number; + public readonly overviewRulerLanes: number; - private readonly _tokensColorTrackerListener: IDisposable; + public readonly renderBorder: boolean; + public readonly borderColor: string; - private _overviewRuler: OverviewRulerImpl; + public readonly hideCursor: boolean; + public readonly cursorColor: string; - private _renderBorder: boolean; - private _borderColor: string; - private _cursorColor: string; + public readonly themeType: 'light' | 'dark' | 'hc'; + public readonly backgroundColor: string; - private _shouldUpdateDecorations: boolean; - private _shouldUpdateCursorPosition: boolean; + public readonly top: number; + public readonly right: number; + public readonly domWidth: number; + public readonly domHeight: number; + public readonly canvasWidth: number; + public readonly canvasHeight: number; - private _hideCursor: boolean; - private _cursorPositions: Position[]; + public readonly x: number[]; + public readonly w: number[]; + + constructor(config: editorCommon.IConfiguration, theme: ITheme) { + this.lineHeight = config.editor.lineHeight; + this.pixelRatio = config.editor.pixelRatio; + this.overviewRulerLanes = config.editor.viewInfo.overviewRulerLanes; + + this.renderBorder = config.editor.viewInfo.overviewRulerBorder; + const borderColor = theme.getColor(editorOverviewRulerBorder); + this.borderColor = borderColor ? borderColor.toString() : null; - private _zonesFromDecorations: OverviewRulerZone[]; - private _zonesFromCursors: OverviewRulerZone[]; + this.hideCursor = config.editor.viewInfo.hideCursorInOverviewRuler; + const cursorColor = theme.getColor(editorCursorForeground); + this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; + + this.themeType = theme.type; + + const minimapEnabled = config.editor.viewInfo.minimap.enabled; + const backgroundColor = (minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null); + this.backgroundColor = (backgroundColor ? Color.Format.CSS.formatHex(backgroundColor) : null); + + const position = config.editor.layoutInfo.overviewRuler; + this.top = position.top; + this.right = position.right; + this.domWidth = position.width; + this.domHeight = position.height; + this.canvasWidth = (this.domWidth * this.pixelRatio) | 0; + this.canvasHeight = (this.domHeight * this.pixelRatio) | 0; + + const [x, w] = this._initLanes(1, this.canvasWidth, this.overviewRulerLanes); + this.x = x; + this.w = w; + } + + private _initLanes(canvasLeftOffset: number, canvasWidth: number, laneCount: number): [number[], number[]] { + const remainingWidth = canvasWidth - canvasLeftOffset; + + if (laneCount >= 3) { + const leftWidth = Math.floor(remainingWidth / 3); + const rightWidth = Math.floor(remainingWidth / 3); + const centerWidth = remainingWidth - leftWidth - rightWidth; + const leftOffset = canvasLeftOffset; + const centerOffset = leftOffset + leftWidth; + const rightOffset = leftOffset + leftWidth + centerWidth; + + return [ + [ + 0, + leftOffset, // Left + centerOffset, // Center + leftOffset, // Left | Center + rightOffset, // Right + leftOffset, // Left | Right + centerOffset, // Center | Right + leftOffset, // Left | Center | Right + ], [ + 0, + leftWidth, // Left + centerWidth, // Center + leftWidth + centerWidth, // Left | Center + rightWidth, // Right + leftWidth + centerWidth + rightWidth, // Left | Right + centerWidth + rightWidth, // Center | Right + leftWidth + centerWidth + rightWidth, // Left | Center | Right + ] + ]; + } else if (laneCount === 2) { + const leftWidth = Math.floor(remainingWidth / 2); + const rightWidth = remainingWidth - leftWidth; + const leftOffset = canvasLeftOffset; + const rightOffset = leftOffset + leftWidth; + + return [ + [ + 0, + leftOffset, // Left + leftOffset, // Center + leftOffset, // Left | Center + rightOffset, // Right + leftOffset, // Left | Right + leftOffset, // Center | Right + leftOffset, // Left | Center | Right + ], [ + 0, + leftWidth, // Left + leftWidth, // Center + leftWidth, // Left | Center + rightWidth, // Right + leftWidth + rightWidth, // Left | Right + leftWidth + rightWidth, // Center | Right + leftWidth + rightWidth, // Left | Center | Right + ] + ]; + } else { + const offset = canvasLeftOffset; + const width = remainingWidth; + + return [ + [ + 0, + offset, // Left + offset, // Center + offset, // Left | Center + offset, // Right + offset, // Left | Right + offset, // Center | Right + offset, // Left | Center | Right + ], [ + 0, + width, // Left + width, // Center + width, // Left | Center + width, // Right + width, // Left | Right + width, // Center | Right + width, // Left | Center | Right + ] + ]; + } + } + + public equals(other: Settings): boolean { + return ( + this.lineHeight === other.lineHeight + && this.pixelRatio === other.pixelRatio + && this.overviewRulerLanes === other.overviewRulerLanes + && this.renderBorder === other.renderBorder + && this.borderColor === other.borderColor + && this.hideCursor === other.hideCursor + && this.cursorColor === other.cursorColor + && this.themeType === other.themeType + && this.backgroundColor === other.backgroundColor + && this.top === other.top + && this.right === other.right + && this.domWidth === other.domWidth + && this.domHeight === other.domHeight + && this.canvasWidth === other.canvasWidth + && this.canvasHeight === other.canvasHeight + ); + } +} + +const enum Constants { + MIN_DECORATION_HEIGHT = 6 +} + +const enum OverviewRulerLane { + Left = 1, + Center = 2, + Right = 4, + Full = 7 +} + +export class DecorationsOverviewRuler extends ViewPart { + + private readonly _tokensColorTrackerListener: IDisposable; + private readonly _domNode: FastDomNode; + private _settings: Settings; + private _cursorPositions: Position[]; constructor(context: ViewContext) { super(context); - this._overviewRuler = new OverviewRulerImpl( - 1, - 'decorationsOverviewRuler', - this._context.viewLayout.getScrollHeight(), - this._context.configuration.editor.lineHeight, - this._context.configuration.editor.pixelRatio, - DecorationsOverviewRuler.MIN_DECORATION_HEIGHT, - DecorationsOverviewRuler.MAX_DECORATION_HEIGHT, - (lineNumber: number) => this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber) - ); - this._overviewRuler.setLanesCount(this._context.configuration.editor.viewInfo.overviewRulerLanes, false); - this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false); - this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder; + this._domNode = createFastDomNode(document.createElement('canvas')); + this._domNode.setClassName('decorationsOverviewRuler'); + this._domNode.setPosition('absolute'); + this._domNode.setLayerHinting(true); - this._updateColors(); + this._settings = null; + this._updateSettings(false); - this._updateBackground(false); this._tokensColorTrackerListener = TokenizationRegistry.onDidChange((e) => { if (e.changedColorMap) { - this._updateBackground(true); + this._updateSettings(true); } }); - this._shouldUpdateDecorations = true; - this._zonesFromDecorations = []; - - this._shouldUpdateCursorPosition = true; - this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler; - - this._zonesFromCursors = []; this._cursorPositions = []; } public dispose(): void { super.dispose(); - this._overviewRuler.dispose(); this._tokensColorTrackerListener.dispose(); } - private _updateBackground(render: boolean): void { - const minimapEnabled = this._context.configuration.editor.viewInfo.minimap.enabled; - this._overviewRuler.setUseBackground((minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null), render); - } - - // ---- begin view event handlers - - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - let prevLanesCount = this._overviewRuler.getLanesCount(); - let newLanesCount = this._context.configuration.editor.viewInfo.overviewRulerLanes; - - if (prevLanesCount !== newLanesCount) { - this._overviewRuler.setLanesCount(newLanesCount, false); + private _updateSettings(renderNow: boolean): boolean { + const newSettings = new Settings(this._context.configuration, this._context.theme); + if (this._settings !== null && this._settings.equals(newSettings)) { + // nothing to do + return false; } - if (e.lineHeight) { - this._overviewRuler.setLineHeight(this._context.configuration.editor.lineHeight, false); - } + this._settings = newSettings; - if (e.pixelRatio) { - this._overviewRuler.setPixelRatio(this._context.configuration.editor.pixelRatio, false); - } + this._domNode.setTop(this._settings.top); + this._domNode.setRight(this._settings.right); + this._domNode.setWidth(this._settings.domWidth); + this._domNode.setHeight(this._settings.domHeight); + this._domNode.domNode.width = this._settings.canvasWidth; + this._domNode.domNode.height = this._settings.canvasHeight; - if (e.viewInfo) { - this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder; - this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler; - this._shouldUpdateCursorPosition = true; - this._updateBackground(false); - } - - if (e.layoutInfo) { - this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false); + if (renderNow) { + this._render(); } return true; } + // ---- begin view event handlers + + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + return this._updateSettings(false); + } public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { - this._shouldUpdateCursorPosition = true; this._cursorPositions = []; for (let i = 0, len = e.selections.length; i < len; i++) { this._cursorPositions[i] = e.selections[i].getPosition(); } return true; } - public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - this._shouldUpdateDecorations = true; return true; } - public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { - this._shouldUpdateCursorPosition = true; - this._shouldUpdateDecorations = true; return true; } - public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { - this._overviewRuler.setScrollHeight(e.scrollHeight, false); - return super.onScrollChanged(e) || e.scrollHeightChanged; + return e.scrollHeightChanged; } - public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; } - public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - this._updateColors(); - this._shouldUpdateDecorations = true; - this._shouldUpdateCursorPosition = true; - return true; + // invalidate color cache + this._context.model.invalidateOverviewRulerColorCache(); + return this._updateSettings(false); } // ---- end view event handlers public getDomNode(): HTMLElement { - return this._overviewRuler.getDomNode(); + return this._domNode.domNode; } - private _updateColors() { - let borderColor = this._context.theme.getColor(editorOverviewRulerBorder); - this._borderColor = borderColor ? borderColor.toString() : null; - - let cursorColor = this._context.theme.getColor(editorCursorForeground); - this._cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; - - this._overviewRuler.setThemeType(this._context.theme.type, false); - } - - private _createZonesFromDecorations(): OverviewRulerZone[] { - let decorations = this._context.model.getAllOverviewRulerDecorations(); - let zones: OverviewRulerZone[] = []; - - for (let i = 0, len = decorations.length; i < len; i++) { - let dec = decorations[i]; - let overviewRuler = dec.source.options.overviewRuler; - zones[i] = new OverviewRulerZone( - dec.range.startLineNumber, - dec.range.endLineNumber, - overviewRuler.position, - 0, - this.resolveRulerColor(overviewRuler.color), - this.resolveRulerColor(overviewRuler.darkColor), - this.resolveRulerColor(overviewRuler.hcColor) - ); - } - - return zones; + public prepareRender(ctx: RenderingContext): void { + // Nothing to read } - private resolveRulerColor(color: string | ThemeColor): string { - if (editorCommon.isThemeColor(color)) { - let c = this._context.theme.getColor(color.id) || Color.transparent; - return c.toString(); - } - return color; + public render(editorCtx: RestrictedRenderingContext): void { + this._render(); } - private _createZonesFromCursors(): OverviewRulerZone[] { - let zones: OverviewRulerZone[] = []; - - for (let i = 0, len = this._cursorPositions.length; i < len; i++) { - let cursor = this._cursorPositions[i]; - - zones[i] = new OverviewRulerZone( - cursor.lineNumber, - cursor.lineNumber, - editorCommon.OverviewRulerLane.Full, - 2, - this._cursorColor, - this._cursorColor, - this._cursorColor - ); + private _render(): void { + const canvasWidth = this._settings.canvasWidth; + const canvasHeight = this._settings.canvasHeight; + const lineHeight = this._settings.lineHeight; + const viewLayout = this._context.viewLayout; + const outerHeight = this._context.viewLayout.getScrollHeight(); + const heightRatio = canvasHeight / outerHeight; + const decorations = this._context.model.getAllOverviewRulerDecorations(this._context.theme); + + const minDecorationHeight = (Constants.MIN_DECORATION_HEIGHT * this._settings.pixelRatio) | 0; + const halfMinDecorationHeight = (minDecorationHeight / 2) | 0; + + const canvasCtx = this._domNode.domNode.getContext('2d'); + if (this._settings.backgroundColor === null) { + canvasCtx.clearRect(0, 0, canvasWidth, canvasHeight); + } else { + canvasCtx.fillStyle = this._settings.backgroundColor; + canvasCtx.fillRect(0, 0, canvasWidth, canvasHeight); } - return zones; - } - - public prepareRender(ctx: RenderingContext): void { - // Nothing to read - } - - public render(ctx: RestrictedRenderingContext): void { - if (this._shouldUpdateDecorations || this._shouldUpdateCursorPosition) { - - if (this._shouldUpdateDecorations) { - this._shouldUpdateDecorations = false; - this._zonesFromDecorations = this._createZonesFromDecorations(); - } + const x = this._settings.x; + const w = this._settings.w; + // Avoid flickering by always rendering the colors in the same order + // colors that don't use transparency will be sorted last (they start with #) + const colors = Object.keys(decorations); + colors.sort(); + for (let cIndex = 0, cLen = colors.length; cIndex < cLen; cIndex++) { + const color = colors[cIndex]; + + const colorDecorations = decorations[color]; + + canvasCtx.fillStyle = color; + + let prevLane = 0; + let prevY1 = 0; + let prevY2 = 0; + for (let i = 0, len = colorDecorations.length; i < len; i++) { + const lane = colorDecorations[3 * i]; + const startLineNumber = colorDecorations[3 * i + 1]; + const endLineNumber = colorDecorations[3 * i + 2]; + + let y1 = (viewLayout.getVerticalOffsetForLineNumber(startLineNumber) * heightRatio) | 0; + let y2 = ((viewLayout.getVerticalOffsetForLineNumber(endLineNumber) + lineHeight) * heightRatio) | 0; + let height = y2 - y1; + if (height < minDecorationHeight) { + let yCenter = ((y1 + y2) / 2) | 0; + if (yCenter < halfMinDecorationHeight) { + yCenter = halfMinDecorationHeight; + } else if (yCenter + halfMinDecorationHeight > canvasHeight) { + yCenter = canvasHeight - halfMinDecorationHeight; + } + y1 = yCenter - halfMinDecorationHeight; + y2 = yCenter + halfMinDecorationHeight; + } - if (this._shouldUpdateCursorPosition) { - this._shouldUpdateCursorPosition = false; - if (this._hideCursor) { - this._zonesFromCursors = []; + if (y1 > prevY2 + 1 || lane !== prevLane) { + // flush prev + if (i !== 0) { + canvasCtx.fillRect(x[prevLane], prevY1, w[prevLane], prevY2 - prevY1); + } + prevLane = lane; + prevY1 = y1; + prevY2 = y2; } else { - this._zonesFromCursors = this._createZonesFromCursors(); + // merge into prev + if (y2 > prevY2) { + prevY2 = y2; + } } } - - let allZones: OverviewRulerZone[] = []; - allZones = allZones.concat(this._zonesFromCursors); - allZones = allZones.concat(this._zonesFromDecorations); - - this._overviewRuler.setZones(allZones, false); + canvasCtx.fillRect(x[prevLane], prevY1, w[prevLane], prevY2 - prevY1); } - let hasRendered = this._overviewRuler.render(false); + // Draw cursors + if (!this._settings.hideCursor) { + const cursorHeight = (2 * this._settings.pixelRatio) | 0; + const halfCursorHeight = (cursorHeight / 2) | 0; + const x = this._settings.x[OverviewRulerLane.Full]; + const w = this._settings.w[OverviewRulerLane.Full]; + canvasCtx.fillStyle = this._settings.cursorColor; + for (let i = 0, len = this._cursorPositions.length; i < len; i++) { + const cursor = this._cursorPositions[i]; + + let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0; + if (yCenter < halfCursorHeight) { + yCenter = halfCursorHeight; + } else if (yCenter + halfCursorHeight > canvasHeight) { + yCenter = canvasHeight - halfCursorHeight; + } + const y1 = yCenter - halfCursorHeight; - if (hasRendered && this._renderBorder && this._borderColor && this._overviewRuler.getLanesCount() > 0 && (this._zonesFromDecorations.length > 0 || this._zonesFromCursors.length > 0)) { - let ctx2 = this._overviewRuler.getDomNode().getContext('2d'); - ctx2.beginPath(); - ctx2.lineWidth = 1; - ctx2.strokeStyle = this._borderColor; - ctx2.moveTo(0, 0); - ctx2.lineTo(0, this._overviewRuler.getPixelHeight()); - ctx2.stroke(); + canvasCtx.fillRect(x, y1, w, cursorHeight); + } + } - ctx2.moveTo(0, 0); - ctx2.lineTo(this._overviewRuler.getPixelWidth(), 0); - ctx2.stroke(); + if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) { + canvasCtx.beginPath(); + canvasCtx.lineWidth = 1; + canvasCtx.strokeStyle = this._settings.borderColor; + canvasCtx.moveTo(0, 0); + canvasCtx.lineTo(0, canvasHeight); + canvasCtx.stroke(); + + canvasCtx.moveTo(0, 0); + canvasCtx.lineTo(canvasWidth, 0); + canvasCtx.stroke(); } } } + diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 2c38ec938d230..4881d847e08a8 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -30,6 +30,7 @@ import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/ed import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; let EDITOR_ID = 0; @@ -856,6 +857,26 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationsIds, newModelDecorations); } + public setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void { + + // remove decoration sub types that are no longer used, deregister decoration type if necessary + let oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {}; + for (let subType in oldDecorationsSubTypes) { + this._removeDecorationType(decorationTypeKey + '-' + subType); + } + this._decorationTypeSubtypes[decorationTypeKey] = {}; + + const opts = ModelDecorationOptions.createDynamic(this._resolveDecorationOptions(decorationTypeKey, false)); + let newModelDecorations: editorCommon.IModelDeltaDecoration[] = new Array(ranges.length); + for (let i = 0, len = ranges.length; i < len; i++) { + newModelDecorations[i] = { range: ranges[i], options: opts }; + } + + // update all decorations + let oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey] || []; + this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationsIds, newModelDecorations); + } + public removeDecorations(decorationTypeKey: string): void { // remove decorations for type and sub type let oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey]; diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 65bc4059e5c82..bbe2d1aa6eee3 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -555,8 +555,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { interface IExecContext { readonly model: editorCommon.IModel; readonly selectionsBefore: Selection[]; - readonly selectionStartMarkers: string[]; - readonly positionMarkers: string[]; + readonly trackedRanges: string[]; + readonly trackedRangesDirection: SelectionDirection[]; } interface ICommandData { @@ -576,15 +576,14 @@ class CommandExecutor { const ctx: IExecContext = { model: model, selectionsBefore: selectionsBefore, - selectionStartMarkers: [], - positionMarkers: [] + trackedRanges: [], + trackedRangesDirection: [] }; 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]); + for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) { + ctx.model._setTrackedRange(ctx.trackedRanges[i], null, editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } return result; @@ -661,9 +660,11 @@ class CommandExecutor { 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); + const range = ctx.model._getTrackedRange(ctx.trackedRanges[idx]); + if (ctx.trackedRangesDirection[idx] === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); } }); } else { @@ -750,37 +751,31 @@ class CommandExecutor { }; const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { - let selectionMarkerStickToPreviousCharacter: boolean; - let positionMarkerStickToPreviousCharacter: boolean; - + let stickiness: editorCommon.TrackedRangeStickiness; if (selection.isEmpty()) { - // Try to lock it with surrounding text if (typeof trackPreviousOnEmpty === 'boolean') { - selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; + if (trackPreviousOnEmpty) { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; + } else { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; + } } else { + // Try to lock it with surrounding text const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); if (selection.startColumn === maxLineColumn) { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = true; + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; } else { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = false; + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; } } } else { - if (selection.getDirection() === SelectionDirection.LTR) { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = true; - } else { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = false; - } + stickiness = editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; } - 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); + const l = ctx.trackedRanges.length; + const id = ctx.model._setTrackedRange(null, selection, stickiness); + ctx.trackedRanges[l] = id; + ctx.trackedRangesDirection[l] = selection.getDirection(); return l.toString(); }; diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index ae9c990ec4a3d..fc2690244fb60 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -8,16 +8,21 @@ import { SingleCursorState, CursorContext, CursorState } from 'vs/editor/common/ 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'; +import { TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; export class OneCursor { public modelState: SingleCursorState; public viewState: SingleCursorState; - private _selStartMarker: string; - private _selEndMarker: string; + private _selTrackedRange: string; constructor(context: CursorContext) { + this.modelState = null; + this.viewState = null; + + this._selTrackedRange = null; + this._setState( context, new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0), @@ -26,8 +31,7 @@ export class OneCursor { } public dispose(context: CursorContext): void { - context.model._removeMarker(this._selStartMarker); - context.model._removeMarker(this._selEndMarker); + this._selTrackedRange = context.model._setTrackedRange(this._selTrackedRange, null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } public asCursorState(): CursorState { @@ -35,14 +39,11 @@ export class OneCursor { } public readSelectionFromMarkers(context: CursorContext): Selection { - const start = context.model._getMarker(this._selStartMarker); - const end = context.model._getMarker(this._selEndMarker); - + const range = context.model._getTrackedRange(this._selTrackedRange); if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { - return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); } - - return new Selection(end.lineNumber, end.column, start.lineNumber, start.column); + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); } public ensureValidState(context: CursorContext): void { @@ -100,17 +101,6 @@ export class OneCursor { 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; - } + this._selTrackedRange = context.model._setTrackedRange(this._selTrackedRange, this.modelState.selection, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index ab9db53d0ceb4..a346ca6ea9d50 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -157,10 +157,6 @@ export interface IModelDecoration { * Options associated with this decoration. */ readonly options: IModelDecorationOptions; - /** - * A flag describing if this is a problem decoration (e.g. warning/error). - */ - readonly isForValidation: boolean; } /** @@ -915,32 +911,6 @@ export interface ITokenizedModel extends ITextModel { getLineIndentGuide(lineNumber: number): number; } -/** - * A model that can track markers. - */ -export interface ITextModelWithMarkers extends ITextModel { - /** - * @internal - */ - _addMarker(internalDecorationId: number, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string; - /** - * @internal - */ - _changeMarker(id: string, newLineNumber: number, newColumn: number): void; - /** - * @internal - */ - _changeMarkerStickiness(id: string, newStickToPreviousCharacter: boolean): void; - /** - * @internal - */ - _getMarker(id: string): Position; - /** - * @internal - */ - _removeMarker(id: string): void; -} - /** * Describes the behavior of decorations when typing/editing near their edges. * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` @@ -1034,12 +1004,29 @@ export interface ITextModelWithDecorations { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + + /** + * Gets all the decorations that should be rendered in the overview ruler as an array. + * @param ownerId If set, it will ignore decorations belonging to other owners. + * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). + */ + getOverviewRulerDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + + /** + * @internal + */ + _getTrackedRange(id: string): Range; + + /** + * @internal + */ + _setTrackedRange(id: string, newRange: Range, newStickiness: TrackedRangeStickiness): string; } /** * An editable text model. */ -export interface IEditableTextModel extends ITextModelWithMarkers { +export interface IEditableTextModel extends ITextModel { /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). @@ -1122,7 +1109,7 @@ export interface IEditableTextModel extends ITextModelWithMarkers { /** * A model. */ -export interface IModel extends IReadOnlyModel, IEditableTextModel, ITextModelWithMarkers, ITokenizedModel, ITextModelWithDecorations { +export interface IModel extends IReadOnlyModel, IEditableTextModel, ITokenizedModel, ITextModelWithDecorations { /** * @deprecated Please use `onDidChangeContent` instead. * An event emitted when the contents of the model have changed. @@ -1977,6 +1964,11 @@ export interface ICommonCodeEditor extends IEditor { */ setDecorations(decorationTypeKey: string, ranges: IDecorationOptions[]): void; + /** + * @internal + */ + setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void; + /** * @internal */ diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index 66566d1092b4f..77c0bd9f8a844 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -7,12 +7,11 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditStack } from 'vs/editor/common/model/editStack'; -import { ILineEdit, LineMarker, MarkersTracker, IModelLine } from 'vs/editor/common/model/modelLine'; +import { ILineEdit, IModelLine } from 'vs/editor/common/model/modelLine'; import { TextModelWithDecorations, ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; import * as strings from 'vs/base/common/strings'; import * as arrays from 'vs/base/common/arrays'; import { Selection } from 'vs/editor/common/core/selection'; -import { Position } from 'vs/editor/common/core/position'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource'; @@ -23,6 +22,7 @@ export interface IValidatedEditOperation { sortIndex: number; identifier: editorCommon.ISingleEditOperationIdentifier; range: Range; + rangeOffset: number; rangeLength: number; lines: string[]; forceMoveMarkers: boolean; @@ -249,6 +249,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito sortIndex: 0, identifier: operations[0].identifier, range: entireEditRange, + rangeOffset: this.getOffsetAt(entireEditRange.getStartPosition()), rangeLength: this.getValueLengthInRange(entireEditRange), lines: result.join('').split('\n'), forceMoveMarkers: forceMoveMarkers, @@ -275,15 +276,15 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public applyEdits(rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { try { this._eventEmitter.beginDeferredEmit(); - let markersTracker = this._acquireMarkersTracker(); - return this._applyEdits(markersTracker, rawOperations); + this._acquireDecorationsTracker(); + return this._applyEdits(rawOperations); } finally { - this._releaseMarkersTracker(); + this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } - private _applyEdits(markersTracker: MarkersTracker, rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { + private _applyEdits(rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { if (rawOperations.length === 0) { return []; } @@ -310,6 +311,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito sortIndex: i, identifier: op.identifier, range: validatedRange, + rangeOffset: this.getOffsetAt(validatedRange.getStartPosition()), rangeLength: this.getValueLengthInRange(validatedRange), lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, forceMoveMarkers: op.forceMoveMarkers, @@ -378,7 +380,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._mightContainRTL = mightContainRTL; this._mightContainNonBasicASCII = mightContainNonBasicASCII; - this._doApplyEdits(markersTracker, operations); + this._doApplyEdits(operations); this._trimAutoWhitespaceLines = null; if (this._options.trimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { @@ -465,7 +467,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito return result; } - private _doApplyEdits(markersTracker: MarkersTracker, operations: IValidatedEditOperation[]): void { + private _doApplyEdits(operations: IValidatedEditOperation[]): void { const tabSize = this._options.tabSize; @@ -503,11 +505,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } this._invalidateLine(currentLineNumber - 1); - this._lines[currentLineNumber - 1].applyEdits(markersTracker, lineEditsQueue.slice(currentLineNumberStart, i), tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); - } + this._lines[currentLineNumber - 1].applyEdits(lineEditsQueue.slice(currentLineNumberStart, i), tabSize); + this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(currentLineNumber, this._lines[currentLineNumber - 1].text) ); @@ -517,11 +516,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } this._invalidateLine(currentLineNumber - 1); - this._lines[currentLineNumber - 1].applyEdits(markersTracker, lineEditsQueue.slice(currentLineNumberStart, lineEditsQueue.length), tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); - } + this._lines[currentLineNumber - 1].applyEdits(lineEditsQueue.slice(currentLineNumberStart, lineEditsQueue.length), tabSize); + this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(currentLineNumber, this._lines[currentLineNumber - 1].text) ); @@ -529,10 +525,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito lineEditsQueue = []; }; - let minTouchedLineNumber = operations[operations.length - 1].range.startLineNumber; - let maxTouchedLineNumber = operations[0].range.endLineNumber + 1; - let totalLinesCountDelta = 0; - for (let i = 0, len = operations.length; i < len; i++) { const op = operations[i]; @@ -556,8 +548,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0); const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt); - totalLinesCountDelta += (insertingLinesCnt - deletingLinesCnt); - // Iterating descending to overlap with previous op // in case there are common lines being edited in both for (let j = editingLinesCnt; j >= 0; j--) { @@ -567,8 +557,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito lineNumber: editLineNumber, startColumn: (editLineNumber === startLineNumber ? startColumn : 1), endColumn: (editLineNumber === endLineNumber ? endColumn : this.getLineMaxColumn(editLineNumber)), - text: (op.lines ? op.lines[j] : ''), - forceMoveMarkers: op.forceMoveMarkers + text: (op.lines ? op.lines[j] : '') }); } @@ -579,43 +568,19 @@ export class EditableTextModel extends TextModelWithDecorations implements edito flushLineEdits(); const spliceStartLineNumber = startLineNumber + editingLinesCnt; - const spliceStartColumn = this.getLineMaxColumn(spliceStartLineNumber); - const endLineRemains = this._lines[endLineNumber - 1].split(markersTracker, endColumn, false, tabSize); + const endLineRemains = this._lines[endLineNumber - 1].split(endColumn, tabSize); this._invalidateLine(spliceStartLineNumber - 1); const spliceCnt = endLineNumber - spliceStartLineNumber; - // Collect all these markers - let markersOnDeletedLines: LineMarker[] = []; - for (let j = 0; j < spliceCnt; j++) { - const deleteLineIndex = spliceStartLineNumber + j; - const deleteLineMarkers = this._lines[deleteLineIndex].getMarkers(); - if (deleteLineMarkers) { - markersOnDeletedLines = markersOnDeletedLines.concat(deleteLineMarkers); - } - } - this._lines.splice(spliceStartLineNumber, spliceCnt); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.removeValues(spliceStartLineNumber, spliceCnt); - } + this._lineStarts.removeValues(spliceStartLineNumber, spliceCnt); // Reconstruct first line - this._lines[spliceStartLineNumber - 1].append(markersTracker, spliceStartLineNumber, endLineRemains, tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(spliceStartLineNumber - 1, this._lines[spliceStartLineNumber - 1].text.length + this._EOL.length); - } + this._lines[spliceStartLineNumber - 1].append(endLineRemains, tabSize); + this._lineStarts.changeValue(spliceStartLineNumber - 1, this._lines[spliceStartLineNumber - 1].text.length + this._EOL.length); - // Update deleted markers - const deletedMarkersPosition = new Position(spliceStartLineNumber, spliceStartColumn); - for (let j = 0, lenJ = markersOnDeletedLines.length; j < lenJ; j++) { - markersOnDeletedLines[j].updatePosition(markersTracker, deletedMarkersPosition); - } - - this._lines[spliceStartLineNumber - 1].addMarkers(markersOnDeletedLines); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(spliceStartLineNumber, this._lines[spliceStartLineNumber - 1].text) ); @@ -638,11 +603,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } // Split last line - let leftoverLine = this._lines[spliceLineNumber - 1].split(markersTracker, spliceColumn, op.forceMoveMarkers, tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(spliceLineNumber - 1, this._lines[spliceLineNumber - 1].text.length + this._EOL.length); - } + let leftoverLine = this._lines[spliceLineNumber - 1].split(spliceColumn, tabSize); + this._lineStarts.changeValue(spliceLineNumber - 1, this._lines[spliceLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(spliceLineNumber, this._lines[spliceLineNumber - 1].text) ); @@ -659,44 +621,31 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } this._lines = arrays.arrayInsert(this._lines, startLineNumber + editingLinesCnt, newLines); newLinesContent[newLinesContent.length - 1] += leftoverLine.text; - if (this._lineStarts) { - // update prefix sum - this._lineStarts.insertValues(startLineNumber + editingLinesCnt, newLinesLengths); - } + this._lineStarts.insertValues(startLineNumber + editingLinesCnt, newLinesLengths); // Last line - this._lines[startLineNumber + insertingLinesCnt - 1].append(markersTracker, startLineNumber + insertingLinesCnt, leftoverLine, tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(startLineNumber + insertingLinesCnt - 1, this._lines[startLineNumber + insertingLinesCnt - 1].text.length + this._EOL.length); - } + this._lines[startLineNumber + insertingLinesCnt - 1].append(leftoverLine, tabSize); + this._lineStarts.changeValue(startLineNumber + insertingLinesCnt - 1, this._lines[startLineNumber + insertingLinesCnt - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLinesInserted(spliceLineNumber + 1, startLineNumber + insertingLinesCnt, newLinesContent.join('\n')) ); } + const text = (op.lines ? op.lines.join(this.getEOL()) : ''); contentChanges.push({ range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), rangeLength: op.rangeLength, - text: op.lines ? op.lines.join(this.getEOL()) : '' + text: text }); + this._adjustDecorationsForEdit(op.rangeOffset, op.rangeLength, text.length, op.forceMoveMarkers); + // console.log('AFTER:'); // console.log('<<<\n' + this._lines.map(l => l.text).join('\n') + '\n>>>'); } flushLineEdits(); - maxTouchedLineNumber = Math.max(1, Math.min(this.getLineCount(), maxTouchedLineNumber + totalLinesCountDelta)); - if (totalLinesCountDelta !== 0) { - // must update line numbers all the way to the bottom - maxTouchedLineNumber = this.getLineCount(); - } - - for (let lineNumber = minTouchedLineNumber; lineNumber <= maxTouchedLineNumber; lineNumber++) { - this._lines[lineNumber - 1].updateLineNumber(markersTracker, lineNumber); - } - if (rawContentChanges.length !== 0 || contentChanges.length !== 0) { this._increaseVersionId(); @@ -718,35 +667,9 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelContentChanged, e); } - // this._assertLineNumbersOK(); this._resetIndentRanges(); } - public _assertLineNumbersOK(): void { - let foundMarkersCnt = 0; - for (let i = 0, len = this._lines.length; i < len; i++) { - let line = this._lines[i]; - let lineNumber = i + 1; - - let markers = line.getMarkers(); - if (markers !== null) { - for (let j = 0, lenJ = markers.length; j < lenJ; j++) { - foundMarkersCnt++; - let markerId = markers[j].id; - let marker = this._markerIdToMarker[markerId]; - if (marker.position.lineNumber !== lineNumber) { - throw new Error('Misplaced marker with id ' + markerId); - } - } - } - } - - let totalMarkersCnt = Object.keys(this._markerIdToMarker).length; - if (totalMarkersCnt !== foundMarkersCnt) { - throw new Error('There are misplaced markers!'); - } - } - private _undo(): Selection[] { this._isUndoing = true; let r = this._commandManager.undo(); @@ -764,10 +687,10 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public undo(): Selection[] { try { this._eventEmitter.beginDeferredEmit(); - this._acquireMarkersTracker(); + this._acquireDecorationsTracker(); return this._undo(); } finally { - this._releaseMarkersTracker(); + this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } @@ -789,10 +712,10 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public redo(): Selection[] { try { this._eventEmitter.beginDeferredEmit(); - this._acquireMarkersTracker(); + this._acquireDecorationsTracker(); return this._redo(); } finally { - this._releaseMarkersTracker(); + this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts new file mode 100644 index 0000000000000..c07fdd33ba225 --- /dev/null +++ b/src/vs/editor/common/model/intervalTree.ts @@ -0,0 +1,1369 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/editorCommon'; + +// +// The red-black tree is based on the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. +// + +export const ClassName = { + EditorInfoDecoration: 'infosquiggly', + EditorWarningDecoration: 'warningsquiggly', + EditorErrorDecoration: 'errorsquiggly' +}; + +/** + * Describes the behavior of decorations when typing/editing near their edges. + * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` + */ +export const enum TrackedRangeStickiness { + AlwaysGrowsWhenTypingAtEdges = 0, + NeverGrowsWhenTypingAtEdges = 1, + GrowsOnlyWhenTypingBefore = 2, + GrowsOnlyWhenTypingAfter = 3, +} + +const enum NodeColor { + Black = 0, + Red = 1, +} + +const enum Constants { + ColorMask = 0b00000001, + ColorMaskInverse = 0b11111110, + ColorOffset = 0, + + IsVisitedMask = 0b00000010, + IsVisitedMaskInverse = 0b11111101, + IsVisitedOffset = 1, + + IsForValidationMask = 0b00000100, + IsForValidationMaskInverse = 0b11111011, + IsForValidationOffset = 2, + + IsInOverviewRulerMask = 0b00001000, + IsInOverviewRulerMaskInverse = 0b11110111, + IsInOverviewRulerOffset = 3, + + StickinessMask = 0b00110000, + StickinessMaskInverse = 0b11001111, + StickinessOffset = 4, + + /** + * Due to how deletion works (in order to avoid always walking the right subtree of the deleted node), + * the deltas for nodes can grow and shrink dramatically. It has been observed, in practice, that unless + * the deltas are corrected, integer overflow will occur. + * + * The integer overflow occurs when 53 bits are used in the numbers, but we will try to avoid it as + * a node's delta gets below a negative 30 bits number. + * + * MIN SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MIN_SAFE_DELTA = -(1 << 30), + /** + * MAX SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MAX_SAFE_DELTA = 1 << 30, +} + +function getNodeColor(node: IntervalNode): NodeColor { + return ((node.metadata & Constants.ColorMask) >>> Constants.ColorOffset); +} +function setNodeColor(node: IntervalNode, color: NodeColor): void { + node.metadata = ( + (node.metadata & Constants.ColorMaskInverse) | (color << Constants.ColorOffset) + ); +} +function getNodeIsVisited(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsVisitedMask) >>> Constants.IsVisitedOffset) === 1; +} +function setNodeIsVisited(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsVisitedMaskInverse) | ((value ? 1 : 0) << Constants.IsVisitedOffset) + ); +} +function getNodeIsForValidation(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsForValidationMask) >>> Constants.IsForValidationOffset) === 1; +} +function setNodeIsForValidation(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset) + ); +} +export function getNodeIsInOverviewRuler(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsInOverviewRulerMask) >>> Constants.IsInOverviewRulerOffset) === 1; +} +function setNodeIsInOverviewRuler(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsInOverviewRulerMaskInverse) | ((value ? 1 : 0) << Constants.IsInOverviewRulerOffset) + ); +} +function getNodeStickiness(node: IntervalNode): TrackedRangeStickiness { + return ((node.metadata & Constants.StickinessMask) >>> Constants.StickinessOffset); +} +function setNodeStickiness(node: IntervalNode, stickiness: TrackedRangeStickiness): void { + node.metadata = ( + (node.metadata & Constants.StickinessMaskInverse) | (stickiness << Constants.StickinessOffset) + ); +} + +export class IntervalNode implements IModelDecoration { + + /** + * contains binary encoded information for color, visited, isForValidation and stickiness. + */ + public metadata: number; + + public parent: IntervalNode; + public left: IntervalNode; + public right: IntervalNode; + + public start: number; + public end: number; + public delta: number; + public maxEnd: number; + + public id: string; + public ownerId: number; + public options: ModelDecorationOptions; + + public cachedVersionId: number; + public cachedAbsoluteStart: number; + public cachedAbsoluteEnd: number; + public range: Range; + + constructor(id: string, start: number, end: number) { + this.metadata = 0; + + this.parent = null; + this.left = null; + this.right = null; + setNodeColor(this, NodeColor.Red); + + this.start = start; + this.end = end; + // FORCE_OVERFLOWING_TEST: this.delta = start; + this.delta = 0; + this.maxEnd = end; + + this.id = id; + this.ownerId = 0; + this.options = null; + setNodeIsForValidation(this, false); + setNodeStickiness(this, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + setNodeIsInOverviewRuler(this, false); + + this.cachedVersionId = 0; + this.cachedAbsoluteStart = start; + this.cachedAbsoluteEnd = end; + this.range = null; + + setNodeIsVisited(this, false); + } + + public reset(versionId: number, start: number, end: number, range: Range): void { + this.start = start; + this.end = end; + this.maxEnd = end; + this.cachedVersionId = versionId; + this.cachedAbsoluteStart = start; + this.cachedAbsoluteEnd = end; + this.range = range; + } + + public setOptions(options: ModelDecorationOptions) { + this.options = options; + setNodeIsForValidation(this, ( + this.options.className === ClassName.EditorErrorDecoration + || this.options.className === ClassName.EditorWarningDecoration + )); + setNodeStickiness(this, this.options.stickiness); + setNodeIsInOverviewRuler(this, this.options.overviewRuler.color ? true : false); + } + + public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { + if (this.cachedVersionId !== cachedVersionId) { + this.range = null; + } + this.cachedVersionId = cachedVersionId; + this.cachedAbsoluteStart = absoluteStart; + this.cachedAbsoluteEnd = absoluteEnd; + } + + public detach(): void { + this.parent = null; + this.left = null; + this.right = null; + } +} + +const SENTINEL: IntervalNode = new IntervalNode(null, 0, 0); +SENTINEL.parent = SENTINEL; +SENTINEL.left = SENTINEL; +SENTINEL.right = SENTINEL; +setNodeColor(SENTINEL, NodeColor.Black); + +export class IntervalTree { + + public root: IntervalNode; + public requestNormalizeDelta: boolean; + + constructor() { + this.root = SENTINEL; + this.requestNormalizeDelta = false; + } + + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + if (this.root === SENTINEL) { + return []; + } + return intervalSearch(this, start, end, filterOwnerId, filterOutValidation, cachedVersionId); + } + + public search(filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + if (this.root === SENTINEL) { + return []; + } + return search(this, filterOwnerId, filterOutValidation, cachedVersionId); + } + + public count(): number { + if (this.root === SENTINEL) { + return 0; + } + return nodeCount(this); + } + + /** + * Will not set `cachedAbsoluteStart` nor `cachedAbsoluteEnd` on the returned nodes! + */ + public collectNodesFromOwner(ownerId: number): IntervalNode[] { + return collectNodesFromOwner(this, ownerId); + } + + /** + * Will not set `cachedAbsoluteStart` nor `cachedAbsoluteEnd` on the returned nodes! + */ + public collectNodesPostOrder(): IntervalNode[] { + return collectNodesPostOrder(this); + } + + public insert(node: IntervalNode): void { + rbTreeInsert(this, node); + this._normalizeDeltaIfNecessary(); + } + + public delete(node: IntervalNode): void { + rbTreeDelete(this, node); + this._normalizeDeltaIfNecessary(); + } + + public resolveNode(node: IntervalNode, cachedVersionId: number): void { + const initialNode = node; + let delta = 0; + while (node !== this.root) { + if (node === node.parent.right) { + delta += node.parent.delta; + } + node = node.parent; + } + + const nodeStart = initialNode.start + delta; + const nodeEnd = initialNode.end + delta; + initialNode.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + } + + public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + // Our strategy is to remove all directly impacted nodes, and then add them back to the tree. + + // (1) collect all nodes that are intersecting this edit as nodes of interest + const nodesOfInterest = searchForEditing(this, offset, offset + length); + + // (2) remove all nodes that are intersecting this edit + for (let i = 0, len = nodesOfInterest.length; i < len; i++) { + const node = nodesOfInterest[i]; + rbTreeDelete(this, node); + } + this._normalizeDeltaIfNecessary(); + + // (3) edit all tree nodes except the nodes of interest + noOverlapReplace(this, offset, offset + length, textLength); + this._normalizeDeltaIfNecessary(); + + // (4) edit the nodes of interest and insert them back in the tree + for (let i = 0, len = nodesOfInterest.length; i < len; i++) { + const node = nodesOfInterest[i]; + node.start = node.cachedAbsoluteStart; + node.end = node.cachedAbsoluteEnd; + nodeAcceptEdit(node, offset, (offset + length), textLength, forceMoveMarkers); + node.maxEnd = node.end; + rbTreeInsert(this, node); + } + this._normalizeDeltaIfNecessary(); + } + + public assertInvariants(): void { + assert(getNodeColor(SENTINEL) === NodeColor.Black); + assert(SENTINEL.parent === SENTINEL); + assert(SENTINEL.left === SENTINEL); + assert(SENTINEL.right === SENTINEL); + assert(SENTINEL.start === 0); + assert(SENTINEL.end === 0); + assert(SENTINEL.delta === 0); + assert(this.root.parent === SENTINEL); + assertValidTree(this); + } + + public getAllInOrder(): IntervalNode[] { + return search(this, 0, false, 0); + } + + public print(): void { + if (this.root === SENTINEL) { + console.log(`~~ empty`); + return; + } + let out: string[] = []; + this._print(this.root, '', 0, out); + console.log(out.join('')); + } + + private _print(n: IntervalNode, indent: string, delta: number, out: string[]): void { + out.push(`${indent}[${getNodeColor(n) === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.start}->${n.end}, ${n.maxEnd}] : {${delta + n.start}->${delta + n.end}}, maxEnd: ${n.maxEnd + delta}\n`); + if (n.left !== SENTINEL) { + this._print(n.left, indent + ' ', delta, out); + } else { + out.push(`${indent} NIL\n`); + } + if (n.right !== SENTINEL) { + this._print(n.right, indent + ' ', delta + n.delta, out); + } else { + out.push(`${indent} NIL\n`); + } + } + + private _normalizeDeltaIfNecessary(): void { + if (!this.requestNormalizeDelta) { + return; + } + this.requestNormalizeDelta = false; + normalizeDelta(this); + } +} + +//#region Delta Normalization +function normalizeDelta(T: IntervalTree): void { + let node = T.root; + let delta = 0; + while (node !== SENTINEL) { + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + delta += node.delta; + node = node.right; + continue; + } + + // handle current node + node.start = delta + node.start; + node.end = delta + node.end; + node.delta = 0; + recomputeMaxEnd(node); + + setNodeIsVisited(node, true); + + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + } + + setNodeIsVisited(T.root, false); +} +//#endregion + +//#region Editing + +const enum MarkerMoveSemantics { + MarkerDefined = 0, + ForceMove = 1, + ForceStay = 2 +} + +function adjustMarkerBeforeColumn(markerOffset: number, markerStickToPreviousCharacter: boolean, checkOffset: number, moveSemantics: MarkerMoveSemantics): boolean { + if (markerOffset < checkOffset) { + return true; + } + if (markerOffset > checkOffset) { + return false; + } + if (moveSemantics === MarkerMoveSemantics.ForceMove) { + return false; + } + if (moveSemantics === MarkerMoveSemantics.ForceStay) { + return true; + } + return markerStickToPreviousCharacter; +}; + +/** + * This is a lot more complicated than strictly necessary to maintain the same behaviour + * as when decorations were implemented using two markers. + */ +function nodeAcceptEdit(node: IntervalNode, start: number, end: number, textLength: number, forceMoveMarkers: boolean): void { + const nodeStickiness = getNodeStickiness(node); + const startStickToPreviousCharacter = ( + nodeStickiness === TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + || nodeStickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + ); + const endStickToPreviousCharacter = ( + nodeStickiness === TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + || nodeStickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + ); + + const deletingCnt = (end - start); + const insertingCnt = textLength; + const commonLength = Math.min(deletingCnt, insertingCnt); + + const nodeStart = node.start; + let startDone = false; + + const nodeEnd = node.end; + let endDone = false; + + { + const moveSemantics = forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > 0 ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined); + if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, start, moveSemantics)) { + startDone = true; + } + if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, start, moveSemantics)) { + endDone = true; + } + } + + if (commonLength > 0 && !forceMoveMarkers) { + const moveSemantics = (deletingCnt > insertingCnt ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined); + if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, start + commonLength, moveSemantics)) { + startDone = true; + } + if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, start + commonLength, moveSemantics)) { + endDone = true; + } + } + + { + const moveSemantics = forceMoveMarkers ? MarkerMoveSemantics.ForceMove : MarkerMoveSemantics.MarkerDefined; + if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, end, moveSemantics)) { + node.start = start + insertingCnt; + startDone = true; + } + if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, end, moveSemantics)) { + node.end = start + insertingCnt; + endDone = true; + } + } + + // Finish + const deltaColumn = (insertingCnt - deletingCnt); + if (!startDone) { + node.start = Math.max(0, nodeStart + deltaColumn); + startDone = true; + } + if (!endDone) { + node.end = Math.max(0, nodeEnd + deltaColumn); + endDone = true; + } + + if (node.start > node.end) { + node.end = node.start; + } +} + +function searchForEditing(T: IntervalTree, start: number, end: number): IntervalNode[] { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. + let node = T.root; + let delta = 0; + let nodeMaxEnd = 0; + let nodeStart = 0; + let nodeEnd = 0; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + continue; + } + + if (!getNodeIsVisited(node.left)) { + // first time seeing this node + nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < start) { + // cover case b) from above + // there is no need to search this node or its children + setNodeIsVisited(node, true); + continue; + } + + if (node.left !== SENTINEL) { + // go left + node = node.left; + continue; + } + } + + // handle current node + nodeStart = delta + node.start; + if (nodeStart > end) { + // cover case a) from above + // there is no need to search this node or its right subtree + setNodeIsVisited(node, true); + continue; + } + + nodeEnd = delta + node.end; + if (nodeEnd >= start) { + node.setCachedOffsets(nodeStart, nodeEnd, 0); + result[resultLen++] = node; + } + setNodeIsVisited(node, true); + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + setNodeIsVisited(T.root, false); + + return result; +} + +function noOverlapReplace(T: IntervalTree, start: number, end: number, textLength: number): void { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. + let node = T.root; + let delta = 0; + let nodeMaxEnd = 0; + let nodeStart = 0; + const editDelta = (textLength - (end - start)); + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + if (node === node.parent.right) { + delta -= node.parent.delta; + } + recomputeMaxEnd(node); + node = node.parent; + continue; + } + + if (!getNodeIsVisited(node.left)) { + // first time seeing this node + nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < start) { + // cover case b) from above + // there is no need to search this node or its children + setNodeIsVisited(node, true); + continue; + } + + if (node.left !== SENTINEL) { + // go left + node = node.left; + continue; + } + } + + // handle current node + nodeStart = delta + node.start; + if (nodeStart > end) { + node.start += editDelta; + node.end += editDelta; + node.delta += editDelta; + if (node.delta < Constants.MIN_SAFE_DELTA || node.delta > Constants.MAX_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } + // cover case a) from above + // there is no need to search this node or its right subtree + setNodeIsVisited(node, true); + continue; + } + + setNodeIsVisited(node, true); + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + setNodeIsVisited(T.root, false); +} + +//#endregion + +//#region Searching + +function nodeCount(T: IntervalTree): number { + let node = T.root; + let count = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + // handle current node + count++; + setNodeIsVisited(node, true); + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + node = node.right; + continue; + } + } + + setNodeIsVisited(T.root, false); + + return count; +} + +function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] { + let node = T.root; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + // handle current node + if (node.ownerId === ownerId) { + result[resultLen++] = node; + } + + setNodeIsVisited(node, true); + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + node = node.right; + continue; + } + } + + setNodeIsVisited(T.root, false); + + return result; +} + +function collectNodesPostOrder(T: IntervalTree): IntervalNode[] { + let node = T.root; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + node = node.right; + continue; + } + + // handle current node + result[resultLen++] = node; + setNodeIsVisited(node, true); + } + + setNodeIsVisited(T.root, false); + + return result; +} + +function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + let node = T.root; + let delta = 0; + let nodeStart = 0; + let nodeEnd = 0; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + // handle current node + nodeStart = delta + node.start; + nodeEnd = delta + node.end; + + node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + + let include = true; + if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { + include = false; + } + if (filterOutValidation && getNodeIsForValidation(node)) { + include = false; + } + if (include) { + result[resultLen++] = node; + } + + setNodeIsVisited(node, true); + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + setNodeIsVisited(T.root, false); + + return result; +} + +function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. + + let node = T.root; + let delta = 0; + let nodeMaxEnd = 0; + let nodeStart = 0; + let nodeEnd = 0; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + continue; + } + + if (!getNodeIsVisited(node.left)) { + // first time seeing this node + nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < intervalStart) { + // cover case b) from above + // there is no need to search this node or its children + setNodeIsVisited(node, true); + continue; + } + + if (node.left !== SENTINEL) { + // go left + node = node.left; + continue; + } + } + + // handle current node + nodeStart = delta + node.start; + if (nodeStart > intervalEnd) { + // cover case a) from above + // there is no need to search this node or its right subtree + setNodeIsVisited(node, true); + continue; + } + + nodeEnd = delta + node.end; + + if (nodeEnd >= intervalStart) { + // There is overlap + node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + + let include = true; + if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { + include = false; + } + if (filterOutValidation && getNodeIsForValidation(node)) { + include = false; + } + + if (include) { + result[resultLen++] = node; + } + } + + setNodeIsVisited(node, true); + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + setNodeIsVisited(T.root, false); + + return result; +} + +//#endregion + +//#region Insertion +function rbTreeInsert(T: IntervalTree, newNode: IntervalNode): IntervalNode { + if (T.root === SENTINEL) { + newNode.parent = SENTINEL; + newNode.left = SENTINEL; + newNode.right = SENTINEL; + setNodeColor(newNode, NodeColor.Black); + T.root = newNode; + return T.root; + } + + treeInsert(T, newNode); + + recomputeMaxEndWalkToRoot(newNode.parent); + + // repair tree + let x = newNode; + while (x !== T.root && getNodeColor(x.parent) === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (getNodeColor(y) === NodeColor.Red) { + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(y, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + x = x.parent.parent; + } else { + if (x === x.parent.right) { + x = x.parent; + leftRotate(T, x); + } + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + rightRotate(T, x.parent.parent); + } + } else { + const y = x.parent.parent.left; + + if (getNodeColor(y) === NodeColor.Red) { + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(y, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + x = x.parent.parent; + } else { + if (x === x.parent.left) { + x = x.parent; + rightRotate(T, x); + } + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); + leftRotate(T, x.parent.parent); + } + } + } + + setNodeColor(T.root, NodeColor.Black); + + return newNode; +} + +function treeInsert(T: IntervalTree, z: IntervalNode): void { + let delta: number = 0; + let x = T.root; + const zAbsoluteStart = z.start; + const zAbsoluteEnd = z.end; + while (true) { + const cmp = intervalCompare(zAbsoluteStart, zAbsoluteEnd, x.start + delta, x.end + delta); + if (cmp < 0) { + // this node should be inserted to the left + // => it is not affected by the node's delta + if (x.left === SENTINEL) { + z.start -= delta; + z.end -= delta; + z.maxEnd -= delta; + x.left = z; + break; + } else { + x = x.left; + } + } else { + // this node should be inserted to the right + // => it is not affected by the node's delta + if (x.right === SENTINEL) { + z.start -= (delta + x.delta); + z.end -= (delta + x.delta); + z.maxEnd -= (delta + x.delta); + x.right = z; + break; + } else { + delta += x.delta; + x = x.right; + } + } + } + + z.parent = x; + z.left = SENTINEL; + z.right = SENTINEL; + setNodeColor(z, NodeColor.Red); +} +//#endregion + +//#region Deletion +function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { + + let x: IntervalNode; + let y: IntervalNode; + + // RB-DELETE except we don't swap z and y in case c) + // i.e. we always delete what's pointed at by z. + + if (z.left === SENTINEL) { + x = z.right; + y = z; + + // x's delta is no longer influenced by z's delta + x.delta += z.delta; + if (x.delta < Constants.MIN_SAFE_DELTA || x.delta > Constants.MAX_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } + x.start += z.delta; + x.end += z.delta; + + } else if (z.right === SENTINEL) { + x = z.left; + y = z; + + } else { + y = leftest(z.right); + x = y.right; + + // y's delta is no longer influenced by z's delta, + // but we don't want to walk the entire right-hand-side subtree of x. + // we therefore maintain z's delta in y, and adjust only x + x.start += y.delta; + x.end += y.delta; + x.delta += y.delta; + if (x.delta < Constants.MIN_SAFE_DELTA || x.delta > Constants.MAX_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } + + y.start += z.delta; + y.end += z.delta; + y.delta = z.delta; + if (y.delta < Constants.MIN_SAFE_DELTA || y.delta > Constants.MAX_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } + } + + if (y === T.root) { + T.root = x; + setNodeColor(x, NodeColor.Black); + + z.detach(); + resetSentinel(); + recomputeMaxEnd(x); + T.root.parent = SENTINEL; + return; + } + + let yWasRed = (getNodeColor(y) === NodeColor.Red); + + if (y === y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + if (y === z) { + x.parent = y.parent; + } else { + + if (y.parent === z) { + x.parent = y; + } else { + x.parent = y.parent; + } + + y.left = z.left; + y.right = z.right; + y.parent = z.parent; + setNodeColor(y, getNodeColor(z)); + + if (z === T.root) { + T.root = y; + } else { + if (z === z.parent.left) { + z.parent.left = y; + } else { + z.parent.right = y; + } + } + + if (y.left !== SENTINEL) { + y.left.parent = y; + } + if (y.right !== SENTINEL) { + y.right.parent = y; + } + } + + z.detach(); + + if (yWasRed) { + recomputeMaxEndWalkToRoot(x.parent); + if (y !== z) { + recomputeMaxEndWalkToRoot(y); + recomputeMaxEndWalkToRoot(y.parent); + } + resetSentinel(); + return; + } + + recomputeMaxEndWalkToRoot(x); + recomputeMaxEndWalkToRoot(x.parent); + if (y !== z) { + recomputeMaxEndWalkToRoot(y); + recomputeMaxEndWalkToRoot(y.parent); + } + + // RB-DELETE-FIXUP + let w: IntervalNode; + while (x !== T.root && getNodeColor(x) === NodeColor.Black) { + + if (x === x.parent.left) { + w = x.parent.right; + + if (getNodeColor(w) === NodeColor.Red) { + setNodeColor(w, NodeColor.Black); + setNodeColor(x.parent, NodeColor.Red); + leftRotate(T, x.parent); + w = x.parent.right; + } + + if (getNodeColor(w.left) === NodeColor.Black && getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w, NodeColor.Red); + x = x.parent; + } else { + if (getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w.left, NodeColor.Black); + setNodeColor(w, NodeColor.Red); + rightRotate(T, w); + w = x.parent.right; + } + + setNodeColor(w, getNodeColor(x.parent)); + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(w.right, NodeColor.Black); + leftRotate(T, x.parent); + x = T.root; + } + + } else { + w = x.parent.left; + + if (getNodeColor(w) === NodeColor.Red) { + setNodeColor(w, NodeColor.Black); + setNodeColor(x.parent, NodeColor.Red); + rightRotate(T, x.parent); + w = x.parent.left; + } + + if (getNodeColor(w.left) === NodeColor.Black && getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w, NodeColor.Red); + x = x.parent; + + } else { + if (getNodeColor(w.left) === NodeColor.Black) { + setNodeColor(w.right, NodeColor.Black); + setNodeColor(w, NodeColor.Red); + leftRotate(T, w); + w = x.parent.left; + } + + setNodeColor(w, getNodeColor(x.parent)); + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(w.left, NodeColor.Black); + rightRotate(T, x.parent); + x = T.root; + } + } + } + + setNodeColor(x, NodeColor.Black); + resetSentinel(); +} + +function leftest(node: IntervalNode): IntervalNode { + while (node.left !== SENTINEL) { + node = node.left; + } + return node; +} + +function resetSentinel(): void { + SENTINEL.parent = SENTINEL; + SENTINEL.delta = 0; // optional + SENTINEL.start = 0; // optional + SENTINEL.end = 0; // optional +} +//#endregion + +//#region Rotations +function leftRotate(T: IntervalTree, x: IntervalNode): void { + const y = x.right; // set y. + + y.delta += x.delta; // y's delta is no longer influenced by x's delta + if (y.delta < Constants.MIN_SAFE_DELTA || y.delta > Constants.MAX_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } + y.start += x.delta; + y.end += x.delta; + + x.right = y.left; // turn y's left subtree into x's right subtree. + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; // link x's parent to y. + if (x.parent === SENTINEL) { + T.root = y; + } else if (x === x.parent.left) { + x.parent.left = y; + } else { + x.parent.right = y; + } + + y.left = x; // put x on y's left. + x.parent = y; + + recomputeMaxEnd(x); + recomputeMaxEnd(y); +} + +function rightRotate(T: IntervalTree, y: IntervalNode): void { + const x = y.left; + + y.delta -= x.delta; + if (y.delta < Constants.MIN_SAFE_DELTA || y.delta > Constants.MAX_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } + y.start -= x.delta; + y.end -= x.delta; + + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; + } + x.parent = y.parent; + if (y.parent === SENTINEL) { + T.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; + } + + x.right = y; + y.parent = x; + + recomputeMaxEnd(y); + recomputeMaxEnd(x); +} +//#endregion + +//#region max end computation + +function computeMaxEnd(node: IntervalNode): number { + let maxEnd = node.end; + if (node.left !== SENTINEL) { + const leftMaxEnd = node.left.maxEnd; + if (leftMaxEnd > maxEnd) { + maxEnd = leftMaxEnd; + } + } + if (node.right !== SENTINEL) { + const rightMaxEnd = node.right.maxEnd + node.delta; + if (rightMaxEnd > maxEnd) { + maxEnd = rightMaxEnd; + } + } + return maxEnd; +} + +export function recomputeMaxEnd(node: IntervalNode): void { + node.maxEnd = computeMaxEnd(node); +} + +function recomputeMaxEndWalkToRoot(node: IntervalNode): void { + while (node !== SENTINEL) { + + const maxEnd = computeMaxEnd(node); + + if (node.maxEnd === maxEnd) { + // no need to go further + return; + } + + node.maxEnd = maxEnd; + node = node.parent; + } +} + +//#endregion + +//#region utils +function intervalCompare(aStart: number, aEnd: number, bStart: number, bEnd: number): number { + if (aStart === bStart) { + return aEnd - bEnd; + } + return aStart - bStart; +} +//#endregion + +//#region Assertion + +function depth(n: IntervalNode): number { + if (n === SENTINEL) { + // The leafs are black + return 1; + } + assert(depth(n.left) === depth(n.right)); + return (getNodeColor(n) === NodeColor.Black ? 1 : 0) + depth(n.left); +} + +function assertValidNode(n: IntervalNode, delta): void { + if (n === SENTINEL) { + return; + } + + let l = n.left; + let r = n.right; + + if (getNodeColor(n) === NodeColor.Red) { + assert(getNodeColor(l) === NodeColor.Black); + assert(getNodeColor(r) === NodeColor.Black); + } + + let expectedMaxEnd = n.end; + if (l !== SENTINEL) { + assert(intervalCompare(l.start + delta, l.end + delta, n.start + delta, n.end + delta) <= 0); + expectedMaxEnd = Math.max(expectedMaxEnd, l.maxEnd); + } + if (r !== SENTINEL) { + assert(intervalCompare(n.start + delta, n.end + delta, r.start + delta + n.delta, r.end + delta + n.delta) <= 0); + expectedMaxEnd = Math.max(expectedMaxEnd, r.maxEnd + n.delta); + } + assert(n.maxEnd === expectedMaxEnd); + + assertValidNode(l, delta); + assertValidNode(r, delta + n.delta); +} + +function assertValidTree(tree: IntervalTree): void { + if (tree.root === SENTINEL) { + return; + } + assert(getNodeColor(tree.root) === NodeColor.Black); + assert(depth(tree.root.left) === depth(tree.root.right)); + assertValidNode(tree.root, 0); +} + +function assert(condition: boolean): void { + if (!condition) { + throw new Error('Assertion violation'); + } +} + +//#endregion diff --git a/src/vs/editor/common/model/model.ts b/src/vs/editor/common/model/model.ts index 1228592fe0fb5..2e1410fc830bb 100644 --- a/src/vs/editor/common/model/model.ts +++ b/src/vs/editor/common/model/model.ts @@ -16,7 +16,7 @@ import { IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; // The hierarchy is: -// Model -> EditableTextModel -> TextModelWithDecorations -> TextModelWithTrackedRanges -> TextModelWithMarkers -> TextModelWithTokens -> TextModel +// Model -> EditableTextModel -> TextModelWithDecorations -> TextModelWithTokens -> TextModel var MODEL_ID = 0; diff --git a/src/vs/editor/common/model/modelLine.ts b/src/vs/editor/common/model/modelLine.ts index e71219bd6898d..b811f40a73f65 100644 --- a/src/vs/editor/common/model/modelLine.ts +++ b/src/vs/editor/common/model/modelLine.ts @@ -7,94 +7,12 @@ import { IState, FontStyle, StandardTokenType, MetadataConsts, ColorId, LanguageId } from 'vs/editor/common/modes'; import { CharCode } from 'vs/base/common/charCode'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; -import { Position } from 'vs/editor/common/core/position'; import { Constants } from 'vs/editor/common/core/uint'; export interface ILineEdit { startColumn: number; endColumn: number; text: string; - forceMoveMarkers: boolean; -} - -export class LineMarker { - _lineMarkerBrand: void; - - public readonly id: string; - public readonly internalDecorationId: number; - - public stickToPreviousCharacter: boolean; - public position: Position; - - constructor(id: string, internalDecorationId: number, position: Position, stickToPreviousCharacter: boolean) { - this.id = id; - this.internalDecorationId = internalDecorationId; - this.position = position; - this.stickToPreviousCharacter = stickToPreviousCharacter; - } - - public toString(): string { - return '{\'' + this.id + '\';' + this.position.toString() + ',' + this.stickToPreviousCharacter + '}'; - } - - public updateLineNumber(markersTracker: MarkersTracker, lineNumber: number): void { - if (this.position.lineNumber === lineNumber) { - return; - } - markersTracker.addChangedMarker(this); - this.position = new Position(lineNumber, this.position.column); - } - - public updateColumn(markersTracker: MarkersTracker, column: number): void { - if (this.position.column === column) { - return; - } - markersTracker.addChangedMarker(this); - this.position = new Position(this.position.lineNumber, column); - } - - public updatePosition(markersTracker: MarkersTracker, position: Position): void { - if (this.position.lineNumber === position.lineNumber && this.position.column === position.column) { - return; - } - markersTracker.addChangedMarker(this); - this.position = position; - } - - public setPosition(position: Position) { - this.position = position; - } - - - public static compareMarkers(a: LineMarker, b: LineMarker): number { - if (a.position.column === b.position.column) { - return (a.stickToPreviousCharacter ? 0 : 1) - (b.stickToPreviousCharacter ? 0 : 1); - } - return a.position.column - b.position.column; - } -} - -export class MarkersTracker { - _changedDecorationsBrand: void; - - private _changedDecorations: number[]; - private _changedDecorationsLen: number; - - constructor() { - this._changedDecorations = []; - this._changedDecorationsLen = 0; - } - - public addChangedMarker(marker: LineMarker): void { - let internalDecorationId = marker.internalDecorationId; - if (internalDecorationId !== 0) { - this._changedDecorations[this._changedDecorationsLen++] = internalDecorationId; - } - } - - public getDecorationIds(): number[] { - return this._changedDecorations; - } } export interface ITokensAdjuster { @@ -102,27 +20,10 @@ export interface ITokensAdjuster { finish(delta: number, lineTextLength: number): void; } -interface IMarkersAdjuster { - adjustDelta(toColumn: number, delta: number, minimumAllowedColumn: number, moveSemantics: MarkerMoveSemantics): void; - adjustSet(toColumn: number, newColumn: number, moveSemantics: MarkerMoveSemantics): void; - finish(delta: number, lineTextLength: number): void; -} - var NO_OP_TOKENS_ADJUSTER: ITokensAdjuster = { adjust: () => { }, finish: () => { } }; -var NO_OP_MARKERS_ADJUSTER: IMarkersAdjuster = { - adjustDelta: () => { }, - adjustSet: () => { }, - finish: () => { } -}; - -const enum MarkerMoveSemantics { - MarkerDefined = 0, - ForceMove = 1, - ForceStay = 2 -} /** * Returns: @@ -156,13 +57,6 @@ function computePlusOneIndentLevel(line: string, tabSize: number): number { export interface IModelLine { readonly text: string; - // --- markers - addMarker(marker: LineMarker): void; - addMarkers(markers: LineMarker[]): void; - removeMarker(marker: LineMarker): void; - removeMarkers(deleteMarkers: { [markerId: string]: boolean; }): void; - getMarkers(): LineMarker[]; - // --- tokenization resetTokenizationState(): void; isInvalid(): boolean; @@ -177,20 +71,14 @@ export interface IModelLine { getIndentLevel(): number; // --- editing - updateLineNumber(markersTracker: MarkersTracker, newLineNumber: number): void; - applyEdits(markersTracker: MarkersTracker, edits: ILineEdit[], tabSize: number): number; - append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void; - split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine; + applyEdits(edits: ILineEdit[], tabSize: number): number; + append(other: IModelLine, tabSize: number): void; + split(splitColumn: number, tabSize: number): IModelLine; } export abstract class AbstractModelLine { - private _markers: LineMarker[]; - - constructor(initializeMarkers: boolean) { - if (initializeMarkers) { - this._markers = null; - } + constructor() { } /// @@ -202,121 +90,18 @@ export abstract class AbstractModelLine { /// - // private _printMarkers(): string { - // if (!this._markers) { - // return '[]'; - // } - // if (this._markers.length === 0) { - // return '[]'; - // } - - // var markers = this._markers; - - // var printMarker = (m:LineMarker) => { - // if (m.stickToPreviousCharacter) { - // return '|' + m.position.column; - // } - // return m.position.column + '|'; - // }; - // return '[' + markers.map(printMarker).join(', ') + ']'; - // } - - private _createMarkersAdjuster(markersTracker: MarkersTracker): IMarkersAdjuster { - if (!this._markers) { - return NO_OP_MARKERS_ADJUSTER; - } - if (this._markers.length === 0) { - return NO_OP_MARKERS_ADJUSTER; - } - - this._markers.sort(LineMarker.compareMarkers); - - var markers = this._markers; - var markersLength = markers.length; - var markersIndex = 0; - var marker = markers[markersIndex]; - - // console.log('------------- INITIAL MARKERS: ' + this._printMarkers()); - - let adjustMarkerBeforeColumn = (toColumn: number, moveSemantics: MarkerMoveSemantics) => { - if (marker.position.column < toColumn) { - return true; - } - if (marker.position.column > toColumn) { - return false; - } - if (moveSemantics === MarkerMoveSemantics.ForceMove) { - return false; - } - if (moveSemantics === MarkerMoveSemantics.ForceStay) { - return true; - } - return marker.stickToPreviousCharacter; - }; - - let adjustDelta = (toColumn: number, delta: number, minimumAllowedColumn: number, moveSemantics: MarkerMoveSemantics) => { - // console.log('------------------------------'); - // console.log('adjustDelta called: toColumn: ' + toColumn + ', delta: ' + delta + ', minimumAllowedColumn: ' + minimumAllowedColumn + ', moveSemantics: ' + MarkerMoveSemantics[moveSemantics]); - // console.log('BEFORE::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - - while (markersIndex < markersLength && adjustMarkerBeforeColumn(toColumn, moveSemantics)) { - if (delta !== 0) { - let newColumn = Math.max(minimumAllowedColumn, marker.position.column + delta); - marker.updateColumn(markersTracker, newColumn); - } - - markersIndex++; - if (markersIndex < markersLength) { - marker = markers[markersIndex]; - } - } - - // console.log('AFTER::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - }; - - let adjustSet = (toColumn: number, newColumn: number, moveSemantics: MarkerMoveSemantics) => { - // console.log('------------------------------'); - // console.log('adjustSet called: toColumn: ' + toColumn + ', newColumn: ' + newColumn + ', moveSemantics: ' + MarkerMoveSemantics[moveSemantics]); - // console.log('BEFORE::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - - while (markersIndex < markersLength && adjustMarkerBeforeColumn(toColumn, moveSemantics)) { - marker.updateColumn(markersTracker, newColumn); - - markersIndex++; - if (markersIndex < markersLength) { - marker = markers[markersIndex]; - } - } - - // console.log('AFTER::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - }; - - let finish = (delta: number, lineTextLength: number) => { - adjustDelta(Constants.MAX_SAFE_SMALL_INTEGER, delta, 1, MarkerMoveSemantics.MarkerDefined); - - // console.log('------------- FINAL MARKERS: ' + this._printMarkers()); - }; - - return { - adjustDelta: adjustDelta, - adjustSet: adjustSet, - finish: finish - }; - } - - public applyEdits(markersTracker: MarkersTracker, edits: ILineEdit[], tabSize: number): number { + public applyEdits(edits: ILineEdit[], tabSize: number): number { let deltaColumn = 0; let resultText = this.text; let tokensAdjuster = this._createTokensAdjuster(); - let markersAdjuster = this._createMarkersAdjuster(markersTracker); for (let i = 0, len = edits.length; i < len; i++) { let edit = edits[i]; // console.log(); // console.log('============================='); - // console.log('EDIT #' + i + ' [ ' + edit.startColumn + ' -> ' + edit.endColumn + ' ] : <<<' + edit.text + '>>>, forceMoveMarkers: ' + edit.forceMoveMarkers); + // console.log('EDIT #' + i + ' [ ' + edit.startColumn + ' -> ' + edit.endColumn + ' ] : <<<' + edit.text + '>>>'); // console.log('deltaColumn: ' + deltaColumn); let startColumn = deltaColumn + edit.startColumn; @@ -324,35 +109,28 @@ export abstract class AbstractModelLine { let deletingCnt = endColumn - startColumn; let insertingCnt = edit.text.length; - // Adjust tokens & markers before this edit - // console.log('Adjust tokens & markers before this edit'); + // Adjust tokens before this edit + // console.log('Adjust tokens before this edit'); tokensAdjuster.adjust(edit.startColumn - 1, deltaColumn, 1); - markersAdjuster.adjustDelta(edit.startColumn, deltaColumn, 1, edit.forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > 0 ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined)); - // Adjust tokens & markers for the common part of this edit + // Adjust tokens for the common part of this edit let commonLength = Math.min(deletingCnt, insertingCnt); if (commonLength > 0) { - // console.log('Adjust tokens & markers for the common part of this edit'); + // console.log('Adjust tokens for the common part of this edit'); tokensAdjuster.adjust(edit.startColumn - 1 + commonLength, deltaColumn, startColumn); - - if (!edit.forceMoveMarkers) { - markersAdjuster.adjustDelta(edit.startColumn + commonLength, deltaColumn, startColumn, edit.forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > insertingCnt ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined)); - } } // Perform the edit & update `deltaColumn` resultText = resultText.substring(0, startColumn - 1) + edit.text + resultText.substring(endColumn - 1); deltaColumn += insertingCnt - deletingCnt; - // Adjust tokens & markers inside this edit - // console.log('Adjust tokens & markers inside this edit'); + // Adjust tokens inside this edit + // console.log('Adjust tokens inside this edit'); tokensAdjuster.adjust(edit.endColumn, deltaColumn, startColumn); - markersAdjuster.adjustSet(edit.endColumn, startColumn + insertingCnt, edit.forceMoveMarkers ? MarkerMoveSemantics.ForceMove : MarkerMoveSemantics.MarkerDefined); } - // Wrap up tokens & markers; adjust remaining if needed + // Wrap up tokens; adjust remaining if needed tokensAdjuster.finish(deltaColumn, resultText.length); - markersAdjuster.finish(deltaColumn, resultText.length); // Save the resulting text this._setText(resultText, tabSize); @@ -360,157 +138,16 @@ export abstract class AbstractModelLine { return deltaColumn; } - public split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine { - // console.log('--> split @ ' + splitColumn + '::: ' + this._printMarkers()); - var myText = this.text.substring(0, splitColumn - 1); - var otherText = this.text.substring(splitColumn - 1); - - var otherMarkers: LineMarker[] = null; - - if (this._markers) { - this._markers.sort(LineMarker.compareMarkers); - for (let i = 0, len = this._markers.length; i < len; i++) { - let marker = this._markers[i]; - - if ( - marker.position.column > splitColumn - || ( - marker.position.column === splitColumn - && ( - forceMoveMarkers - || !marker.stickToPreviousCharacter - ) - ) - ) { - let myMarkers = this._markers.slice(0, i); - otherMarkers = this._markers.slice(i); - this._markers = myMarkers; - break; - } - } - - if (otherMarkers) { - for (let i = 0, len = otherMarkers.length; i < len; i++) { - let marker = otherMarkers[i]; - - marker.updateColumn(markersTracker, marker.position.column - (splitColumn - 1)); - } - } - } + public split(splitColumn: number, tabSize: number): IModelLine { + const myText = this.text.substring(0, splitColumn - 1); + const otherText = this.text.substring(splitColumn - 1); this._setText(myText, tabSize); - - var otherLine = this._createModelLine(otherText, tabSize); - if (otherMarkers) { - otherLine.addMarkers(otherMarkers); - } - return otherLine; + return this._createModelLine(otherText, tabSize); } - public append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void { - // console.log('--> append: THIS :: ' + this._printMarkers()); - // console.log('--> append: OTHER :: ' + this._printMarkers()); - let thisTextLength = this.text.length; + public append(other: IModelLine, tabSize: number): void { this._setText(this.text + other.text, tabSize); - - if (other instanceof AbstractModelLine) { - if (other._markers) { - // Other has markers - let otherMarkers = other._markers; - - // Adjust other markers - for (let i = 0, len = otherMarkers.length; i < len; i++) { - let marker = otherMarkers[i]; - - marker.updatePosition(markersTracker, new Position(myLineNumber, marker.position.column + thisTextLength)); - } - - this.addMarkers(otherMarkers); - } - } - } - - public addMarker(marker: LineMarker): void { - if (!this._markers) { - this._markers = [marker]; - } else { - this._markers.push(marker); - } - } - - public addMarkers(markers: LineMarker[]): void { - if (markers.length === 0) { - return; - } - - if (!this._markers) { - this._markers = markers.slice(0); - } else { - this._markers = this._markers.concat(markers); - } - } - - public removeMarker(marker: LineMarker): void { - if (!this._markers) { - return; - } - - let index = this._indexOfMarkerId(marker.id); - if (index < 0) { - return; - } - - if (this._markers.length === 1) { - // was last marker on line - this._markers = null; - } else { - this._markers.splice(index, 1); - } - } - - public removeMarkers(deleteMarkers: { [markerId: string]: boolean; }): void { - if (!this._markers) { - return; - } - for (let i = 0, len = this._markers.length; i < len; i++) { - let marker = this._markers[i]; - - if (deleteMarkers[marker.id]) { - this._markers.splice(i, 1); - len--; - i--; - } - } - if (this._markers.length === 0) { - this._markers = null; - } - } - - public getMarkers(): LineMarker[] { - if (!this._markers) { - return null; - } - return this._markers; - } - - public updateLineNumber(markersTracker: MarkersTracker, newLineNumber: number): void { - if (this._markers) { - let markers = this._markers; - for (let i = 0, len = markers.length; i < len; i++) { - let marker = markers[i]; - marker.updateLineNumber(markersTracker, newLineNumber); - } - } - } - - private _indexOfMarkerId(markerId: string): number { - let markers = this._markers; - for (let i = 0, len = markers.length; i < len; i++) { - if (markers[i].id === markerId) { - return i; - } - } - return undefined; } } @@ -559,7 +196,7 @@ export class ModelLine extends AbstractModelLine implements IModelLine { private _lineTokens: ArrayBuffer; constructor(text: string, tabSize: number) { - super(true); + super(); this._metadata = 0; this._setText(text, tabSize); this._state = null; @@ -570,8 +207,8 @@ export class ModelLine extends AbstractModelLine implements IModelLine { return new ModelLine(text, tabSize); } - public split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine { - let result = super.split(markersTracker, splitColumn, forceMoveMarkers, tabSize); + public split(splitColumn: number, tabSize: number): IModelLine { + let result = super.split(splitColumn, tabSize); // Mark overflowing tokens for deletion & delete marked tokens this._deleteMarkedTokens(this._markOverflowingTokensForDeletion(0, this.text.length)); @@ -579,10 +216,10 @@ export class ModelLine extends AbstractModelLine implements IModelLine { return result; } - public append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void { + public append(other: IModelLine, tabSize: number): void { let thisTextLength = this.text.length; - super.append(markersTracker, myLineNumber, other, tabSize); + super.append(other, tabSize); if (other instanceof ModelLine) { let otherRawTokens = other._lineTokens; @@ -828,7 +465,7 @@ export class MinimalModelLine extends AbstractModelLine implements IModelLine { } constructor(text: string, tabSize: number) { - super(false); + super(); this._setText(text, tabSize); } @@ -836,12 +473,12 @@ export class MinimalModelLine extends AbstractModelLine implements IModelLine { return new MinimalModelLine(text, tabSize); } - public split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine { - return super.split(markersTracker, splitColumn, forceMoveMarkers, tabSize); + public split(splitColumn: number, tabSize: number): IModelLine { + return super.split(splitColumn, tabSize); } - public append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void { - super.append(markersTracker, myLineNumber, other, tabSize); + public append(other: IModelLine, tabSize: number): void { + super.append(other, tabSize); } // --- BEGIN STATE diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 3e55882ed9605..aa93b0ed106e7 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -8,6 +8,7 @@ import { OrderGuaranteeEventEmitter, BulkListenerCallback } from 'vs/base/common import * as strings from 'vs/base/common/strings'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range, IRange } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ModelLine, IModelLine, MinimalModelLine } from 'vs/editor/common/model/modelLine'; import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; @@ -18,8 +19,6 @@ import { TextSource, ITextSource, IRawTextSource, RawTextSource } from 'vs/edito import { IDisposable } from 'vs/base/common/lifecycle'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; -const USE_MIMINAL_MODEL_LINE = true; - const LIMIT_FIND_COUNT = 999; export const LONG_LINE_BOUNDARY = 10000; @@ -123,7 +122,7 @@ export class TextModel implements editorCommon.ITextModel { } protected _createModelLine(text: string, tabSize: number): IModelLine { - if (USE_MIMINAL_MODEL_LINE && this._isTooLargeForTokenization) { + if (this._isTooLargeForTokenization) { return new MinimalModelLine(text, tabSize); } return new ModelLine(text, tabSize); @@ -261,22 +260,9 @@ export class TextModel implements editorCommon.ITextModel { return this._alternativeVersionId; } - private _ensureLineStarts(): void { - if (!this._lineStarts) { - const eolLength = this._EOL.length; - const linesLength = this._lines.length; - const lineStartValues = new Uint32Array(linesLength); - for (let i = 0; i < linesLength; i++) { - lineStartValues[i] = this._lines[i].text.length + eolLength; - } - this._lineStarts = new PrefixSumComputer(lineStartValues); - } - } - public getOffsetAt(rawPosition: IPosition): number { this._assertNotDisposed(); let position = this._validatePosition(rawPosition.lineNumber, rawPosition.column, false); - this._ensureLineStarts(); return this._lineStarts.getAccumulatedValue(position.lineNumber - 2) + position.column - 1; } @@ -285,7 +271,6 @@ export class TextModel implements editorCommon.ITextModel { offset = Math.floor(offset); offset = Math.max(0, offset); - this._ensureLineStarts(); let out = this._lineStarts.getIndexOf(offset); let lineLength = this._lines[out.index].text.length; @@ -525,6 +510,12 @@ export class TextModel implements editorCommon.ITextModel { return this._EOL; } + protected _onBeforeEOLChange(): void { + } + + protected _onAfterEOLChange(): void { + } + public setEOL(eol: editorCommon.EndOfLineSequence): void { this._assertNotDisposed(); const newEOL = (eol === editorCommon.EndOfLineSequence.CRLF ? '\r\n' : '\n'); @@ -538,9 +529,11 @@ export class TextModel implements editorCommon.ITextModel { const endLineNumber = this.getLineCount(); const endColumn = this.getLineMaxColumn(endLineNumber); + this._onBeforeEOLChange(); this._EOL = newEOL; - this._lineStarts = null; + this._constructLineStarts(); this._increaseVersionId(); + this._onAfterEOLChange(); this._emitModelRawContentChangedEvent( new textModelEvents.ModelRawContentChangedEvent( @@ -607,6 +600,77 @@ export class TextModel implements editorCommon.ITextModel { return lineNumber; } + /** + * Validates `range` is within buffer bounds, but allows it to sit in between surrogate pairs, etc. + * Will try to not allocate if possible. + */ + protected _validateRangeRelaxedNoAllocations(range: IRange): Range { + const linesCount = this._lines.length; + + const initialStartLineNumber = range.startLineNumber; + const initialStartColumn = range.startColumn; + let startLineNumber: number; + let startColumn: number; + + if (initialStartLineNumber < 1) { + startLineNumber = 1; + startColumn = 1; + } else if (initialStartLineNumber > linesCount) { + startLineNumber = linesCount; + startColumn = this.getLineMaxColumn(startLineNumber); + } else { + startLineNumber = initialStartLineNumber | 0; + if (initialStartColumn <= 1) { + startColumn = 1; + } else { + const maxColumn = this.getLineMaxColumn(startLineNumber); + if (initialStartColumn >= maxColumn) { + startColumn = maxColumn; + } else { + startColumn = initialStartColumn | 0; + } + } + } + + const initialEndLineNumber = range.endLineNumber; + const initialEndColumn = range.endColumn; + let endLineNumber: number; + let endColumn: number; + + if (initialEndLineNumber < 1) { + endLineNumber = 1; + endColumn = 1; + } else if (initialEndLineNumber > linesCount) { + endLineNumber = linesCount; + endColumn = this.getLineMaxColumn(endLineNumber); + } else { + endLineNumber = initialEndLineNumber | 0; + if (initialEndColumn <= 1) { + endColumn = 1; + } else { + const maxColumn = this.getLineMaxColumn(endLineNumber); + if (initialEndColumn >= maxColumn) { + endColumn = maxColumn; + } else { + endColumn = initialEndColumn | 0; + } + } + } + + if ( + initialStartLineNumber === startLineNumber + && initialStartColumn === startColumn + && initialEndLineNumber === endLineNumber + && initialEndColumn === endColumn + && range instanceof Range + && !(range instanceof Selection) + ) { + return range; + } + + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } + /** * @param strict Do NOT allow a position inside a high-low surrogate pair */ @@ -723,7 +787,17 @@ export class TextModel implements editorCommon.ITextModel { this._mightContainNonBasicASCII = !textSource.isBasicASCII; this._EOL = textSource.EOL; this._lines = modelLines; - this._lineStarts = null; + this._constructLineStarts(); + } + + private _constructLineStarts(): void { + const eolLength = this._EOL.length; + const linesLength = this._lines.length; + const lineStartValues = new Uint32Array(linesLength); + for (let i = 0; i < linesLength; i++) { + lineStartValues[i] = this._lines[i].text.length + eolLength; + } + this._lineStarts = new PrefixSumComputer(lineStartValues); } private _getEndOfLine(eol: editorCommon.EndOfLinePreference): string { diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index 2acaf260be289..aaa66c7beabf5 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -88,18 +88,6 @@ export interface IModelContentChangedEvent { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { - /** - * Lists of ids for added decorations. - */ - readonly addedDecorations: string[]; - /** - * Lists of ids for changed decorations. - */ - readonly changedDecorations: string[]; - /** - * List of ids for removed decorations. - */ - readonly removedDecorations: string[]; } /** diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index d86288a6b2cf9..ff79fbfccce32 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -10,107 +10,12 @@ import * as strings from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; import { Range, IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { MarkersTracker, LineMarker } from 'vs/editor/common/model/modelLine'; -import { Position } from 'vs/editor/common/core/position'; -import { INewMarker, TextModelWithMarkers } from 'vs/editor/common/model/textModelWithMarkers'; +import { TextModelWithTokens } from 'vs/editor/common/model/textModelWithTokens'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; - -export const ClassName = { - EditorInfoDecoration: 'infosquiggly', - EditorWarningDecoration: 'warningsquiggly', - EditorErrorDecoration: 'errorsquiggly' -}; - -class DecorationsTracker { - - public addedDecorations: string[]; - public addedDecorationsLen: number; - public changedDecorations: string[]; - public changedDecorationsLen: number; - public removedDecorations: string[]; - public removedDecorationsLen: number; - - constructor() { - this.addedDecorations = []; - this.addedDecorationsLen = 0; - this.changedDecorations = []; - this.changedDecorationsLen = 0; - this.removedDecorations = []; - this.removedDecorationsLen = 0; - } - - // --- Build decoration events - - public addNewDecoration(id: string): void { - this.addedDecorations[this.addedDecorationsLen++] = id; - } - - public addRemovedDecoration(id: string): void { - this.removedDecorations[this.removedDecorationsLen++] = id; - } - - public addMovedDecoration(id: string): void { - this.changedDecorations[this.changedDecorationsLen++] = id; - } - - public addUpdatedDecoration(id: string): void { - this.changedDecorations[this.changedDecorationsLen++] = id; - } -} - -export class InternalDecoration implements editorCommon.IModelDecoration { - _internalDecorationBrand: void; - - public readonly id: string; - public readonly internalId: number; - public readonly ownerId: number; - public readonly startMarker: LineMarker; - public readonly endMarker: LineMarker; - public options: ModelDecorationOptions; - public isForValidation: boolean; - public range: Range; - - constructor(id: string, internalId: number, ownerId: number, range: Range, startMarker: LineMarker, endMarker: LineMarker, options: ModelDecorationOptions) { - this.id = id; - this.internalId = internalId; - this.ownerId = ownerId; - this.range = range; - this.startMarker = startMarker; - this.endMarker = endMarker; - this.setOptions(options); - } - - public setOptions(options: ModelDecorationOptions) { - this.options = options; - this.isForValidation = ( - this.options.className === ClassName.EditorErrorDecoration - || this.options.className === ClassName.EditorWarningDecoration - ); - } - - public setRange(multiLineDecorationsMap: { [key: string]: InternalDecoration; }, range: Range): void { - if (this.range.equalsRange(range)) { - return; - } - - let rangeWasMultiLine = (this.range.startLineNumber !== this.range.endLineNumber); - this.range = range; - let rangeIsMultiline = (this.range.startLineNumber !== this.range.endLineNumber); - - if (rangeWasMultiLine === rangeIsMultiline) { - return; - } - - if (rangeIsMultiline) { - multiLineDecorationsMap[this.id] = this; - } else { - delete multiLineDecorationsMap[this.id]; - } - } -} +import { IntervalNode, IntervalTree, recomputeMaxEnd, getNodeIsInOverviewRuler } from 'vs/editor/common/model/intervalTree'; let _INSTANCE_COUNT = 0; /** @@ -129,7 +34,7 @@ function nextInstanceId(): string { return String.fromCharCode(CharCode.A + result - LETTERS_CNT); } -export class TextModelWithDecorations extends TextModelWithMarkers implements editorCommon.ITextModelWithDecorations { +export class TextModelWithDecorations extends TextModelWithTokens implements editorCommon.ITextModelWithDecorations { /** * Used to workaround broken clients that might attempt using a decoration id generated by a different model. @@ -137,39 +42,26 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed */ private readonly _instanceId: string; private _lastDecorationId: number; - - private _currentDecorationsTracker: DecorationsTracker; private _currentDecorationsTrackerCnt: number; - - private _currentMarkersTracker: MarkersTracker; - private _currentMarkersTrackerCnt: number; - - private _decorations: { [decorationId: string]: InternalDecoration; }; - private _internalDecorations: { [internalDecorationId: number]: InternalDecoration; }; - private _multiLineDecorationsMap: { [key: string]: InternalDecoration; }; + private _currentDecorationsTrackerDidChange: boolean; + private _decorations: { [decorationId: string]: IntervalNode; }; + private _decorationsTree: DecorationsTrees; constructor(rawTextSource: IRawTextSource, creationOptions: editorCommon.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { super(rawTextSource, creationOptions, languageIdentifier); this._instanceId = nextInstanceId(); this._lastDecorationId = 0; - - // Initialize decorations - this._currentDecorationsTracker = null; this._currentDecorationsTrackerCnt = 0; - - this._currentMarkersTracker = null; - this._currentMarkersTrackerCnt = 0; - + this._currentDecorationsTrackerDidChange = false; this._decorations = Object.create(null); - this._internalDecorations = Object.create(null); - this._multiLineDecorationsMap = Object.create(null); + this._decorationsTree = new DecorationsTrees(); } public dispose(): void { this._decorations = null; - this._internalDecorations = null; - this._multiLineDecorationsMap = null; + this._decorationsTree = null; + super.dispose(); } @@ -178,59 +70,108 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed // Destroy all my decorations this._decorations = Object.create(null); - this._internalDecorations = Object.create(null); - this._multiLineDecorationsMap = Object.create(null); + this._decorationsTree = new DecorationsTrees(); + } + + _getTrackedRangesCount(): number { + return this._decorationsTree.count(); } - private static _shouldStartMarkerSticksToPreviousCharacter(stickiness: editorCommon.TrackedRangeStickiness): boolean { - if (stickiness === editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges || stickiness === editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore) { - return true; + // --- END TrackedRanges + + protected _acquireDecorationsTracker(): void { + if (this._currentDecorationsTrackerCnt === 0) { + this._currentDecorationsTrackerDidChange = false; } - return false; + this._currentDecorationsTrackerCnt++; } - private static _shouldEndMarkerSticksToPreviousCharacter(stickiness: editorCommon.TrackedRangeStickiness): boolean { - if (stickiness === editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges || stickiness === editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore) { - return true; + protected _releaseDecorationsTracker(): void { + this._currentDecorationsTrackerCnt--; + if (this._currentDecorationsTrackerCnt === 0) { + if (this._currentDecorationsTrackerDidChange) { + this._emitModelDecorationsChangedEvent(); + } } - return false; } - _getTrackedRangesCount(): number { - return Object.keys(this._decorations).length; + protected _adjustDecorationsForEdit(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + this._currentDecorationsTrackerDidChange = true; + this._decorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers); } - // --- END TrackedRanges + protected _onBeforeEOLChange(): void { + super._onBeforeEOLChange(); + + // Ensure all decorations get their `range` set. + const versionId = this.getVersionId(); + const allDecorations = this._decorationsTree.search(0, false, false, versionId); + this._ensureNodesHaveRanges(allDecorations); + } + + protected _onAfterEOLChange(): void { + super._onAfterEOLChange(); + + // Transform back `range` to offsets + const versionId = this.getVersionId(); + const allDecorations = this._decorationsTree.collectNodesPostOrder(); + for (let i = 0, len = allDecorations.length; i < len; i++) { + const node = allDecorations[i]; + + const delta = node.cachedAbsoluteStart - node.start; + + const startOffset = this._lineStarts.getAccumulatedValue(node.range.startLineNumber - 2) + node.range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(node.range.endLineNumber - 2) + node.range.endColumn - 1; + + node.cachedAbsoluteStart = startOffset; + node.cachedAbsoluteEnd = endOffset; + node.cachedVersionId = versionId; + + node.start = startOffset - delta; + node.end = endOffset - delta; + + recomputeMaxEnd(node); + } + } public changeDecorations(callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T { this._assertNotDisposed(); try { this._eventEmitter.beginDeferredEmit(); - let decorationsTracker = this._acquireDecorationsTracker(); - return this._changeDecorations(decorationsTracker, ownerId, callback); + this._acquireDecorationsTracker(); + return this._changeDecorations(ownerId, callback); } finally { this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } - private _changeDecorations(decorationsTracker: DecorationsTracker, ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { + private _changeDecorations(ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { - return this._addDecorationImpl(decorationsTracker, ownerId, this.validateRange(range), _normalizeOptions(options)); + this._currentDecorationsTrackerDidChange = true; + return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - this._changeDecorationImpl(decorationsTracker, id, this.validateRange(newRange)); + this._currentDecorationsTrackerDidChange = true; + this._changeDecorationImpl(id, newRange); }, changeDecorationOptions: (id: string, options: editorCommon.IModelDecorationOptions) => { - this._changeDecorationOptionsImpl(decorationsTracker, id, _normalizeOptions(options)); + this._currentDecorationsTrackerDidChange = true; + this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { - this._removeDecorationImpl(decorationsTracker, id); + this._currentDecorationsTrackerDidChange = true; + this._deltaDecorationsImpl(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { - return this._deltaDecorationsImpl(decorationsTracker, ownerId, oldDecorations, this._normalizeDeltaDecorations(newDecorations)); + if (oldDecorations.length === 0 && newDecorations.length === 0) { + // nothing to do + return []; + } + this._currentDecorationsTrackerDidChange = true; + return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } }; let result: T = null; @@ -252,41 +193,90 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed if (!oldDecorations) { oldDecorations = []; } - return this.changeDecorations((changeAccessor) => { - return changeAccessor.deltaDecorations(oldDecorations, newDecorations); - }, ownerId); - } + if (oldDecorations.length === 0 && newDecorations.length === 0) { + // nothing to do + return []; + } - public removeAllDecorationsWithOwnerId(ownerId: number): void { - let toRemove: string[] = []; + try { + this._eventEmitter.beginDeferredEmit(); + this._acquireDecorationsTracker(); + this._currentDecorationsTrackerDidChange = true; + return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); + } finally { + this._releaseDecorationsTracker(); + this._eventEmitter.endDeferredEmit(); + } + } - for (let decorationId in this._decorations) { - // No `hasOwnProperty` call due to using Object.create(null) + _getTrackedRange(id: string): Range { + return this.getDecorationRange(id); + } - let decoration = this._decorations[decorationId]; + _setTrackedRange(id: string, newRange: Range, newStickiness: editorCommon.TrackedRangeStickiness): string { + const node = (id ? this._decorations[id] : null); - if (decoration.ownerId === ownerId) { - toRemove.push(decoration.id); + if (!node) { + if (!newRange) { + // node doesn't exist, the request is to delete => nothing to do + return null; } + // node doesn't exist, the request is to set => add the tracked range + return this._deltaDecorationsImpl(0, [], [{ range: newRange, options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; } - this._removeDecorationsImpl(null, toRemove); + if (!newRange) { + // node exists, the request is to delete => delete node + this._decorationsTree.delete(node); + delete this._decorations[node.id]; + return null; + } + + // node exists, the request is to set => change the tracked range and its options + const range = this._validateRangeRelaxedNoAllocations(newRange); + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; + this._decorationsTree.delete(node); + node.reset(this.getVersionId(), startOffset, endOffset, range); + node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]); + this._decorationsTree.insert(node); + return node.id; + } + + public removeAllDecorationsWithOwnerId(ownerId: number): void { + if (this._isDisposed) { + return; + } + const nodes = this._decorationsTree.collectNodesFromOwner(ownerId); + for (let i = 0, len = nodes.length; i < len; i++) { + const node = nodes[i]; + + this._decorationsTree.delete(node); + delete this._decorations[node.id]; + } } public getDecorationOptions(decorationId: string): editorCommon.IModelDecorationOptions { - let decoration = this._decorations[decorationId]; - if (!decoration) { + const node = this._decorations[decorationId]; + if (!node) { return null; } - return decoration.options; + return node.options; } public getDecorationRange(decorationId: string): Range { - let decoration = this._decorations[decorationId]; - if (!decoration) { + const node = this._decorations[decorationId]; + if (!node) { return null; } - return decoration.range; + const versionId = this.getVersionId(); + if (node.cachedVersionId !== versionId) { + this._decorationsTree.resolveNode(node, versionId); + } + if (node.range === null) { + node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); + } + return node.range; } public getLineDecorations(lineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { @@ -297,116 +287,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return this.getLinesDecorations(lineNumber, lineNumber, ownerId, filterOutValidation); } - /** - * Fetch only multi-line decorations that intersect with the given line number range - */ - private _getMultiLineDecorations(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): InternalDecoration[] { - const filterStartLineNumber = filterRange.startLineNumber; - const filterStartColumn = filterRange.startColumn; - const filterEndLineNumber = filterRange.endLineNumber; - const filterEndColumn = filterRange.endColumn; - - let result: InternalDecoration[] = [], resultLen = 0; - - for (let decorationId in this._multiLineDecorationsMap) { - // No `hasOwnProperty` call due to using Object.create(null) - let decoration = this._multiLineDecorationsMap[decorationId]; - - if (filterOwnerId && decoration.ownerId && decoration.ownerId !== filterOwnerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } - - let range = decoration.range; - - if (range.startLineNumber > filterEndLineNumber) { - continue; - } - if (range.startLineNumber === filterEndLineNumber && range.startColumn > filterEndColumn) { - continue; - } - if (range.endLineNumber < filterStartLineNumber) { - continue; - } - if (range.endLineNumber === filterStartLineNumber && range.endColumn < filterStartColumn) { - continue; - } - - result[resultLen++] = decoration; - } - - return result; - } - - private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): InternalDecoration[] { - const filterStartLineNumber = filterRange.startLineNumber; - const filterStartColumn = filterRange.startColumn; - const filterEndLineNumber = filterRange.endLineNumber; - const filterEndColumn = filterRange.endColumn; - - let result = this._getMultiLineDecorations(filterRange, filterOwnerId, filterOutValidation); - let resultLen = result.length; - let resultMap: { [decorationId: string]: boolean; } = {}; - - for (let i = 0, len = resultLen; i < len; i++) { - resultMap[result[i].id] = true; - } - - for (let lineNumber = filterStartLineNumber; lineNumber <= filterEndLineNumber; lineNumber++) { - let lineMarkers = this._lines[lineNumber - 1].getMarkers(); - if (lineMarkers === null) { - continue; - } - for (let i = 0, len = lineMarkers.length; i < len; i++) { - let lineMarker = lineMarkers[i]; - let internalDecorationId = lineMarker.internalDecorationId; - - if (internalDecorationId === 0) { - // marker does not belong to any decoration - continue; - } - - let decoration = this._internalDecorations[internalDecorationId]; - - if (resultMap.hasOwnProperty(decoration.id)) { - // decoration already in result - continue; - } - - if (filterOwnerId && decoration.ownerId && decoration.ownerId !== filterOwnerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } - - let range = decoration.range; - - if (range.startLineNumber > filterEndLineNumber) { - continue; - } - if (range.startLineNumber === filterEndLineNumber && range.startColumn > filterEndColumn) { - continue; - } - if (range.endLineNumber < filterStartLineNumber) { - continue; - } - if (range.endLineNumber === filterStartLineNumber && range.endColumn < filterStartColumn) { - continue; - } - - result[resultLen++] = decoration; - resultMap[decoration.id] = true; - } - } - - return result; - } - public getLinesDecorations(_startLineNumber: number, _endLineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { let lineCount = this.getLineCount(); let startLineNumber = Math.min(lineCount, Math.max(1, _startLineNumber)); @@ -415,426 +295,235 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return this._getDecorationsInRange(new Range(startLineNumber, 1, endLineNumber, endColumn), ownerId, filterOutValidation); } - public getDecorationsInRange(range: IRange, ownerId?: number, filterOutValidation?: boolean): editorCommon.IModelDecoration[] { + public getDecorationsInRange(range: IRange, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { let validatedRange = this.validateRange(range); return this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation); } - public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { - let result: InternalDecoration[] = [], resultLen = 0; - - for (let decorationId in this._decorations) { - // No `hasOwnProperty` call due to using Object.create(null) - let decoration = this._decorations[decorationId]; - - if (ownerId && decoration.ownerId && decoration.ownerId !== ownerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } - - result[resultLen++] = decoration; - } - - return result; + public getOverviewRulerDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { + const versionId = this.getVersionId(); + const result = this._decorationsTree.search(ownerId, filterOutValidation, true, versionId); + return this._ensureNodesHaveRanges(result); } - protected _acquireMarkersTracker(): MarkersTracker { - if (this._currentMarkersTrackerCnt === 0) { - this._currentMarkersTracker = new MarkersTracker(); - } - this._currentMarkersTrackerCnt++; - return this._currentMarkersTracker; + public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { + const versionId = this.getVersionId(); + const result = this._decorationsTree.search(ownerId, filterOutValidation, false, versionId); + return this._ensureNodesHaveRanges(result); } - protected _releaseMarkersTracker(): void { - this._currentMarkersTrackerCnt--; - if (this._currentMarkersTrackerCnt === 0) { - let markersTracker = this._currentMarkersTracker; - this._currentMarkersTracker = null; - this._handleTrackedMarkers(markersTracker); + private _emitModelDecorationsChangedEvent(): void { + if (!this._isDisposing) { + let e: textModelEvents.IModelDecorationsChangedEvent = {}; + this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelDecorationsChanged, e); } } - /** - * Handle changed markers (i.e. update decorations ranges and return the changed decorations, unique and sorted by id) - */ - private _handleTrackedMarkers(markersTracker: MarkersTracker): void { - let changedInternalDecorationIds = markersTracker.getDecorationIds(); - if (changedInternalDecorationIds.length === 0) { - return; - } + private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] { + const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; - changedInternalDecorationIds.sort(); + const versionId = this.getVersionId(); + const result = this._decorationsTree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId); - let uniqueChangedDecorations: string[] = [], uniqueChangedDecorationsLen = 0; - let previousInternalDecorationId: number = 0; - for (let i = 0, len = changedInternalDecorationIds.length; i < len; i++) { - let internalDecorationId = changedInternalDecorationIds[i]; - if (internalDecorationId === previousInternalDecorationId) { - continue; - } - previousInternalDecorationId = internalDecorationId; + return this._ensureNodesHaveRanges(result); + } - let decoration = this._internalDecorations[internalDecorationId]; - if (!decoration) { - // perhaps the decoration was removed in the meantime - continue; + private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] { + for (let i = 0, len = nodes.length; i < len; i++) { + const node = nodes[i]; + if (node.range === null) { + node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); } - - let startMarker = decoration.startMarker.position; - let endMarker = decoration.endMarker.position; - let range = TextModelWithDecorations._createRangeFromMarkers(startMarker, endMarker); - decoration.setRange(this._multiLineDecorationsMap, range); - - uniqueChangedDecorations[uniqueChangedDecorationsLen++] = decoration.id; - } - - if (uniqueChangedDecorations.length > 0) { - let e: textModelEvents.IModelDecorationsChangedEvent = { - addedDecorations: [], - changedDecorations: uniqueChangedDecorations, - removedDecorations: [] - }; - this.emitModelDecorationsChangedEvent(e); } + return nodes; } - private static _createRangeFromMarkers(startPosition: Position, endPosition: Position): Range { - if (endPosition.isBefore(startPosition)) { - // This tracked range has turned in on itself (end marker before start marker) - // This can happen in extreme editing conditions where lots of text is removed and lots is added + private _getRangeAt(start: number, end: number): Range { + const startResult = this._lineStarts.getIndexOf(start); + const startLineLength = this._lines[startResult.index].text.length; + const startColumn = Math.min(startResult.remainder + 1, startLineLength + 1); - // Treat it as a collapsed range - return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column); - } - return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - } + const endResult = this._lineStarts.getIndexOf(end); + const endLineLength = this._lines[endResult.index].text.length; + const endColumn = Math.min(endResult.remainder + 1, endLineLength + 1); - private _acquireDecorationsTracker(): DecorationsTracker { - if (this._currentDecorationsTrackerCnt === 0) { - this._currentDecorationsTracker = new DecorationsTracker(); - } - this._currentDecorationsTrackerCnt++; - return this._currentDecorationsTracker; + return new Range(startResult.index + 1, startColumn, endResult.index + 1, endColumn); } - private _releaseDecorationsTracker(): void { - this._currentDecorationsTrackerCnt--; - if (this._currentDecorationsTrackerCnt === 0) { - let decorationsTracker = this._currentDecorationsTracker; - this._currentDecorationsTracker = null; - this._handleTrackedDecorations(decorationsTracker); - } - } - - private _handleTrackedDecorations(decorationsTracker: DecorationsTracker): void { - if ( - decorationsTracker.addedDecorationsLen === 0 - && decorationsTracker.changedDecorationsLen === 0 - && decorationsTracker.removedDecorationsLen === 0 - ) { + private _changeDecorationImpl(decorationId: string, _range: IRange): void { + const node = this._decorations[decorationId]; + if (!node) { return; } + const range = this._validateRangeRelaxedNoAllocations(_range); + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; - let e: textModelEvents.IModelDecorationsChangedEvent = { - addedDecorations: decorationsTracker.addedDecorations, - changedDecorations: decorationsTracker.changedDecorations, - removedDecorations: decorationsTracker.removedDecorations - }; - this.emitModelDecorationsChangedEvent(e); + this._decorationsTree.delete(node); + node.reset(this.getVersionId(), startOffset, endOffset, range); + this._decorationsTree.insert(node); } - private emitModelDecorationsChangedEvent(e: textModelEvents.IModelDecorationsChangedEvent): void { - if (!this._isDisposing) { - this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelDecorationsChanged, e); + private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void { + const node = this._decorations[decorationId]; + if (!node) { + return; } - } - private _normalizeDeltaDecorations(deltaDecorations: editorCommon.IModelDeltaDecoration[]): ModelDeltaDecoration[] { - let result: ModelDeltaDecoration[] = []; - for (let i = 0, len = deltaDecorations.length; i < len; i++) { - let deltaDecoration = deltaDecorations[i]; - result.push(new ModelDeltaDecoration(i, this.validateRange(deltaDecoration.range), _normalizeOptions(deltaDecoration.options))); - } - return result; - } + const nodeWasInOverviewRuler = (node.options.overviewRuler.color ? true : false); + const nodeIsInOverviewRuler = (options.overviewRuler.color ? true : false); - private _externalDecorationId(internalId: number): string { - return `${this._instanceId};${internalId}`; + if (nodeWasInOverviewRuler !== nodeIsInOverviewRuler) { + // Delete + Insert due to an overview ruler status change + this._decorationsTree.delete(node); + node.setOptions(options); + this._decorationsTree.insert(node); + } else { + node.setOptions(options); + } } - private _addDecorationImpl(decorationsTracker: DecorationsTracker, ownerId: number, _range: Range, options: ModelDecorationOptions): string { - let range = this.validateRange(_range); - - let internalDecorationId = (++this._lastDecorationId); - let decorationId = this._externalDecorationId(internalDecorationId); - - let markers = this._addMarkers([ - { - internalDecorationId: internalDecorationId, - position: new Position(range.startLineNumber, range.startColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldStartMarkerSticksToPreviousCharacter(options.stickiness) - }, - { - internalDecorationId: internalDecorationId, - position: new Position(range.endLineNumber, range.endColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldEndMarkerSticksToPreviousCharacter(options.stickiness) - } - ]); + private _deltaDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { + const versionId = this.getVersionId(); - let decoration = new InternalDecoration(decorationId, internalDecorationId, ownerId, range, markers[0], markers[1], options); - this._decorations[decorationId] = decoration; - this._internalDecorations[internalDecorationId] = decoration; - if (range.startLineNumber !== range.endLineNumber) { - this._multiLineDecorationsMap[decorationId] = decoration; - } + const oldDecorationsLen = oldDecorationsIds.length; + let oldDecorationIndex = 0; - decorationsTracker.addNewDecoration(decorationId); + const newDecorationsLen = newDecorations.length; + let newDecorationIndex = 0; - return decorationId; - } + let result = new Array(newDecorationsLen); + while (oldDecorationIndex < oldDecorationsLen || newDecorationIndex < newDecorationsLen) { - private _addDecorationsImpl(decorationsTracker: DecorationsTracker, ownerId: number, newDecorations: ModelDeltaDecoration[]): string[] { - let internalDecorationIds: number[] = []; - let decorationIds: string[] = []; - let newMarkers: INewMarker[] = []; + let node: IntervalNode = null; - for (let i = 0, len = newDecorations.length; i < len; i++) { - let newDecoration = newDecorations[i]; - let range = newDecoration.range; - let stickiness = newDecoration.options.stickiness; + if (oldDecorationIndex < oldDecorationsLen) { + // (1) get ourselves an old node + do { + node = this._decorations[oldDecorationsIds[oldDecorationIndex++]]; + } while (!node && oldDecorationIndex < oldDecorationsLen); - let internalDecorationId = (++this._lastDecorationId); - let decorationId = this._externalDecorationId(internalDecorationId); + // (2) remove the node from the tree (if it exists) + if (node) { + this._decorationsTree.delete(node); + } + } - internalDecorationIds[i] = internalDecorationId; - decorationIds[i] = decorationId; + if (newDecorationIndex < newDecorationsLen) { + // (3) create a new node if necessary + if (!node) { + const internalDecorationId = (++this._lastDecorationId); + const decorationId = `${this._instanceId};${internalDecorationId}`; + node = new IntervalNode(decorationId, 0, 0); + this._decorations[decorationId] = node; + } - newMarkers[2 * i] = { - internalDecorationId: internalDecorationId, - position: new Position(range.startLineNumber, range.startColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldStartMarkerSticksToPreviousCharacter(stickiness) - }; + // (4) initialize node + const newDecoration = newDecorations[newDecorationIndex]; + const range = this._validateRangeRelaxedNoAllocations(newDecoration.range); + const options = _normalizeOptions(newDecoration.options); + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; - newMarkers[2 * i + 1] = { - internalDecorationId: internalDecorationId, - position: new Position(range.endLineNumber, range.endColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldEndMarkerSticksToPreviousCharacter(stickiness) - }; - } + node.ownerId = ownerId; + node.reset(versionId, startOffset, endOffset, range); + node.setOptions(options); - let markerIds = this._addMarkers(newMarkers); + this._decorationsTree.insert(node); - for (let i = 0, len = newDecorations.length; i < len; i++) { - let newDecoration = newDecorations[i]; - let range = newDecoration.range; - let internalDecorationId = internalDecorationIds[i]; - let decorationId = decorationIds[i]; - let startMarker = markerIds[2 * i]; - let endMarker = markerIds[2 * i + 1]; + result[newDecorationIndex] = node.id; - let decoration = new InternalDecoration(decorationId, internalDecorationId, ownerId, range, startMarker, endMarker, newDecoration.options); - this._decorations[decorationId] = decoration; - this._internalDecorations[internalDecorationId] = decoration; - if (range.startLineNumber !== range.endLineNumber) { - this._multiLineDecorationsMap[decorationId] = decoration; + newDecorationIndex++; + } else { + if (node) { + delete this._decorations[node.id]; + } } - - decorationsTracker.addNewDecoration(decorationId); } - return decorationIds; + return result; } +} - private _changeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string, newRange: Range): void { - let decoration = this._decorations[decorationId]; - if (!decoration) { - return; - } - - let startMarker = decoration.startMarker; - if (newRange.startLineNumber !== startMarker.position.lineNumber) { - // move marker between lines - this._lines[startMarker.position.lineNumber - 1].removeMarker(startMarker); - this._lines[newRange.startLineNumber - 1].addMarker(startMarker); - } - startMarker.setPosition(new Position(newRange.startLineNumber, newRange.startColumn)); +class DecorationsTrees { - let endMarker = decoration.endMarker; - if (newRange.endLineNumber !== endMarker.position.lineNumber) { - // move marker between lines - this._lines[endMarker.position.lineNumber - 1].removeMarker(endMarker); - this._lines[newRange.endLineNumber - 1].addMarker(endMarker); - } - endMarker.setPosition(new Position(newRange.endLineNumber, newRange.endColumn)); + /** + * This tree holds decorations that do not show up in the overview ruler. + */ + private _decorationsTree0: IntervalTree; - decoration.setRange(this._multiLineDecorationsMap, newRange); + /** + * This tree holds decorations that show up in the overview ruler. + */ + private _decorationsTree1: IntervalTree; - decorationsTracker.addMovedDecoration(decorationId); + constructor() { + this._decorationsTree0 = new IntervalTree(); + this._decorationsTree1 = new IntervalTree(); } - private _changeDecorationOptionsImpl(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { - let decoration = this._decorations[decorationId]; - if (!decoration) { - return; - } - - if (decoration.options.stickiness !== options.stickiness) { - decoration.startMarker.stickToPreviousCharacter = TextModelWithDecorations._shouldStartMarkerSticksToPreviousCharacter(options.stickiness); - decoration.endMarker.stickToPreviousCharacter = TextModelWithDecorations._shouldEndMarkerSticksToPreviousCharacter(options.stickiness); - } - - decoration.setOptions(options); - - decorationsTracker.addUpdatedDecoration(decorationId); + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + const r0 = this._decorationsTree0.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + const r1 = this._decorationsTree1.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + return r0.concat(r1); } - private _removeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string): void { - let decoration = this._decorations[decorationId]; - if (!decoration) { - return; - } - - this._removeMarkers([decoration.startMarker, decoration.endMarker]); - - delete this._multiLineDecorationsMap[decorationId]; - delete this._decorations[decorationId]; - delete this._internalDecorations[decoration.internalId]; - - if (decorationsTracker) { - decorationsTracker.addRemovedDecoration(decorationId); + public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + if (overviewRulerOnly) { + return this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId); + } else { + const r0 = this._decorationsTree0.search(filterOwnerId, filterOutValidation, cachedVersionId); + const r1 = this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId); + return r0.concat(r1); } } - private _removeDecorationsImpl(decorationsTracker: DecorationsTracker, decorationIds: string[]): void { - let removeMarkers: LineMarker[] = [], removeMarkersLen = 0; - - for (let i = 0, len = decorationIds.length; i < len; i++) { - let decorationId = decorationIds[i]; - let decoration = this._decorations[decorationId]; - if (!decoration) { - continue; - } - - if (decorationsTracker) { - decorationsTracker.addRemovedDecoration(decorationId); - } - - removeMarkers[removeMarkersLen++] = decoration.startMarker; - removeMarkers[removeMarkersLen++] = decoration.endMarker; - delete this._multiLineDecorationsMap[decorationId]; - delete this._decorations[decorationId]; - delete this._internalDecorations[decoration.internalId]; - } - - if (removeMarkers.length > 0) { - this._removeMarkers(removeMarkers); - } + public count(): number { + const c0 = this._decorationsTree0.count(); + const c1 = this._decorationsTree1.count(); + return c0 + c1; } - private _resolveOldDecorations(oldDecorations: string[]): InternalDecoration[] { - let result: InternalDecoration[] = []; - for (let i = 0, len = oldDecorations.length; i < len; i++) { - let id = oldDecorations[i]; - let decoration = this._decorations[id]; - if (!decoration) { - continue; - } - - result.push(decoration); - } - return result; + public collectNodesFromOwner(ownerId: number): IntervalNode[] { + const r0 = this._decorationsTree0.collectNodesFromOwner(ownerId); + const r1 = this._decorationsTree1.collectNodesFromOwner(ownerId); + return r0.concat(r1); } - private _deltaDecorationsImpl(decorationsTracker: DecorationsTracker, ownerId: number, oldDecorationsIds: string[], newDecorations: ModelDeltaDecoration[]): string[] { - - if (oldDecorationsIds.length === 0) { - // Nothing to remove - return this._addDecorationsImpl(decorationsTracker, ownerId, newDecorations); - } - - if (newDecorations.length === 0) { - // Nothing to add - this._removeDecorationsImpl(decorationsTracker, oldDecorationsIds); - return []; - } - - let oldDecorations = this._resolveOldDecorations(oldDecorationsIds); - - oldDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - newDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - - let result: string[] = [], - oldDecorationsIndex = 0, - oldDecorationsLength = oldDecorations.length, - newDecorationsIndex = 0, - newDecorationsLength = newDecorations.length, - decorationsToAdd: ModelDeltaDecoration[] = [], - decorationsToRemove: string[] = []; - - while (oldDecorationsIndex < oldDecorationsLength && newDecorationsIndex < newDecorationsLength) { - let oldDecoration = oldDecorations[oldDecorationsIndex]; - let newDecoration = newDecorations[newDecorationsIndex]; - let comparison = Range.compareRangesUsingStarts(oldDecoration.range, newDecoration.range); - - if (comparison < 0) { - // `oldDecoration` is before `newDecoration` => remove `oldDecoration` - decorationsToRemove.push(oldDecoration.id); - oldDecorationsIndex++; - continue; - } - - if (comparison > 0) { - // `newDecoration` is before `oldDecoration` => add `newDecoration` - decorationsToAdd.push(newDecoration); - newDecorationsIndex++; - continue; - } - - // The ranges of `oldDecoration` and `newDecoration` are equal - - if (!oldDecoration.options.equals(newDecoration.options)) { - // The options do not match => remove `oldDecoration` - decorationsToRemove.push(oldDecoration.id); - oldDecorationsIndex++; - continue; - } - - // Bingo! We can reuse `oldDecoration` for `newDecoration` - result[newDecoration.index] = oldDecoration.id; - oldDecorationsIndex++; - newDecorationsIndex++; - } - - while (oldDecorationsIndex < oldDecorationsLength) { - // No more new decorations => remove decoration at `oldDecorationsIndex` - decorationsToRemove.push(oldDecorations[oldDecorationsIndex].id); - oldDecorationsIndex++; - } + public collectNodesPostOrder(): IntervalNode[] { + const r0 = this._decorationsTree0.collectNodesPostOrder(); + const r1 = this._decorationsTree1.collectNodesPostOrder(); + return r0.concat(r1); + } - while (newDecorationsIndex < newDecorationsLength) { - // No more old decorations => add decoration at `newDecorationsIndex` - decorationsToAdd.push(newDecorations[newDecorationsIndex]); - newDecorationsIndex++; + public insert(node: IntervalNode): void { + if (getNodeIsInOverviewRuler(node)) { + this._decorationsTree1.insert(node); + } else { + this._decorationsTree0.insert(node); } + } - // Remove `decorationsToRemove` - if (decorationsToRemove.length > 0) { - this._removeDecorationsImpl(decorationsTracker, decorationsToRemove); + public delete(node: IntervalNode): void { + if (getNodeIsInOverviewRuler(node)) { + this._decorationsTree1.delete(node); + } else { + this._decorationsTree0.delete(node); } + } - // Add `decorationsToAdd` - if (decorationsToAdd.length > 0) { - let newIds = this._addDecorationsImpl(decorationsTracker, ownerId, decorationsToAdd); - for (let i = 0, len = decorationsToAdd.length; i < len; i++) { - result[decorationsToAdd[i].index] = newIds[i]; - } + public resolveNode(node: IntervalNode, cachedVersionId: number): void { + if (getNodeIsInOverviewRuler(node)) { + this._decorationsTree1.resolveNode(node, cachedVersionId); + } else { + this._decorationsTree0.resolveNode(node, cachedVersionId); } + } - return result; + public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + this._decorationsTree0.acceptReplace(offset, length, textLength, forceMoveMarkers); + this._decorationsTree1.acceptReplace(offset, length, textLength, forceMoveMarkers); } } @@ -847,12 +536,14 @@ export class ModelDecorationOverviewRulerOptions implements editorCommon.IModelD readonly darkColor: string | ThemeColor; readonly hcColor: string | ThemeColor; readonly position: editorCommon.OverviewRulerLane; + _resolvedColor: string; constructor(options: editorCommon.IModelDecorationOverviewRulerOptions) { this.color = strings.empty; this.darkColor = strings.empty; this.hcColor = strings.empty; this.position = editorCommon.OverviewRulerLane.Center; + this._resolvedColor = null; if (options && options.color) { this.color = options.color; @@ -949,18 +640,15 @@ export class ModelDecorationOptions implements editorCommon.IModelDecorationOpti } ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({}); -class ModelDeltaDecoration implements editorCommon.IModelDeltaDecoration { - - index: number; - range: Range; - options: ModelDecorationOptions; - - constructor(index: number, range: Range, options: ModelDecorationOptions) { - this.index = index; - this.range = range; - this.options = options; - } -} +/** + * The order carefully matches the values of the enum. + */ +const TRACKED_RANGE_OPTIONS = [ + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), +]; function _normalizeOptions(options: editorCommon.IModelDecorationOptions): ModelDecorationOptions { if (options instanceof ModelDecorationOptions) { diff --git a/src/vs/editor/common/model/textModelWithMarkers.ts b/src/vs/editor/common/model/textModelWithMarkers.ts deleted file mode 100644 index 6100bfc399924..0000000000000 --- a/src/vs/editor/common/model/textModelWithMarkers.ts +++ /dev/null @@ -1,174 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { IdGenerator } from 'vs/base/common/idGenerator'; -import { Position } from 'vs/editor/common/core/position'; -import { ITextModelWithMarkers, ITextModelCreationOptions } from 'vs/editor/common/editorCommon'; -import { LineMarker } from 'vs/editor/common/model/modelLine'; -import { TextModelWithTokens } from 'vs/editor/common/model/textModelWithTokens'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; - -export interface IMarkerIdToMarkerMap { - [key: string]: LineMarker; -} - -export interface INewMarker { - internalDecorationId: number; - position: Position; - stickToPreviousCharacter: boolean; -} - -var _INSTANCE_COUNT = 0; - -export class TextModelWithMarkers extends TextModelWithTokens implements ITextModelWithMarkers { - - private _markerIdGenerator: IdGenerator; - protected _markerIdToMarker: IMarkerIdToMarkerMap; - - constructor(rawTextSource: IRawTextSource, creationOptions: ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { - super(rawTextSource, creationOptions, languageIdentifier); - this._markerIdGenerator = new IdGenerator((++_INSTANCE_COUNT) + ';'); - this._markerIdToMarker = Object.create(null); - } - - public dispose(): void { - this._markerIdToMarker = null; - super.dispose(); - } - - protected _resetValue(newValue: ITextSource): void { - super._resetValue(newValue); - - // Destroy all my markers - this._markerIdToMarker = Object.create(null); - } - - _addMarker(internalDecorationId: number, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string { - var pos = this.validatePosition(new Position(lineNumber, column)); - - var marker = new LineMarker(this._markerIdGenerator.nextId(), internalDecorationId, pos, stickToPreviousCharacter); - this._markerIdToMarker[marker.id] = marker; - - this._lines[pos.lineNumber - 1].addMarker(marker); - - return marker.id; - } - - protected _addMarkers(newMarkers: INewMarker[]): LineMarker[] { - if (newMarkers.length === 0) { - return []; - } - - let markers: LineMarker[] = []; - for (let i = 0, len = newMarkers.length; i < len; i++) { - let newMarker = newMarkers[i]; - - let marker = new LineMarker(this._markerIdGenerator.nextId(), newMarker.internalDecorationId, newMarker.position, newMarker.stickToPreviousCharacter); - this._markerIdToMarker[marker.id] = marker; - - markers[i] = marker; - } - - let sortedMarkers = markers.slice(0); - sortedMarkers.sort((a, b) => { - return a.position.lineNumber - b.position.lineNumber; - }); - - let currentLineNumber = 0; - let currentMarkers: LineMarker[] = [], currentMarkersLen = 0; - for (let i = 0, len = sortedMarkers.length; i < len; i++) { - let marker = sortedMarkers[i]; - - if (marker.position.lineNumber !== currentLineNumber) { - if (currentLineNumber !== 0) { - this._lines[currentLineNumber - 1].addMarkers(currentMarkers); - } - currentLineNumber = marker.position.lineNumber; - currentMarkers.length = 0; - currentMarkersLen = 0; - } - - currentMarkers[currentMarkersLen++] = marker; - } - this._lines[currentLineNumber - 1].addMarkers(currentMarkers); - - return markers; - } - - _changeMarker(id: string, lineNumber: number, column: number): void { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return; - } - - let newPos = this.validatePosition(new Position(lineNumber, column)); - - if (newPos.lineNumber !== marker.position.lineNumber) { - // Move marker between lines - this._lines[marker.position.lineNumber - 1].removeMarker(marker); - this._lines[newPos.lineNumber - 1].addMarker(marker); - } - - marker.setPosition(newPos); - } - - _changeMarkerStickiness(id: string, newStickToPreviousCharacter: boolean): void { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return; - } - - marker.stickToPreviousCharacter = newStickToPreviousCharacter; - } - - _getMarker(id: string): Position { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return null; - } - - return marker.position; - } - - _getMarkersCount(): number { - return Object.keys(this._markerIdToMarker).length; - } - - _removeMarker(id: string): void { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return; - } - - this._lines[marker.position.lineNumber - 1].removeMarker(marker); - delete this._markerIdToMarker[id]; - } - - protected _removeMarkers(markers: LineMarker[]): void { - markers.sort((a, b) => { - return a.position.lineNumber - b.position.lineNumber; - }); - - let currentLineNumber = 0; - let currentMarkers: { [markerId: string]: boolean; } = null; - for (let i = 0, len = markers.length; i < len; i++) { - let marker = markers[i]; - delete this._markerIdToMarker[marker.id]; - - if (marker.position.lineNumber !== currentLineNumber) { - if (currentLineNumber !== 0) { - this._lines[currentLineNumber - 1].removeMarkers(currentMarkers); - } - currentLineNumber = marker.position.lineNumber; - currentMarkers = Object.create(null); - } - - currentMarkers[marker.id] = true; - } - this._lines[currentLineNumber - 1].removeMarkers(currentMarkers); - } -} diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 1fbc491f2715c..caaa8470b7e68 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -26,7 +26,7 @@ import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; import { IRawTextSource, TextSource, RawTextSource, ITextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; -import { ClassName } from 'vs/editor/common/model/textModelWithDecorations'; +import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { themeColorFromId, ThemeColor } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 5cf9d2c728a9e..f57572f1e38b8 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -10,10 +10,12 @@ import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { PrefixSumComputerWithCache } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ViewLineData, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; +import { ViewLineData, ICoordinatesConverter, IOverviewRulerDecorations } from 'vs/editor/common/viewModel/viewModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { ThemeColor, ITheme } from 'vs/platform/theme/common/themeService'; +import { Color } from 'vs/base/common/color'; export class OutputPosition { _outputPositionBrand: void; @@ -57,6 +59,7 @@ export interface ISplitLine { getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number; getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position; + getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number; } export interface IViewModelLinesCollection { @@ -82,6 +85,9 @@ export interface IViewModelLinesCollection { getViewLineMaxColumn(viewLineNumber: number): number; getViewLineData(viewLineNumber: number): ViewLineData; getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[]; + + getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations; + getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): editorCommon.IModelDecoration[]; } export class CoordinatesConverter implements ICoordinatesConverter { @@ -650,6 +656,83 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r); return r; } + + private _getViewLineNumberForModelPosition(inputLineNumber: number, inputColumn: number): number { + let lineIndex = inputLineNumber - 1; + if (this.lines[lineIndex].isVisible()) { + // this model line is visible + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, inputColumn); + } + + // this model line is not visible + while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) { + lineIndex--; + } + if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) { + // Could not reach a real line + return 1; + } + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); + } + + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations { + const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); + const result = new OverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.options.overviewRuler; + const lane = opts.position; + if (lane === 0) { + continue; + } + const color = resolveColor(opts, theme); + const viewStartLineNumber = this._getViewLineNumberForModelPosition(decoration.range.startLineNumber, decoration.range.startColumn); + const viewEndLineNumber = this._getViewLineNumberForModelPosition(decoration.range.endLineNumber, decoration.range.endColumn); + + result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); + } + return result.result; + } + + public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): editorCommon.IModelDecoration[] { + const modelStart = this.convertViewPositionToModelPosition(range.startLineNumber, range.startColumn); + const modelEnd = this.convertViewPositionToModelPosition(range.endLineNumber, range.endColumn); + + if (modelEnd.lineNumber - modelStart.lineNumber <= range.endLineNumber - range.startLineNumber) { + // most likely there are no hidden lines => fast path + return this.model.getDecorationsInRange(new Range(modelStart.lineNumber, modelStart.column, modelEnd.lineNumber, modelEnd.column), ownerId, filterOutValidation); + } + + let result: editorCommon.IModelDecoration[] = []; + const modelStartLineIndex = modelStart.lineNumber - 1; + const modelEndLineIndex = modelEnd.lineNumber - 1; + + let reqStart: Position = null; + for (let modelLineIndex = modelStartLineIndex; modelLineIndex <= modelEndLineIndex; modelLineIndex++) { + const line = this.lines[modelLineIndex]; + if (line.isVisible()) { + // merge into previous request + if (reqStart === null) { + reqStart = new Position(modelLineIndex + 1, modelLineIndex === modelStartLineIndex ? modelStart.column : 1); + } + } else { + // hit invisible line => flush request + if (reqStart !== null) { + result = result.concat(this.model.getDecorationsInRange(new Range(reqStart.lineNumber, reqStart.column, modelLineIndex + 1, 1))); + reqStart = null; + } + } + } + + if (reqStart !== null) { + result = result.concat(this.model.getDecorationsInRange(new Range(reqStart.lineNumber, reqStart.column, modelEnd.lineNumber, modelEnd.column))); + reqStart = null; + } + + return result; + } } class VisibleIdentitySplitLine implements ISplitLine { @@ -711,6 +794,10 @@ class VisibleIdentitySplitLine implements ISplitLine { public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { return new Position(deltaLineNumber, inputColumn); } + + public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number { + return deltaLineNumber; + } } class InvisibleIdentitySplitLine implements ISplitLine { @@ -761,6 +848,10 @@ class InvisibleIdentitySplitLine implements ISplitLine { public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { throw new Error('Not supported'); } + + public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number { + throw new Error('Not supported'); + } } export class SplitLine implements ISplitLine { @@ -914,6 +1005,14 @@ export class SplitLine implements ISplitLine { // console.log('in -> out ' + deltaLineNumber + ',' + inputColumn + ' ===> ' + (deltaLineNumber+outputLineIndex) + ',' + outputColumn); return new Position(deltaLineNumber + outputLineIndex, outputColumn); } + + public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number { + if (!this._isVisible) { + throw new Error('Not supported'); + } + const r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1); + return (deltaLineNumber + r.outputLineIndex); + } } function createSplitLine(linePositionMapperFactory: ILineMapperFactory, text: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, isVisible: boolean): ISplitLine { @@ -1093,4 +1192,77 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return result; } + + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations { + const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); + const result = new OverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.options.overviewRuler; + const lane = opts.position; + if (lane === 0) { + continue; + } + const color = resolveColor(opts, theme); + const viewStartLineNumber = decoration.range.startLineNumber; + const viewEndLineNumber = decoration.range.endLineNumber; + + result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); + } + return result.result; + } + + public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): editorCommon.IModelDecoration[] { + return this.model.getDecorationsInRange(range, ownerId, filterOutValidation); + } +} + +class OverviewRulerDecorations { + + readonly result: IOverviewRulerDecorations = Object.create(null); + + constructor() { + } + + public accept(color: string, startLineNumber: number, endLineNumber: number, lane: number): void { + let prev = this.result[color]; + + if (prev) { + const prevLane = prev[prev.length - 3]; + const prevEndLineNumber = prev[prev.length - 1]; + if (prevLane === lane && prevEndLineNumber + 1 >= startLineNumber) { + // merge into prev + if (endLineNumber > prevEndLineNumber) { + prev[prev.length - 1] = endLineNumber; + } + return; + } + + // push + prev.push(lane, startLineNumber, endLineNumber); + } else { + this.result[color] = [lane, startLineNumber, endLineNumber]; + } + } +} + + +function resolveColor(opts: ModelDecorationOverviewRulerOptions, theme: ITheme): string { + if (!opts._resolvedColor) { + const themeType = theme.type; + const color = (themeType === 'dark' ? opts.darkColor : themeType === 'light' ? opts.color : opts.hcColor); + opts._resolvedColor = resolveRulerColor(color, theme); + } + return opts._resolvedColor; +} + +function resolveRulerColor(color: string | ThemeColor, theme: ITheme): string { + if (typeof color === 'string') { + return color; + } + let c = color ? theme.getColor(color.id) : null; + if (!c) { + c = Color.transparent; + } + return c.toString(); } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 2890bf9314f88..88787418a97df 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { INewScrollPosition, IModelDecoration, EndOfLinePreference, IViewState } from 'vs/editor/common/editorCommon'; +import { INewScrollPosition, EndOfLinePreference, IViewState, IModelDecorationOptions } from 'vs/editor/common/editorCommon'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -14,6 +14,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Scrollable, IScrollPosition } from 'vs/base/common/scrollable'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; +import { ITheme } from 'vs/platform/theme/common/themeService'; export interface IViewWhitespaceViewportData { readonly id: number; @@ -138,7 +139,8 @@ export interface IViewModel { getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; - getAllOverviewRulerDecorations(): ViewModelDecoration[]; + getAllOverviewRulerDecorations(theme: ITheme): IOverviewRulerDecorations; + invalidateOverviewRulerColorCache(): void; getValueInRange(range: Range, eol: EndOfLinePreference): string; getModelLineMaxColumn(modelLineNumber: number): number; @@ -267,15 +269,25 @@ export class InlineDecoration { export class ViewModelDecoration { _viewModelDecorationBrand: void; - public range: Range; - public readonly source: IModelDecoration; + public readonly range: Range; + public readonly options: IModelDecorationOptions; - constructor(source: IModelDecoration) { - this.range = null; - this.source = source; + constructor(range: Range, options: IModelDecorationOptions) { + this.range = range; + this.options = options; } } +/** + * Decorations are encoded in a number array using the following scheme: + * - 3*i = lane + * - 3*i+1 = startLineNumber + * - 3*i+2 = endLineNumber + */ +export interface IOverviewRulerDecorations { + [color: string]: number[]; +} + export class ViewEventsCollector { private _events: ViewEvent[]; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index f4311681f8214..b5c6aa4b5ba01 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { InlineDecoration, ViewModelDecoration, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { IViewModelLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; export interface IDecorationsViewportData { /** @@ -27,6 +27,7 @@ export class ViewModelDecorations implements IDisposable { private readonly editorId: number; private readonly model: editorCommon.IModel; private readonly configuration: editorCommon.IConfiguration; + private readonly _linesCollection: IViewModelLinesCollection; private readonly _coordinatesConverter: ICoordinatesConverter; private _decorationsCache: { [decorationId: string]: ViewModelDecoration; }; @@ -34,10 +35,11 @@ export class ViewModelDecorations implements IDisposable { private _cachedModelDecorationsResolver: IDecorationsViewportData; private _cachedModelDecorationsResolverViewRange: Range; - constructor(editorId: number, model: editorCommon.IModel, configuration: editorCommon.IConfiguration, coordinatesConverter: ICoordinatesConverter) { + constructor(editorId: number, model: editorCommon.IModel, configuration: editorCommon.IConfiguration, linesCollection: IViewModelLinesCollection, coordinatesConverter: ICoordinatesConverter) { this.editorId = editorId; this.model = model; this.configuration = configuration; + this._linesCollection = linesCollection; this._coordinatesConverter = coordinatesConverter; this._decorationsCache = Object.create(null); this._clearCachedModelDecorationsResolver(); @@ -58,26 +60,8 @@ export class ViewModelDecorations implements IDisposable { this._clearCachedModelDecorationsResolver(); } - public onModelDecorationsChanged(e: IModelDecorationsChangedEvent): void { - let changedDecorations = e.changedDecorations; - for (let i = 0, len = changedDecorations.length; i < len; i++) { - let changedDecoration = changedDecorations[i]; - let myDecoration = this._decorationsCache[changedDecoration]; - if (!myDecoration) { - continue; - } - - myDecoration.range = null; - } - - let removedDecorations = e.removedDecorations; - if (this._decorationsCache !== null && this._decorationsCache !== undefined) { - for (let i = 0, len = removedDecorations.length; i < len; i++) { - let removedDecoration = removedDecorations[i]; - delete this._decorationsCache[removedDecoration]; - } - } - + public onModelDecorationsChanged(): void { + this._decorationsCache = Object.create(null); this._clearCachedModelDecorationsResolver(); } @@ -88,42 +72,25 @@ export class ViewModelDecorations implements IDisposable { } private _getOrCreateViewModelDecoration(modelDecoration: editorCommon.IModelDecoration): ViewModelDecoration { - let id = modelDecoration.id; + const id = modelDecoration.id; let r = this._decorationsCache[id]; if (!r) { - r = new ViewModelDecoration(modelDecoration); - this._decorationsCache[id] = r; - } - if (r.range === null) { const modelRange = modelDecoration.range; - if (modelDecoration.options.isWholeLine) { - let start = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.startLineNumber, 1)); - let end = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.endLineNumber, this.model.getLineMaxColumn(modelRange.endLineNumber))); - r.range = new Range(start.lineNumber, start.column, end.lineNumber, end.column); + const options = modelDecoration.options; + let viewRange: Range; + if (options.isWholeLine) { + const start = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.startLineNumber, 1)); + const end = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.endLineNumber, this.model.getLineMaxColumn(modelRange.endLineNumber))); + viewRange = new Range(start.lineNumber, start.column, end.lineNumber, end.column); } else { - r.range = this._coordinatesConverter.convertModelRangeToViewRange(modelRange); + viewRange = this._coordinatesConverter.convertModelRangeToViewRange(modelRange); } + r = new ViewModelDecoration(viewRange, options); + this._decorationsCache[id] = r; } return r; } - public getAllOverviewRulerDecorations(): ViewModelDecoration[] { - let modelDecorations = this.model.getAllDecorations(this.editorId, this.configuration.editor.readOnly); - let result: ViewModelDecoration[] = [], resultLen = 0; - for (let i = 0, len = modelDecorations.length; i < len; i++) { - let modelDecoration = modelDecorations[i]; - let decorationOptions = modelDecoration.options; - - if (!decorationOptions.overviewRuler.color) { - continue; - } - - let viewModelDecoration = this._getOrCreateViewModelDecoration(modelDecoration); - result[resultLen++] = viewModelDecoration; - } - return result; - } - public getDecorationsViewportData(viewRange: Range): IDecorationsViewportData { var cacheIsValid = true; cacheIsValid = cacheIsValid && (this._cachedModelDecorationsResolver !== null); @@ -136,10 +103,9 @@ export class ViewModelDecorations implements IDisposable { } private _getDecorationsViewportData(viewportRange: Range): IDecorationsViewportData { - let viewportModelRange = this._coordinatesConverter.convertViewRangeToModelRange(viewportRange); - let startLineNumber = viewportRange.startLineNumber; - let endLineNumber = viewportRange.endLineNumber; - let modelDecorations = this.model.getDecorationsInRange(viewportModelRange, this.editorId, this.configuration.editor.readOnly); + const modelDecorations = this._linesCollection.getDecorationsInRange(viewportRange, this.editorId, this.configuration.editor.readOnly); + const startLineNumber = viewportRange.startLineNumber; + const endLineNumber = viewportRange.endLineNumber; let decorationsInViewport: ViewModelDecoration[] = [], decorationsInViewportLen = 0; let inlineDecorations: InlineDecoration[][] = []; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index f1a7fcbf961b3..25a52e2f2b9e5 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -12,7 +12,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { TokenizationRegistry, ColorId, LanguageId } from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { MinimapLinesRenderingData, ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter, ViewEventsCollector } from 'vs/editor/common/viewModel/viewModel'; +import { MinimapLinesRenderingData, ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter, ViewEventsCollector, IOverviewRulerDecorations } from 'vs/editor/common/viewModel/viewModel'; import { SplitLinesCollection, IViewModelLinesCollection, IdentityLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { MinimapTokensColorTracker } from 'vs/editor/common/view/minimapCharRenderer'; @@ -22,6 +22,8 @@ import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewMod import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ITheme } from 'vs/platform/theme/common/themeService'; +import { ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -80,7 +82,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this._isDisposing = false; this._centeredViewLine = -1; - this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.coordinatesConverter); + this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.lines, this.coordinatesConverter); this._register(this.model.addBulkListener((events: EmitterEvent[]) => { if (this._isDisposing) { @@ -282,8 +284,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel break; } case textModelEvents.TextModelEventType.ModelDecorationsChanged: { - const e = data; - this.decorations.onModelDecorationsChanged(e); + this.decorations.onModelDecorationsChanged(); eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); break; } @@ -429,8 +430,17 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - public getAllOverviewRulerDecorations(): ViewModelDecoration[] { - return this.decorations.getAllOverviewRulerDecorations(); + public getAllOverviewRulerDecorations(theme: ITheme): IOverviewRulerDecorations { + return this.lines.getAllOverviewRulerDecorations(this.editorId, this.configuration.editor.readOnly, theme); + } + + public invalidateOverviewRulerColorCache(): void { + const decorations = this.model.getOverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.options.overviewRuler; + opts._resolvedColor = null; + } } public getValueInRange(range: Range, eol: editorCommon.EndOfLinePreference): string { diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 726c2451eeff8..7cb6a00b5f0a7 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -48,7 +48,7 @@ const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Repla const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); -const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first 999 results are highlighted, but all find operations work on the entire text."); +const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first {0} results are highlighted, but all find operations work on the entire text.", MATCHES_LIMIT); const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}"); const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results"); diff --git a/src/vs/editor/contrib/find/common/findController.ts b/src/vs/editor/contrib/find/common/findController.ts index 09536d1f5f82e..43ea3992e6854 100644 --- a/src/vs/editor/contrib/find/common/findController.ts +++ b/src/vs/editor/contrib/find/common/findController.ts @@ -1038,6 +1038,23 @@ export class SelectionHighlighter extends Disposable implements editorCommon.IEd } } + // Return early if the find widget shows the exact same matches + if (findState.isRevealed) { + let findStateSearchString = findState.searchString; + if (!caseSensitive) { + findStateSearchString = findStateSearchString.toLowerCase(); + } + + let mySearchString = r.searchText; + if (!caseSensitive) { + mySearchString = mySearchString.toLowerCase(); + } + + if (findStateSearchString === mySearchString && r.matchCase === findState.matchCase && r.wholeWord === findState.wholeWord && !findState.isRegex) { + return null; + } + } + return new SelectionHighlighterState(lastWordUnderCursor, r.searchText, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null); } diff --git a/src/vs/editor/contrib/find/common/findDecorations.ts b/src/vs/editor/contrib/find/common/findDecorations.ts index 202e32602faca..09cb7eb07d06f 100644 --- a/src/vs/editor/contrib/find/common/findDecorations.ts +++ b/src/vs/editor/contrib/find/common/findDecorations.ts @@ -16,6 +16,7 @@ export class FindDecorations implements IDisposable { private _editor: editorCommon.ICommonCodeEditor; private _decorations: string[]; + private _overviewRulerApproximateDecorations: string[]; private _findScopeDecorationId: string; private _rangeHighlightDecorationId: string; private _highlightedDecorationId: string; @@ -24,6 +25,7 @@ export class FindDecorations implements IDisposable { constructor(editor: editorCommon.ICommonCodeEditor) { this._editor = editor; this._decorations = []; + this._overviewRulerApproximateDecorations = []; this._findScopeDecorationId = null; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; @@ -35,6 +37,7 @@ export class FindDecorations implements IDisposable { this._editor = null; this._decorations = []; + this._overviewRulerApproximateDecorations = []; this._findScopeDecorationId = null; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; @@ -43,6 +46,7 @@ export class FindDecorations implements IDisposable { public reset(): void { this._decorations = []; + this._overviewRulerApproximateDecorations = []; this._findScopeDecorationId = null; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; @@ -68,11 +72,21 @@ export class FindDecorations implements IDisposable { this.setCurrentFindMatch(null); } + private _getDecorationIndex(decorationId: string): number { + const index = this._decorations.indexOf(decorationId); + if (index >= 0) { + return index + 1; + } + return 1; + } + public getCurrentMatchesPosition(desiredRange: Range): number { - for (let i = 0, len = this._decorations.length; i < len; i++) { - let range = this._editor.getModel().getDecorationRange(this._decorations[i]); - if (desiredRange.equalsRange(range)) { - return (i + 1); + let candidates = this._editor.getModel().getDecorationsInRange(desiredRange); + for (let i = 0, len = candidates.length; i < len; i++) { + const candidate = candidates[i]; + const candidateOpts = candidate.options; + if (candidateOpts === FindDecorations._FIND_MATCH_DECORATION || candidateOpts === FindDecorations._CURRENT_FIND_MATCH_DECORATION) { + return this._getDecorationIndex(candidate.id); } } return 1; @@ -95,12 +109,12 @@ export class FindDecorations implements IDisposable { if (this._highlightedDecorationId !== null || newCurrentDecorationId !== null) { this._editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => { if (this._highlightedDecorationId !== null) { - changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(false)); + changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations._FIND_MATCH_DECORATION); this._highlightedDecorationId = null; } if (newCurrentDecorationId !== null) { this._highlightedDecorationId = newCurrentDecorationId; - changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(true)); + changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations._CURRENT_FIND_MATCH_DECORATION); } if (this._rangeHighlightDecorationId !== null) { changeAccessor.removeDecoration(this._rangeHighlightDecorationId); @@ -121,34 +135,82 @@ export class FindDecorations implements IDisposable { return matchPosition; } - public set(matches: Range[], findScope: Range): void { - let newDecorations: editorCommon.IModelDeltaDecoration[] = matches.map((match) => { - return { - range: match, - options: FindDecorations.createFindMatchDecorationOptions(false) - }; - }); - if (findScope) { - newDecorations.unshift({ - range: findScope, - options: FindDecorations._FIND_SCOPE_DECORATION - }); - } - let tmpDecorations = this._editor.deltaDecorations(this._allDecorations(), newDecorations); + public set(findMatches: editorCommon.FindMatch[], findScope: Range): void { + this._editor.changeDecorations((accessor) => { - if (findScope) { - this._findScopeDecorationId = tmpDecorations.shift(); - } else { - this._findScopeDecorationId = null; - } - this._decorations = tmpDecorations; - this._rangeHighlightDecorationId = null; - this._highlightedDecorationId = null; + let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; + let newOverviewRulerApproximateDecorations: editorCommon.IModelDeltaDecoration[] = []; + + if (findMatches.length > 1000) { + // we go into a mode where the overview ruler gets "approximate" decorations + // the reason is that the overview ruler paints all the decorations in the file and we don't want to cause freezes + findMatchesOptions = FindDecorations._FIND_MATCH_NO_OVERVIEW_DECORATION; + + // approximate a distance in lines where matches should be merged + const lineCount = this._editor.getModel().getLineCount(); + const height = this._editor.getLayoutInfo().height; + const approxPixelsPerLine = height / lineCount; + const mergeLinesDelta = Math.max(2, Math.ceil(3 / approxPixelsPerLine)); + + // merge decorations as much as possible + let prevStartLineNumber = findMatches[0].range.startLineNumber; + let prevEndLineNumber = findMatches[0].range.endLineNumber; + for (let i = 1, len = findMatches.length; i < len; i++) { + const range = findMatches[i].range; + if (prevEndLineNumber + mergeLinesDelta >= range.startLineNumber) { + if (range.endLineNumber > prevEndLineNumber) { + prevEndLineNumber = range.endLineNumber; + } + } else { + newOverviewRulerApproximateDecorations.push({ + range: new Range(prevStartLineNumber, 1, prevEndLineNumber, 1), + options: FindDecorations._FIND_MATCH_ONLY_OVERVIEW_DECORATION + }); + prevStartLineNumber = range.startLineNumber; + prevEndLineNumber = range.endLineNumber; + } + } + + newOverviewRulerApproximateDecorations.push({ + range: new Range(prevStartLineNumber, 1, prevEndLineNumber, 1), + options: FindDecorations._FIND_MATCH_ONLY_OVERVIEW_DECORATION + }); + } + + // Find matches + let newFindMatchesDecorations: editorCommon.IModelDeltaDecoration[] = new Array(findMatches.length); + for (let i = 0, len = findMatches.length; i < len; i++) { + newFindMatchesDecorations[i] = { + range: findMatches[i].range, + options: findMatchesOptions + }; + } + this._decorations = accessor.deltaDecorations(this._decorations, newFindMatchesDecorations); + + // Overview ruler approximate decorations + this._overviewRulerApproximateDecorations = accessor.deltaDecorations(this._overviewRulerApproximateDecorations, newOverviewRulerApproximateDecorations); + + // Range highlight + if (this._rangeHighlightDecorationId) { + accessor.removeDecoration(this._rangeHighlightDecorationId); + this._rangeHighlightDecorationId = null; + } + + // Find scope + if (this._findScopeDecorationId) { + accessor.removeDecoration(this._findScopeDecorationId); + this._findScopeDecorationId = null; + } + if (findScope) { + this._findScopeDecorationId = accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION); + } + }); } private _allDecorations(): string[] { let result: string[] = []; result = result.concat(this._decorations); + result = result.concat(this._overviewRulerApproximateDecorations); if (this._findScopeDecorationId) { result.push(this._findScopeDecorationId); } @@ -158,10 +220,6 @@ export class FindDecorations implements IDisposable { return result; } - private static createFindMatchDecorationOptions(isCurrent: boolean): ModelDecorationOptions { - return (isCurrent ? this._CURRENT_FIND_MATCH_DECORATION : this._FIND_MATCH_DECORATION); - } - private static _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'currentFindMatch', @@ -184,6 +242,21 @@ export class FindDecorations implements IDisposable { } }); + private static _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'findMatch', + showIfCollapsed: true + }); + + private static _FIND_MATCH_ONLY_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + overviewRuler: { + color: themeColorFromId(editorFindMatchHighlight), + darkColor: themeColorFromId(editorFindMatchHighlight), + position: editorCommon.OverviewRulerLane.Center + } + }); + private static _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index be3705b47f72e..dda6ca2f533bd 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -67,7 +67,7 @@ export const FIND_IDS = { ShowNextFindTermAction: 'find.history.showNext' }; -export const MATCHES_LIMIT = 999; +export const MATCHES_LIMIT = 19999; export class FindModelBoundToEditorModel { @@ -171,7 +171,7 @@ export class FindModelBoundToEditorModel { } let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT); - this._decorations.set(findMatches.map(match => match.range), findScope); + this._decorations.set(findMatches, findScope); this._state.changeMatchInfo( this._decorations.getCurrentMatchesPosition(this._editor.getSelection()), diff --git a/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts index ec19e7bbe0dc5..58aa224ff6cd9 100644 --- a/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts @@ -77,7 +77,7 @@ suite('Editor Contrib - Sort Lines Command', () => { 'third line', 'fifth' ], - new Selection(3, 3, 4, 2) + new Selection(3, 3, 4, 1) ); }); @@ -119,7 +119,7 @@ suite('Editor Contrib - Sort Lines Command', () => { 'second line', 'third line', ], - new Selection(1, 1, 5, 6) + new Selection(1, 1, 5, 11) ); }); diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts index 0d299ca85af65..ce7638670eb62 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts @@ -38,7 +38,6 @@ import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/r import { registerColor, activeContrastBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { attachListStyler, attachBadgeStyler } from 'vs/platform/theme/common/styler'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; @@ -81,7 +80,7 @@ class DecorationsManager implements IDisposable { } private _addDecorations(reference: FileReferences): void { - this._callOnModelChange.push(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged(event))); + this._callOnModelChange.push(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged())); this._editor.changeDecorations(accessor => { @@ -107,21 +106,20 @@ class DecorationsManager implements IDisposable { }); } - private _onDecorationChanged(event: IModelDecorationsChangedEvent): void { - const changedDecorations = event.changedDecorations, - toRemove: string[] = []; + private _onDecorationChanged(): void { + const toRemove: string[] = []; - for (let i = 0, len = changedDecorations.length; i < len; i++) { - let reference = this._decorations.get(changedDecorations[i]); - if (!reference) { - continue; + this._decorations.forEach((reference, decorationId) => { + const newRange = this._editor.getModel().getDecorationRange(decorationId); + + if (!newRange) { + return; } - const newRange = this._editor.getModel().getDecorationRange(changedDecorations[i]); let ignore = false; if (Range.equalsRange(newRange, reference.range)) { - continue; + return; } else if (Range.spansMultipleLines(newRange)) { ignore = true; @@ -137,11 +135,11 @@ class DecorationsManager implements IDisposable { if (ignore) { this._decorationIgnoreSet.add(reference.id); - toRemove.push(changedDecorations[i]); + toRemove.push(decorationId); } else { reference.range = newRange; } - } + }); this._editor.changeDecorations((accessor) => { for (let i = 0, len = toRemove.length; i < len; i++) { diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index 2bb0c11006dff..05d96ea0b304e 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -234,9 +234,9 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); session.prev(); - assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + assertSelections(editor, new Selection(1, 7, 1, 10), new Selection(2, 11, 2, 14)); session.prev(); - assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); session.prev(); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); }); diff --git a/src/vs/editor/test/common/commands/sideEditing.test.ts b/src/vs/editor/test/common/commands/sideEditing.test.ts index 08fe4e0c5e747..ceaa7df8265a5 100644 --- a/src/vs/editor/test/common/commands/sideEditing.test.ts +++ b/src/vs/editor/test/common/commands/sideEditing.test.ts @@ -10,10 +10,11 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; -import { ILineEdit, ModelLine, LineMarker, MarkersTracker } from 'vs/editor/common/model/modelLine'; import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; - -const NO_TAB_SIZE = 0; +import { Model } from 'vs/editor/common/model/model'; +import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { Cursor } from 'vs/editor/common/controller/cursor'; function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void { withMockCodeEditor(lines, {}, (editor, cursor) => { @@ -31,15 +32,6 @@ function testCommand(lines: string[], selections: Selection[], edits: IIdentifie }); } -function testLineEditMarker(text: string, column: number, stickToPreviousCharacter: boolean, edit: ILineEdit, expectedColumn: number): void { - var line = new ModelLine(text, NO_TAB_SIZE); - line.addMarker(new LineMarker('1', 0, new Position(0, column), stickToPreviousCharacter)); - - line.applyEdits(new MarkersTracker(), [edit], NO_TAB_SIZE); - - assert.equal(line.getMarkers()[0].position.column, expectedColumn); -} - suite('Editor Side Editing - collapsed selection', () => { test('replace at selection', () => { @@ -86,14 +78,6 @@ suite('Editor Side Editing - collapsed selection', () => { ); }); - test('ModelLine.applyEdits uses `isReplace`', () => { - testLineEditMarker('something', 1, true, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: false }, 1); - testLineEditMarker('something', 1, true, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: true }, 4); - - testLineEditMarker('something', 1, false, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: false }, 4); - testLineEditMarker('something', 1, false, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: true }, 4); - }); - test('insert at selection', () => { testCommand( [ @@ -204,5 +188,699 @@ suite('Editor Side Editing - collapsed selection', () => { [new Selection(1, 1, 1, 1), new Selection(1, 3, 1, 3)] ); }); +}); + +suite('SideEditing', () => { + + const LINES = [ + 'My First Line', + 'My Second Line', + 'Third Line' + ]; + + function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void { + const model = Model.createFromString(LINES.join('\n')); + const config = new TestConfiguration(null); + const viewModel = new ViewModel(0, config, model, null); + const cursor = new Cursor(config, model, viewModel); + + cursor.setSelections('tests', [selection]); + model.applyEdits([{ range: editRange, text: editText, forceMoveMarkers: editForceMoveMarkers, identifier: null }]); + const actual = cursor.getSelection(); + assert.deepEqual(actual.toString(), expected.toString(), msg); + + cursor.dispose(); + viewModel.dispose(); + config.dispose(); + model.dispose(); + } + + function runTest(selection: Range, editRange: Range, editText: string, expected: Selection[][]): void { + const sel1 = new Selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn); + _runTest(sel1, editRange, editText, false, expected[0][0], '0-0-regular-no-force'); + _runTest(sel1, editRange, editText, true, expected[1][0], '1-0-regular-force'); + + // RTL selection + const sel2 = new Selection(selection.endLineNumber, selection.endColumn, selection.startLineNumber, selection.startColumn); + _runTest(sel2, editRange, editText, false, expected[0][1], '0-1-inverse-no-force'); + _runTest(sel2, editRange, editText, true, expected[1][1], '1-1-inverse-force'); + } + + suite('insert', () => { + suite('collapsed sel', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 3), 'xx', + [ + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('equal', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 4), 'xx', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 4, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 5), 'xx', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 3), 'xx', + [ + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 4), 'xx', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('inside', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 5), 'xx', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + ] + ); + }); + test('end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 9), 'xx', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 10), 'xx', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); + + suite('delete', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), '', + [ + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), '', + [ + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), '', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), '', + [ + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), '', + [ + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), '', + [ + [new Selection(1, 3, 1, 7), new Selection(1, 7, 1, 3)], + [new Selection(1, 3, 1, 7), new Selection(1, 7, 1, 3)], + ] + ); + }); + + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), '', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), '', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), '', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + ] + ); + }); + + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), '', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), '', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), '', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + ] + ); + }); + + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), '', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), '', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); + + suite('replace short', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'c', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'c', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 5, 1, 5), new Selection(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'c', + [ + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'c', + [ + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'c', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'c', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 5, 1, 8), new Selection(1, 8, 1, 5)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'c', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 5, 1, 5), new Selection(1, 5, 1, 5)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'c', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 5, 1, 5), new Selection(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'c', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'c', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'c', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'c', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 10), new Selection(1, 10, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'c', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); + suite('replace long', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 4, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 7, 1, 7), new Selection(1, 7, 1, 7)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 8, 1, 8), new Selection(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 7, 1, 11), new Selection(1, 11, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'cccc', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 7, 1, 7), new Selection(1, 7, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'cccc', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 7, 1, 7), new Selection(1, 7, 1, 7)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 8, 1, 11), new Selection(1, 11, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'cccc', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 8, 1, 8), new Selection(1, 8, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'cccc', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 8, 1, 8), new Selection(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 13), new Selection(1, 13, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); }); \ No newline at end of file diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index e21b52a667a5a..8b8a9bd46c46a 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -1651,7 +1651,8 @@ suite('Editor Controller - Regression tests', () => { }); }); - test('issue #23913: Greater than 1000+ multi cursor typing replacement text appears inverted, lines begin to drop off selection', () => { + test('issue #23913: Greater than 1000+ multi cursor typing replacement text appears inverted, lines begin to drop off selection', function () { + this.timeout(10000); const LINE_CNT = 2000; let text = []; diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 6166f3eb8e568..d04e5269cadea 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; -import { EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; +import { EndOfLineSequence, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; import { EditableTextModel, IValidatedEditOperation } from 'vs/editor/common/model/editableTextModel'; import { MirrorModel } from 'vs/editor/common/model/mirrorModel'; import { assertSyncedModels, testApplyEditsWithSyncedModels } from 'vs/editor/test/common/model/editableTextModelTestUtils'; @@ -15,12 +15,13 @@ import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvent suite('EditorModel - EditableTextModel._getInverseEdits', () => { - function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeLength: number, text: string[]): IValidatedEditOperation { + function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): IValidatedEditOperation { return { sortIndex: 0, identifier: null, range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), - rangeLength: rangeLength, + rangeOffset: 0, + rangeLength: 0, lines: text, forceMoveMarkers: false, isAutoWhitespaceEdit: false @@ -39,7 +40,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('single insert', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']) + editOp(1, 1, 1, 1, ['hello']) ], [ inverseEditOp(1, 1, 1, 6) @@ -50,8 +51,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('Bug 19872: Undo is funky', () => { assertInverseEdits( [ - editOp(2, 1, 2, 2, 0, ['']), - editOp(3, 1, 4, 2, 0, ['']) + editOp(2, 1, 2, 2, ['']), + editOp(3, 1, 4, 2, ['']) ], [ inverseEditOp(2, 1, 2, 1), @@ -63,8 +64,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single unrelated inserts', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']), - editOp(2, 1, 2, 1, 0, ['world']) + editOp(1, 1, 1, 1, ['hello']), + editOp(2, 1, 2, 1, ['world']) ], [ inverseEditOp(1, 1, 1, 6), @@ -76,8 +77,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single inserts 1', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']), - editOp(1, 2, 1, 2, 0, ['world']) + editOp(1, 1, 1, 1, ['hello']), + editOp(1, 2, 1, 2, ['world']) ], [ inverseEditOp(1, 1, 1, 6), @@ -89,8 +90,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single inserts 2', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']), - editOp(1, 4, 1, 4, 0, ['world']) + editOp(1, 1, 1, 1, ['hello']), + editOp(1, 4, 1, 4, ['world']) ], [ inverseEditOp(1, 1, 1, 6), @@ -102,7 +103,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('multiline insert', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello', 'world']) + editOp(1, 1, 1, 1, ['hello', 'world']) ], [ inverseEditOp(1, 1, 2, 6) @@ -113,8 +114,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two unrelated multiline inserts', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello', 'world']), - editOp(2, 1, 2, 1, 0, ['how', 'are', 'you?']), + editOp(1, 1, 1, 1, ['hello', 'world']), + editOp(2, 1, 2, 1, ['how', 'are', 'you?']), ], [ inverseEditOp(1, 1, 2, 6), @@ -126,8 +127,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two multiline inserts 1', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello', 'world']), - editOp(1, 2, 1, 2, 0, ['how', 'are', 'you?']), + editOp(1, 1, 1, 1, ['hello', 'world']), + editOp(1, 2, 1, 2, ['how', 'are', 'you?']), ], [ inverseEditOp(1, 1, 2, 6), @@ -139,7 +140,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('single delete', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null) + editOp(1, 1, 1, 6, null) ], [ inverseEditOp(1, 1, 1, 1) @@ -150,8 +151,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single unrelated deletes', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null), - editOp(2, 1, 2, 6, 0, null) + editOp(1, 1, 1, 6, null), + editOp(2, 1, 2, 6, null) ], [ inverseEditOp(1, 1, 1, 1), @@ -163,8 +164,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single deletes 1', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null), - editOp(1, 7, 1, 12, 0, null) + editOp(1, 1, 1, 6, null), + editOp(1, 7, 1, 12, null) ], [ inverseEditOp(1, 1, 1, 1), @@ -176,8 +177,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single deletes 2', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null), - editOp(1, 9, 1, 14, 0, null) + editOp(1, 1, 1, 6, null), + editOp(1, 9, 1, 14, null) ], [ inverseEditOp(1, 1, 1, 1), @@ -189,7 +190,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('multiline delete', () => { assertInverseEdits( [ - editOp(1, 1, 2, 6, 0, null) + editOp(1, 1, 2, 6, null) ], [ inverseEditOp(1, 1, 1, 1) @@ -200,8 +201,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two unrelated multiline deletes', () => { assertInverseEdits( [ - editOp(1, 1, 2, 6, 0, null), - editOp(3, 1, 5, 5, 0, null), + editOp(1, 1, 2, 6, null), + editOp(3, 1, 5, 5, null), ], [ inverseEditOp(1, 1, 1, 1), @@ -213,8 +214,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two multiline deletes 1', () => { assertInverseEdits( [ - editOp(1, 1, 2, 6, 0, null), - editOp(2, 7, 4, 5, 0, null), + editOp(1, 1, 2, 6, null), + editOp(2, 7, 4, 5, null), ], [ inverseEditOp(1, 1, 1, 1), @@ -226,7 +227,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('single replace', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, ['Hello world']) + editOp(1, 1, 1, 6, ['Hello world']) ], [ inverseEditOp(1, 1, 1, 12) @@ -237,8 +238,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two replaces', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, ['Hello world']), - editOp(1, 7, 1, 8, 0, ['How are you?']), + editOp(1, 1, 1, 6, ['Hello world']), + editOp(1, 7, 1, 8, ['How are you?']), ], [ inverseEditOp(1, 1, 1, 12), @@ -250,9 +251,9 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('many edits', () => { assertInverseEdits( [ - editOp(1, 2, 1, 2, 0, ['', ' ']), - editOp(1, 5, 1, 6, 0, ['']), - editOp(1, 9, 1, 9, 0, ['', '']) + editOp(1, 2, 1, 2, ['', ' ']), + editOp(1, 5, 1, 6, ['']), + editOp(1, 9, 1, 9, ['', '']) ], [ inverseEditOp(1, 2, 2, 3), @@ -265,11 +266,12 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { - function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeLength: number, text: string[]): IValidatedEditOperation { + function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeOffset: number, rangeLength: number, text: string[]): IValidatedEditOperation { return { sortIndex: 0, identifier: null, range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), + rangeOffset: rangeOffset, rangeLength: rangeLength, lines: text, forceMoveMarkers: false, @@ -297,9 +299,9 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '1' ], [ - editOp(1, 3, 1, 3, 0, [' new line', 'No longer']) + editOp(1, 3, 1, 3, 2, 0, [' new line', 'No longer']) ], - editOp(1, 3, 1, 3, 0, [' new line', 'No longer']) + editOp(1, 3, 1, 3, 2, 0, [' new line', 'No longer']) ); }); @@ -311,11 +313,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '', '1' ], [ - editOp(1, 1, 1, 3, 0, ['Your']), - editOp(1, 4, 1, 4, 0, ['Interesting ']), - editOp(2, 3, 2, 6, 0, null) + editOp(1, 1, 1, 3, 0, 2, ['Your']), + editOp(1, 4, 1, 4, 3, 0, ['Interesting ']), + editOp(2, 3, 2, 6, 16, 3, null) ], - editOp(1, 1, 2, 6, 19, [ + editOp(1, 1, 2, 6, 0, 19, [ 'Your Interesting First Line', '\t\t' ])); @@ -331,10 +333,10 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '1' ], [ - editOp(1, 3, 1, 3, 0, ['', '', '', '', '']), - editOp(3, 15, 3, 15, 0, ['a', 'b']) + editOp(1, 3, 1, 3, 2, 0, ['', '', '', '', '']), + editOp(3, 15, 3, 15, 45, 0, ['a', 'b']) ], - editOp(1, 3, 3, 15, 43, [ + editOp(1, 3, 3, 15, 2, 43, [ '', '', '', @@ -357,9 +359,9 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '1' ], [ - editOp(1, 1, 1, 1, 0, ['']) + editOp(1, 1, 1, 1, 0, 0, ['']) ], - editOp(1, 1, 1, 1, 0, ['']) + editOp(1, 1, 1, 1, 0, 0, ['']) ); }); @@ -373,10 +375,10 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '123' ], [ - editOp(2, 1, 2, 3, 0, ['\t']), - editOp(3, 1, 3, 5, 0, ['']) + editOp(2, 1, 2, 3, 14, 2, ['\t']), + editOp(3, 1, 3, 5, 31, 4, ['']) ], - editOp(2, 1, 3, 5, 21, ['\tMy Second Line', '']) + editOp(2, 1, 3, 5, 14, 21, ['\tMy Second Line', '']) ); }); @@ -386,11 +388,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '{"x" : 1}' ], [ - editOp(1, 2, 1, 2, 0, ['\n ']), - editOp(1, 5, 1, 6, 0, ['']), - editOp(1, 9, 1, 9, 0, ['\n']) + editOp(1, 2, 1, 2, 1, 0, ['\n ']), + editOp(1, 5, 1, 6, 4, 1, ['']), + editOp(1, 9, 1, 9, 8, 0, ['\n']) ], - editOp(1, 2, 1, 9, 7, [ + editOp(1, 2, 1, 9, 1, 7, [ '', ' "x": 1', '' @@ -406,11 +408,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '}' ], [ - editOp(1, 2, 2, 3, 0, ['']), - editOp(2, 6, 2, 6, 0, [' ']), - editOp(2, 9, 3, 1, 0, ['']) + editOp(1, 2, 2, 3, 1, 3, ['']), + editOp(2, 6, 2, 6, 7, 0, [' ']), + editOp(2, 9, 3, 1, 10, 1, ['']) ], - editOp(1, 2, 3, 1, 10, ['"x" : 1']) + editOp(1, 2, 3, 1, 1, 10, ['"x" : 1']) ); }); @@ -424,10 +426,10 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '}' ], [ - editOp(1, 2, 2, 1, 0, ['', '\t']), - editOp(2, 11, 4, 1, 0, ['', '\t']) + editOp(1, 2, 2, 1, 1, 1, ['', '\t']), + editOp(2, 11, 4, 1, 12, 2, ['', '\t']) ], - editOp(1, 2, 4, 1, 13, [ + editOp(1, 2, 4, 1, 1, 13, [ '', '\t"a": true,', '\t' @@ -446,12 +448,12 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { 'and the last line' ], [ - editOp(1, 5, 3, 1, 0, [' text', 'some more text', 'some more text']), - editOp(3, 2, 4, 1, 0, ['o more lines', 'asd', 'asd', 'asd']), - editOp(5, 1, 5, 6, 0, ['zzzzzzzz']), - editOp(5, 11, 6, 16, 0, ['1', '2', '3', '4']) + editOp(1, 5, 3, 1, 4, 21, [' text', 'some more text', 'some more text']), + editOp(3, 2, 4, 1, 26, 23, ['o more lines', 'asd', 'asd', 'asd']), + editOp(5, 1, 5, 6, 50, 5, ['zzzzzzzz']), + editOp(5, 11, 6, 16, 60, 22, ['1', '2', '3', '4']) ], - editOp(1, 5, 6, 16, 78, [ + editOp(1, 5, 6, 16, 4, 78, [ ' text', 'some more text', 'some more textno more lines', @@ -475,17 +477,17 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { ' ,"e": /*comment*/ [null] }', ], [ - editOp(1, 1, 1, 2, 0, ['']), - editOp(1, 3, 1, 10, 0, ['', ' ']), - editOp(1, 16, 2, 14, 0, ['', ' ']), - editOp(2, 18, 3, 9, 0, ['', ' ']), - editOp(3, 22, 4, 9, 0, ['']), - editOp(4, 10, 4, 10, 0, ['', ' ']), - editOp(4, 28, 4, 28, 0, ['', ' ']), - editOp(4, 32, 4, 32, 0, ['', ' ']), - editOp(4, 33, 4, 34, 0, ['', '']) + editOp(1, 1, 1, 2, 0, 1, ['']), + editOp(1, 3, 1, 10, 2, 7, ['', ' ']), + editOp(1, 16, 2, 14, 15, 14, ['', ' ']), + editOp(2, 18, 3, 9, 33, 9, ['', ' ']), + editOp(3, 22, 4, 9, 55, 9, ['']), + editOp(4, 10, 4, 10, 65, 0, ['', ' ']), + editOp(4, 28, 4, 28, 83, 0, ['', ' ']), + editOp(4, 32, 4, 32, 87, 0, ['', ' ']), + editOp(4, 33, 4, 34, 88, 1, ['', '']) ], - editOp(1, 1, 4, 34, 89, [ + editOp(1, 1, 4, 34, 0, 89, [ '{', ' "d": [', ' null', @@ -505,11 +507,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { ' ,def' ], [ - editOp(1, 1, 1, 4, 0, ['']), - editOp(1, 7, 2, 2, 0, ['']), - editOp(2, 3, 2, 3, 0, ['', '']) + editOp(1, 1, 1, 4, 0, 3, ['']), + editOp(1, 7, 2, 2, 6, 2, ['']), + editOp(2, 3, 2, 3, 9, 0, ['', '']) ], - editOp(1, 1, 2, 3, 9, [ + editOp(1, 1, 2, 3, 0, 9, [ 'abc,', '' ]) @@ -1567,7 +1569,6 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { }); let assertMirrorModels = () => { - model._assertLineNumbersOK(); assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; @@ -1579,257 +1580,3 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { mirrorModel2.dispose(); }); }); - -interface ILightWeightMarker { - id: string; - lineNumber: number; - column: number; - stickToPreviousCharacter: boolean; -} - -suite('EditorModel - EditableTextModel.applyEdits & markers', () => { - - function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): IIdentifiedSingleEditOperation { - return { - identifier: null, - range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), - text: text.join('\n'), - forceMoveMarkers: false - }; - } - - function marker(id: string, lineNumber: number, column: number, stickToPreviousCharacter: boolean): ILightWeightMarker { - return { - id: id, - lineNumber: lineNumber, - column: column, - stickToPreviousCharacter: stickToPreviousCharacter - }; - } - - function toMarkersMap(markers: ILightWeightMarker[]): { [markerId: string]: ILightWeightMarker } { - var result: { [markerId: string]: ILightWeightMarker } = {}; - markers.forEach(m => { - result[m.id] = m; - }); - return result; - } - - function testApplyEditsAndMarkers(text: string[], markers: ILightWeightMarker[], edits: IIdentifiedSingleEditOperation[], changedMarkers: string[], expectedText: string[], expectedMarkers: ILightWeightMarker[]): void { - var textStr = text.join('\n'); - var expectedTextStr = expectedText.join('\n'); - var markersMap = toMarkersMap(markers); - // var expectedMarkersMap = toMarkersMap(expectedMarkers); - var markerId2ModelMarkerId = Object.create(null); - - var model = EditableTextModel.createFromString(textStr); - model.setEOL(EndOfLineSequence.LF); - - // Add markers - markers.forEach((m) => { - let modelMarkerId = model._addMarker(0, m.lineNumber, m.column, m.stickToPreviousCharacter); - markerId2ModelMarkerId[m.id] = modelMarkerId; - }); - - // Apply edits & collect inverse edits - model.applyEdits(edits); - model._assertLineNumbersOK(); - - // Assert edits produced expected result - assert.deepEqual(model.getValue(EndOfLinePreference.LF), expectedTextStr); - - let actualChangedMarkers: string[] = []; - for (let i = 0, len = expectedMarkers.length; i < len; i++) { - let expectedMarker = expectedMarkers[i]; - let initialMarker = markersMap[expectedMarker.id]; - let expectedMarkerModelMarkerId = markerId2ModelMarkerId[expectedMarker.id]; - let actualMarker = model._getMarker(expectedMarkerModelMarkerId); - - if (actualMarker.lineNumber !== initialMarker.lineNumber || actualMarker.column !== initialMarker.column) { - actualChangedMarkers.push(initialMarker.id); - } - - assert.equal(actualMarker.lineNumber, expectedMarker.lineNumber, 'marker lineNumber of marker ' + expectedMarker.id); - assert.equal(actualMarker.column, expectedMarker.column, 'marker column of marker ' + expectedMarker.id); - } - - changedMarkers.sort(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, changedMarkers, 'changed markers'); - - model.dispose(); - } - - test('no markers changed', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ], - [ - editOp(1, 13, 1, 13, [' how are you?']) - ], - [], - [ - 'Hello world, how are you?', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ] - ); - }); - - test('first line changes', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ], - [ - editOp(1, 7, 1, 12, ['friends']) - ], - [], - [ - 'Hello friends,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ] - ); - }); - - test('inserting lines', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ], - [ - editOp(1, 7, 1, 12, ['friends']), - editOp(1, 13, 1, 13, ['', 'this is an inserted line', 'and another one. By the way,']) - ], - ['e', 'f', 'g', 'h'], - [ - 'Hello friends,', - 'this is an inserted line', - 'and another one. By the way,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 4, 1, false), - marker('f', 4, 16, true), - marker('g', 4, 21, true), - marker('h', 5, 24, false) - ] - ); - }); - - test('replacing a lot', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing', - 'more lines...', - 'more lines...', - 'more lines...', - 'more lines...' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false), - marker('i', 5, 1, false), - marker('j', 6, 1, false), - marker('k', 7, 14, false), - ], - [ - editOp(1, 7, 1, 12, ['friends']), - editOp(1, 13, 1, 13, ['', 'this is an inserted line', 'and another one. By the way,', 'This is another line']), - editOp(2, 1, 7, 14, ['Some new text here']) - ], - ['e', 'f', 'g', 'h', 'i', 'j', 'k'], - [ - 'Hello friends,', - 'this is an inserted line', - 'and another one. By the way,', - 'This is another line', - 'Some new text here' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 5, 1, false), - marker('f', 5, 16, true), - marker('g', 5, 19, true), - marker('h', 5, 19, false), - marker('i', 5, 19, false), - marker('j', 5, 19, false), - marker('k', 5, 19, false), - ] - ); - }); -}); diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index a901d29fcd326..b0b18723062fb 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -105,7 +105,6 @@ export function assertSyncedModels(text: string, callback: (model: EditableTextM var assertMirrorModels = () => { assertLineMapping(model, 'model'); - model._assertLineNumbersOK(); assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts new file mode 100644 index 0000000000000..29a5978432e78 --- /dev/null +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -0,0 +1,555 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { IntervalTree, IntervalNode } from 'vs/editor/common/model/intervalTree'; + +const GENERATE_TESTS = false; +let TEST_COUNT = GENERATE_TESTS ? 10000 : 0; +let PRINT_TREE = false; +const MIN_INTERVAL_START = 1; +const MAX_INTERVAL_END = 100; +const MIN_INSERTS = 1; +const MAX_INSERTS = 30; +const MIN_CHANGE_CNT = 10; +const MAX_CHANGE_CNT = 20; + +suite('IntervalTree', () => { + + class Interval { + _intervalBrand: void; + + public start: number; + public end: number; + + constructor(start: number, end: number) { + this.start = start; + this.end = end; + } + } + + class Oracle { + public intervals: Interval[]; + + constructor() { + this.intervals = []; + } + + public insert(interval: Interval): Interval { + this.intervals.push(interval); + this.intervals.sort((a, b) => { + if (a.start === b.start) { + return a.end - b.end; + } + return a.start - b.start; + }); + return interval; + } + + public delete(interval: Interval): void { + for (let i = 0, len = this.intervals.length; i < len; i++) { + if (this.intervals[i] === interval) { + this.intervals.splice(i, 1); + return; + } + } + } + + public search(interval: Interval): Interval[] { + let result: Interval[] = []; + for (let i = 0, len = this.intervals.length; i < len; i++) { + let int = this.intervals[i]; + if (int.start <= interval.end && int.end >= interval.start) { + result.push(int); + } + } + return result; + } + } + + class TestState { + private _oracle: Oracle = new Oracle(); + private _tree: IntervalTree = new IntervalTree(); + private _lastNodeId = -1; + private _treeNodes: IntervalNode[] = []; + private _oracleNodes: Interval[] = []; + + public acceptOp(op: IOperation): void { + + if (op.type === 'insert') { + if (PRINT_TREE) { + console.log(`insert: {${JSON.stringify(new Interval(op.begin, op.end))}}`); + } + let nodeId = (++this._lastNodeId); + this._treeNodes[nodeId] = new IntervalNode(null, op.begin, op.end); + this._tree.insert(this._treeNodes[nodeId]); + this._oracleNodes[nodeId] = this._oracle.insert(new Interval(op.begin, op.end)); + } else if (op.type === 'delete') { + if (PRINT_TREE) { + console.log(`delete: {${JSON.stringify(this._oracleNodes[op.id])}}`); + } + this._tree.delete(this._treeNodes[op.id]); + this._oracle.delete(this._oracleNodes[op.id]); + + this._treeNodes[op.id] = null; + this._oracleNodes[op.id] = null; + } else if (op.type === 'change') { + + this._tree.delete(this._treeNodes[op.id]); + this._treeNodes[op.id].reset(0, op.begin, op.end, null); + this._tree.insert(this._treeNodes[op.id]); + + this._oracle.delete(this._oracleNodes[op.id]); + this._oracleNodes[op.id].start = op.begin; + this._oracleNodes[op.id].end = op.end; + this._oracle.insert(this._oracleNodes[op.id]); + + } else { + let actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); + let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); + let expected = this._oracle.search(new Interval(op.begin, op.end)); + assert.deepEqual(actual, expected); + return; + } + + if (PRINT_TREE) { + this._tree.print(); + } + + this._tree.assertInvariants(); + + let actual = this._tree.getAllInOrder().map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); + let expected = this._oracle.intervals; + assert.deepEqual(actual, expected); + } + + public getExistingNodeId(index: number): number { + let currIndex = -1; + for (let i = 0; i < this._treeNodes.length; i++) { + if (this._treeNodes[i] === null) { + continue; + } + currIndex++; + if (currIndex === index) { + return i; + } + } + throw new Error('unexpected'); + } + } + + interface IInsertOperation { + type: 'insert'; + begin: number; + end: number; + } + + interface IDeleteOperation { + type: 'delete'; + id: number; + } + + interface IChangeOperation { + type: 'change'; + id: number; + begin: number; + end: number; + } + + interface ISearchOperation { + type: 'search'; + begin: number; + end: number; + } + + type IOperation = IInsertOperation | IDeleteOperation | IChangeOperation | ISearchOperation; + + function testIntervalTree(ops: IOperation[]): void { + let state = new TestState(); + for (let i = 0; i < ops.length; i++) { + state.acceptOp(ops[i]); + } + } + + function getRandomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + function getRandomRange(min: number, max: number): [number, number] { + let begin = getRandomInt(min, max); + let length: number; + if (getRandomInt(1, 10) <= 2) { + // large range + length = getRandomInt(0, max - begin); + } else { + // small range + length = getRandomInt(0, Math.min(max - begin, 10)); + } + return [begin, begin + length]; + } + + class AutoTest { + private _ops: IOperation[] = []; + private _state: TestState = new TestState(); + private _insertCnt: number; + private _deleteCnt: number; + private _changeCnt: number; + + constructor() { + this._insertCnt = getRandomInt(MIN_INSERTS, MAX_INSERTS); + this._changeCnt = getRandomInt(MIN_CHANGE_CNT, MAX_CHANGE_CNT); + this._deleteCnt = 0; + } + + private _doRandomInsert(): void { + let range = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); + this._run({ + type: 'insert', + begin: range[0], + end: range[1] + }); + } + + private _doRandomDelete(): void { + let idx = getRandomInt(Math.floor(this._deleteCnt / 2), this._deleteCnt - 1); + this._run({ + type: 'delete', + id: this._state.getExistingNodeId(idx) + }); + } + + private _doRandomChange(): void { + let idx = getRandomInt(0, this._deleteCnt - 1); + let range = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); + this._run({ + type: 'change', + id: this._state.getExistingNodeId(idx), + begin: range[0], + end: range[1] + }); + } + + public run() { + while (this._insertCnt > 0 || this._deleteCnt > 0 || this._changeCnt > 0) { + if (this._insertCnt > 0) { + this._doRandomInsert(); + this._insertCnt--; + this._deleteCnt++; + } else if (this._changeCnt > 0) { + this._doRandomChange(); + this._changeCnt--; + } else { + this._doRandomDelete(); + this._deleteCnt--; + } + + // Let's also search for something... + let searchRange = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); + this._run({ + type: 'search', + begin: searchRange[0], + end: searchRange[1] + }); + } + } + + private _run(op: IOperation): void { + this._ops.push(op); + this._state.acceptOp(op); + } + + public print(): void { + console.log(`testIntervalTree(${JSON.stringify(this._ops)})`); + } + + } + + suite('generated', () => { + test('gen01', () => { + testIntervalTree([ + { type: 'insert', begin: 28, end: 35 }, + { type: 'insert', begin: 52, end: 54 }, + { type: 'insert', begin: 63, end: 69 } + ]); + }); + + test('gen02', () => { + testIntervalTree([ + { type: 'insert', begin: 80, end: 89 }, + { type: 'insert', begin: 92, end: 100 }, + { type: 'insert', begin: 99, end: 99 } + ]); + }); + + test('gen03', () => { + testIntervalTree([ + { type: 'insert', begin: 89, end: 96 }, + { type: 'insert', begin: 71, end: 74 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen04', () => { + testIntervalTree([ + { type: 'insert', begin: 44, end: 46 }, + { type: 'insert', begin: 85, end: 88 }, + { type: 'delete', id: 0 } + ]); + }); + + test('gen05', () => { + testIntervalTree([ + { type: 'insert', begin: 82, end: 90 }, + { type: 'insert', begin: 69, end: 73 }, + { type: 'delete', id: 0 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen06', () => { + testIntervalTree([ + { type: 'insert', begin: 41, end: 63 }, + { type: 'insert', begin: 98, end: 98 }, + { type: 'insert', begin: 47, end: 51 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen07', () => { + testIntervalTree([ + { type: 'insert', begin: 24, end: 26 }, + { type: 'insert', begin: 11, end: 28 }, + { type: 'insert', begin: 27, end: 30 }, + { type: 'insert', begin: 80, end: 85 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen08', () => { + testIntervalTree([ + { type: 'insert', begin: 100, end: 100 }, + { type: 'insert', begin: 100, end: 100 } + ]); + }); + + test('gen09', () => { + testIntervalTree([ + { type: 'insert', begin: 58, end: 65 }, + { type: 'insert', begin: 82, end: 96 }, + { type: 'insert', begin: 58, end: 65 } + ]); + }); + + test('gen10', () => { + testIntervalTree([ + { type: 'insert', begin: 32, end: 40 }, + { type: 'insert', begin: 25, end: 29 }, + { type: 'insert', begin: 24, end: 32 } + ]); + }); + + test('gen11', () => { + testIntervalTree([ + { type: 'insert', begin: 25, end: 70 }, + { type: 'insert', begin: 99, end: 100 }, + { type: 'insert', begin: 46, end: 51 }, + { type: 'insert', begin: 57, end: 57 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen12', () => { + testIntervalTree([ + { type: 'insert', begin: 20, end: 26 }, + { type: 'insert', begin: 10, end: 18 }, + { type: 'insert', begin: 99, end: 99 }, + { type: 'insert', begin: 37, end: 59 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen13', () => { + testIntervalTree([ + { type: 'insert', begin: 3, end: 91 }, + { type: 'insert', begin: 57, end: 57 }, + { type: 'insert', begin: 35, end: 44 }, + { type: 'insert', begin: 72, end: 81 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen14', () => { + testIntervalTree([ + { type: 'insert', begin: 58, end: 61 }, + { type: 'insert', begin: 34, end: 35 }, + { type: 'insert', begin: 56, end: 62 }, + { type: 'insert', begin: 69, end: 78 }, + { type: 'delete', id: 0 } + ]); + }); + + test('gen15', () => { + testIntervalTree([ + { type: 'insert', begin: 63, end: 69 }, + { type: 'insert', begin: 17, end: 24 }, + { type: 'insert', begin: 3, end: 13 }, + { type: 'insert', begin: 84, end: 94 }, + { type: 'insert', begin: 18, end: 23 }, + { type: 'insert', begin: 96, end: 98 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen16', () => { + testIntervalTree([ + { type: 'insert', begin: 27, end: 27 }, + { type: 'insert', begin: 42, end: 87 }, + { type: 'insert', begin: 42, end: 49 }, + { type: 'insert', begin: 69, end: 71 }, + { type: 'insert', begin: 20, end: 27 }, + { type: 'insert', begin: 8, end: 9 }, + { type: 'insert', begin: 42, end: 49 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen17', () => { + testIntervalTree([ + { type: 'insert', begin: 21, end: 23 }, + { type: 'insert', begin: 83, end: 87 }, + { type: 'insert', begin: 56, end: 58 }, + { type: 'insert', begin: 1, end: 55 }, + { type: 'insert', begin: 56, end: 59 }, + { type: 'insert', begin: 58, end: 60 }, + { type: 'insert', begin: 56, end: 65 }, + { type: 'delete', id: 1 }, + { type: 'delete', id: 0 }, + { type: 'delete', id: 6 } + ]); + }); + + test('gen18', () => { + testIntervalTree([ + { type: 'insert', begin: 25, end: 25 }, + { type: 'insert', begin: 67, end: 79 }, + { type: 'delete', id: 0 }, + { type: 'search', begin: 65, end: 75 } + ]); + }); + + test('force delta overflow', () => { + // Search the IntervalNode ctor for FORCE_OVERFLOWING_TEST + // to force that this test leads to a delta normalization + testIntervalTree([ + { type: 'insert', begin: 686081138593427, end: 733009856502260 }, + { type: 'insert', begin: 591031326181669, end: 591031326181672 }, + { type: 'insert', begin: 940037682731896, end: 940037682731903 }, + { type: 'insert', begin: 598413641151120, end: 598413641151128 }, + { type: 'insert', begin: 800564156553344, end: 800564156553351 }, + { type: 'insert', begin: 894198957565481, end: 894198957565491 } + ]); + }); + }); + + // TEST_COUNT = 0; + // PRINT_TREE = true; + + for (let i = 0; i < TEST_COUNT; i++) { + if (i % 100 === 0) { + console.log(`TEST ${i + 1}/${TEST_COUNT}`); + } + let test = new AutoTest(); + + try { + test.run(); + } catch (err) { + console.log(err); + test.print(); + return; + } + } + + suite('searching', () => { + + function createCormenTree(): IntervalTree { + let r = new IntervalTree(); + let data: [number, number][] = [ + [16, 21], + [8, 9], + [25, 30], + [5, 8], + [15, 23], + [17, 19], + [26, 26], + [0, 3], + [6, 10], + [19, 20] + ]; + data.forEach((int) => { + let node = new IntervalNode(null, int[0], int[1]); + r.insert(node); + }); + return r; + } + + const T = createCormenTree(); + + function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { + let actualNodes = T.intervalSearch(start, end, 0, false, 0); + let actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); + assert.deepEqual(actual, expected); + } + + test('cormen 1->2', () => { + assertIntervalSearch( + 1, 2, + [ + [0, 3], + ] + ); + }); + + test('cormen 4->8', () => { + assertIntervalSearch( + 4, 8, + [ + [5, 8], + [6, 10], + [8, 9], + ] + ); + }); + + test('cormen 10->15', () => { + assertIntervalSearch( + 10, 15, + [ + [6, 10], + [15, 23], + ] + ); + }); + + test('cormen 21->25', () => { + assertIntervalSearch( + 21, 25, + [ + [15, 23], + [16, 21], + [25, 30], + ] + ); + }); + + test('cormen 24->24', () => { + assertIntervalSearch( + 24, 24, + [ + ] + ); + }); + }); +}); diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 81dbba468d517..a2e75ba4ae938 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; -import { ModelLine, ILineEdit, LineMarker, MarkersTracker } from 'vs/editor/common/model/modelLine'; +import { ModelLine, ILineEdit } from 'vs/editor/common/model/modelLine'; import { MetadataConsts } from 'vs/editor/common/modes'; -import { Position } from 'vs/editor/common/core/position'; import { ViewLineToken, ViewLineTokenFactory } from 'vs/editor/common/core/viewLineToken'; function assertLineTokens(_actual: LineTokens, _expected: TestToken[]): void { @@ -54,7 +53,7 @@ suite('Editor Model - modelLine.applyEdits text', () => { function testEdits(initial: string, edits: ILineEdit[], expected: string): void { var line = new ModelLine(initial, NO_TAB_SIZE); - line.applyEdits(new MarkersTracker(), edits, NO_TAB_SIZE); + line.applyEdits(edits, NO_TAB_SIZE); assert.equal(line.text, expected); } @@ -62,8 +61,7 @@ suite('Editor Model - modelLine.applyEdits text', () => { return { startColumn: startColumn, endColumn: endColumn, - text: text, - forceMoveMarkers: false + text: text }; } @@ -201,7 +199,7 @@ suite('Editor Model - modelLine.split text', () => { function testLineSplit(initial: string, splitColumn: number, expected1: string, expected2: string): void { var line = new ModelLine(initial, NO_TAB_SIZE); - var newLine = line.split(new MarkersTracker(), splitColumn, false, NO_TAB_SIZE); + var newLine = line.split(splitColumn, NO_TAB_SIZE); assert.equal(line.text, expected1); assert.equal(newLine.text, expected2); } @@ -239,7 +237,7 @@ suite('Editor Model - modelLine.append text', () => { function testLineAppend(a: string, b: string, expected: string): void { var line1 = new ModelLine(a, NO_TAB_SIZE); var line2 = new ModelLine(b, NO_TAB_SIZE); - line1.append(new MarkersTracker(), 1, line2, NO_TAB_SIZE); + line1.append(line2, NO_TAB_SIZE); assert.equal(line1.text, expected); } @@ -301,7 +299,7 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { let line = new ModelLine(initialText, NO_TAB_SIZE); line.setTokens(0, TestToken.toTokens(initialTokens)); - line.applyEdits(new MarkersTracker(), edits, NO_TAB_SIZE); + line.applyEdits(edits, NO_TAB_SIZE); assert.equal(line.text, expectedText); assertLineTokens(line.getTokens(0), expectedTokens); @@ -311,10 +309,10 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { let line = new ModelLine('some text', NO_TAB_SIZE); line.setTokens(0, TestToken.toTokens([new TestToken(0, 1)])); - line.applyEdits(new MarkersTracker(), [{ startColumn: 1, endColumn: 10, text: '', forceMoveMarkers: false }], NO_TAB_SIZE); + line.applyEdits([{ startColumn: 1, endColumn: 10, text: '' }], NO_TAB_SIZE); line.setTokens(0, new Uint32Array(0)); - line.applyEdits(new MarkersTracker(), [{ startColumn: 1, endColumn: 1, text: 'a', forceMoveMarkers: false }], NO_TAB_SIZE); + line.applyEdits([{ startColumn: 1, endColumn: 1, text: 'a' }], NO_TAB_SIZE); assertLineTokens(line.getTokens(0), [new TestToken(0, 1)]); }); @@ -330,7 +328,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', [ @@ -353,7 +350,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 2, endColumn: 2, text: 'x', - forceMoveMarkers: false }], 'axabcd efgh', [ @@ -376,7 +372,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 3, endColumn: 3, text: 'stu', - forceMoveMarkers: false }], 'axstuabcd efgh', [ @@ -399,7 +394,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 10, endColumn: 10, text: '\t', - forceMoveMarkers: false }], 'axstuabcd\t efgh', [ @@ -422,7 +416,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 12, endColumn: 12, text: 'dd', - forceMoveMarkers: false }], 'axstuabcd\t ddefgh', [ @@ -445,7 +438,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 18, endColumn: 18, text: 'xyz', - forceMoveMarkers: false }], 'axstuabcd\t ddefghxyz', [ @@ -468,7 +460,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: 'x', - forceMoveMarkers: false }], 'xaxstuabcd\t ddefghxyz', [ @@ -491,7 +482,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 22, endColumn: 22, text: 'x', - forceMoveMarkers: false }], 'xaxstuabcd\t ddefghxyzx', [ @@ -514,7 +504,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 2, endColumn: 2, text: '', - forceMoveMarkers: false }], 'xaxstuabcd\t ddefghxyzx', [ @@ -533,7 +522,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'a', [ @@ -554,7 +542,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 4, endColumn: 7, text: '', - forceMoveMarkers: false }], 'abcghij', [ @@ -576,7 +563,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 4, endColumn: 4, text: 'hello', - forceMoveMarkers: false }], 'abchellodefghij', [ @@ -599,7 +585,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 2, text: '', - forceMoveMarkers: false }], 'bcd efgh', [ @@ -622,7 +607,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 2, endColumn: 4, text: '', - forceMoveMarkers: false }], 'ad efgh', [ @@ -645,7 +629,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 5, text: '', - forceMoveMarkers: false }], ' efgh', [ @@ -667,7 +650,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 6, text: '', - forceMoveMarkers: false }], 'abcdefgh', [ @@ -689,7 +671,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 7, text: '', - forceMoveMarkers: false }], 'abcdfgh', [ @@ -711,7 +692,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 10, text: '', - forceMoveMarkers: false }], 'abcd', [ @@ -732,7 +712,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 10, text: '', - forceMoveMarkers: false }], '', [ @@ -753,7 +732,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: '', - forceMoveMarkers: false }], 'abcd efgh', [ @@ -776,7 +754,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 3, text: '', - forceMoveMarkers: false }], 'cd efgh', [ @@ -799,7 +776,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 10, text: '', - forceMoveMarkers: false }], 'abcd', [ @@ -822,7 +798,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 6, text: 'Hi', - forceMoveMarkers: false }], 'Hi world, ciao', [ @@ -849,12 +824,10 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 6, text: 'Hi', - forceMoveMarkers: false }, { startColumn: 8, endColumn: 12, text: 'my friends', - forceMoveMarkers: false }], 'Hi wmy friends, ciao', [ @@ -873,7 +846,7 @@ suite('Editor Model - modelLine.split text & tokens', () => { let line = new ModelLine(initialText, NO_TAB_SIZE); line.setTokens(0, TestToken.toTokens(initialTokens)); - let other = line.split(new MarkersTracker(), splitColumn, false, NO_TAB_SIZE); + let other = line.split(splitColumn, NO_TAB_SIZE); assert.equal(line.text, expectedText1); assert.equal(other.text, expectedText2); @@ -960,7 +933,7 @@ suite('Editor Model - modelLine.append text & tokens', () => { let b = new ModelLine(bText, NO_TAB_SIZE); b.setTokens(0, TestToken.toTokens(bTokens)); - a.append(new MarkersTracker(), 1, b, NO_TAB_SIZE); + a.append(b, NO_TAB_SIZE); assert.equal(a.text, expectedText); assertLineTokens(a.getTokens(0), expectedTokens); @@ -1071,1260 +1044,483 @@ suite('Editor Model - modelLine.append text & tokens', () => { }); }); -interface ILightWeightMarker { - id: string; - lineNumber: number; - column: number; - stickToPreviousCharacter: boolean; -} - -suite('Editor Model - modelLine.applyEdits text & markers', () => { - - function marker(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(0, column), stickToPreviousCharacter); - } - - function toLightWeightMarker(marker: LineMarker): ILightWeightMarker { - return { - id: marker.id, - lineNumber: marker.position.lineNumber, - column: marker.position.column, - stickToPreviousCharacter: marker.stickToPreviousCharacter - }; - } +suite('Editor Model - modelLine.applyEdits', () => { - function testLineEditMarkers(initialText: string, initialMarkers: LineMarker[], edits: ILineEdit[], expectedText: string, expectedChangedMarkers: number[], _expectedMarkers: LineMarker[]): void { + function testLineEdit(initialText: string, edits: ILineEdit[], expectedText: string): void { let line = new ModelLine(initialText, NO_TAB_SIZE); - line.addMarkers(initialMarkers); - let changedMarkers = new MarkersTracker(); - line.applyEdits(changedMarkers, edits, NO_TAB_SIZE); + line.applyEdits(edits, NO_TAB_SIZE); assert.equal(line.text, expectedText, 'text'); - - let actualMarkers = line.getMarkers().map(toLightWeightMarker); - let expectedMarkers = _expectedMarkers.map(toLightWeightMarker); - assert.deepEqual(actualMarkers, expectedMarkers, 'markers'); - - let actualChangedMarkers = changedMarkers.getDecorationIds(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, expectedChangedMarkers, 'changed markers'); } test('insertion: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 1, text: 'abc', - forceMoveMarkers: false }], 'abcabcd efgh', - [2, 3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 4, false), - marker(3, 5, true), - marker(4, 5, false), - marker(5, 8, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 2, endColumn: 2, text: 'abc', - forceMoveMarkers: false }], 'aabcbcd efgh', - [4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 5, false), - marker(5, 8, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 3', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 3, endColumn: 3, text: 'abc', - forceMoveMarkers: false }], 'ababccd efgh', - [5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 8, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 4', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 5, endColumn: 5, text: 'abc', - forceMoveMarkers: false }], 'abcdabc efgh', - [6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 5', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 10, endColumn: 10, text: 'abc', - forceMoveMarkers: false }], 'abcd efghabc', - [8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 13, false) - ] ); }); test('insertion bis: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [2, 3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 2, false), - marker(3, 3, true), - marker(4, 3, false), - marker(5, 6, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 2, endColumn: 2, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 3, false), - marker(5, 6, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 3', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 3, endColumn: 3, text: 'a', - forceMoveMarkers: false }], 'abacd efgh', - [5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 6, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 4', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 5, endColumn: 5, text: 'a', - forceMoveMarkers: false }], 'abcda efgh', - [6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 5', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 10, endColumn: 10, text: 'a', - forceMoveMarkers: false }], 'abcd efgha', - [8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 11, false) - ] ); }); test('insertion: does not move marker at column 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [marker(1, 1, true)], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [], - [marker(1, 1, true)] ); }); test('insertion: does move marker at column 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [marker(1, 1, false)], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [1], - [marker(1, 2, false)] ); }); test('insertion: two markers at column 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [2], - [ - marker(1, 1, true), - marker(2, 2, false) - ] ); }); test('insertion: two markers at column 1 unsorted', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(2, 1, false), - marker(1, 1, true), - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [2], - [ - marker(1, 1, true), - marker(2, 2, false) - ] ); }); test('deletion: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 2, text: '', - forceMoveMarkers: false }], 'bcd efgh', - [3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 1, true), - marker(4, 1, false), - marker(5, 4, true), - marker(6, 4, false), - marker(7, 9, true), - marker(8, 9, false) - ] ); }); test('deletion: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 4, text: '', - forceMoveMarkers: false }], 'd efgh', - [3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 1, true), - marker(4, 1, false), - marker(5, 2, true), - marker(6, 2, false), - marker(7, 7, true), - marker(8, 7, false) - ] ); }); test('deletion: updates markers 3', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 5, endColumn: 6, text: '', - forceMoveMarkers: false }], 'abcdefgh', - [7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 9, true), - marker(8, 9, false) - ] ); }); test('replace: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }, { startColumn: 2, endColumn: 3, text: '', - forceMoveMarkers: false }], 'aacd efgh', - [2, 3, 4], - [ - marker(1, 1, true), - marker(2, 2, false), - marker(3, 3, true), - marker(4, 3, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ] ); }); test('delete near markers', () => { - testLineEditMarkers( + testLineEdit( 'abcd', - [ - marker(1, 3, true), - marker(2, 3, false) - ], [{ startColumn: 3, endColumn: 4, text: '', - forceMoveMarkers: false }], 'abd', - [], - [ - marker(1, 3, true), - marker(2, 3, false) - ] ); }); test('replace: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'Hello world, how are you', - [ - marker(1, 1, false), - marker(2, 6, true), - marker(3, 14, false), - marker(4, 21, true) - ], [{ startColumn: 1, endColumn: 1, text: ' - ', - forceMoveMarkers: false }, { startColumn: 6, endColumn: 12, text: '', - forceMoveMarkers: false }, { startColumn: 22, endColumn: 25, text: 'things', - forceMoveMarkers: false }], ' - Hello, how are things', - [1, 2, 3, 4], - [ - marker(1, 4, false), - marker(2, 9, true), - marker(3, 11, false), - marker(4, 18, true) - ] ); }); test('sorts markers', () => { - testLineEditMarkers( + testLineEdit( 'Hello world, how are you', - [ - marker(4, 21, true), - marker(2, 6, true), - marker(1, 1, false), - marker(3, 14, false) - ], [{ startColumn: 1, endColumn: 1, text: ' - ', - forceMoveMarkers: false }, { startColumn: 6, endColumn: 12, text: '', - forceMoveMarkers: false }, { startColumn: 22, endColumn: 25, text: 'things', - forceMoveMarkers: false }], ' - Hello, how are things', - [1, 2, 3, 4], - [ - marker(1, 4, false), - marker(2, 9, true), - marker(3, 11, false), - marker(4, 18, true) - ] ); }); test('change text inside markers', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 6, false), - marker(4, 10, true) - ], [{ startColumn: 6, endColumn: 10, text: '1234567', - forceMoveMarkers: false }], 'abcd 1234567', - [], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 6, false), - marker(4, 10, true) - ] ); }); test('inserting is different than replacing for markers part 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd', - [ - marker(1, 2, false) - ], [{ startColumn: 2, endColumn: 2, text: 'INSERT', - forceMoveMarkers: false }], 'aINSERTbcd', - [1], - [ - marker(1, 8, false) - ] ); }); test('inserting is different than replacing for markers part 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd', - [ - marker(1, 2, false) - ], [{ startColumn: 2, endColumn: 3, text: 'REPLACED', - forceMoveMarkers: false }], 'aREPLACEDcd', - [], - [ - marker(1, 2, false) - ] ); }); test('replacing the entire line with more text', () => { - testLineEditMarkers( + testLineEdit( 'this is a short text', - [ - marker(1, 1, false), - marker(2, 16, true), - ], [{ startColumn: 1, endColumn: 21, text: 'Some new text here', - forceMoveMarkers: false }], 'Some new text here', - [], - [ - marker(1, 1, false), - marker(2, 16, true), - ] ); }); test('replacing the entire line with less text', () => { - testLineEditMarkers( + testLineEdit( 'this is a short text', - [ - marker(1, 1, false), - marker(2, 16, true), - ], [{ startColumn: 1, endColumn: 21, text: 'ttt', - forceMoveMarkers: false }], 'ttt', - [2], - [ - marker(1, 1, false), - marker(2, 4, true), - ] ); }); test('replace selection', () => { - testLineEditMarkers( + testLineEdit( 'first', - [ - marker(1, 1, true), - marker(2, 6, false), - ], [{ startColumn: 1, endColumn: 6, text: 'something', - forceMoveMarkers: false }], 'something', - [2], - [ - marker(1, 1, true), - marker(2, 10, false), - ] ); }); }); -suite('Editor Model - modelLine.split text & markers', () => { - - function marker(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(0, column), stickToPreviousCharacter); - } - - function toLightWeightMarker(marker: LineMarker): ILightWeightMarker { - return { - id: marker.id, - lineNumber: marker.position.lineNumber, - column: marker.position.column, - stickToPreviousCharacter: marker.stickToPreviousCharacter - }; - } +suite('Editor Model - modelLine.split', () => { - function testLineSplitMarkers(initialText: string, initialMarkers: LineMarker[], splitColumn: number, forceMoveMarkers: boolean, expectedText1: string, expectedText2: string, expectedChangedMarkers: number[], _expectedMarkers1: LineMarker[], _expectedMarkers2: LineMarker[]): void { + function testLineSplit(initialText: string, splitColumn: number, forceMoveMarkers: boolean, expectedText1: string, expectedText2: string): void { let line = new ModelLine(initialText, NO_TAB_SIZE); - line.addMarkers(initialMarkers); - let changedMarkers = new MarkersTracker(); - let otherLine = line.split(changedMarkers, splitColumn, forceMoveMarkers, NO_TAB_SIZE); + let otherLine = line.split(splitColumn, NO_TAB_SIZE); assert.equal(line.text, expectedText1, 'text'); assert.equal(otherLine.text, expectedText2, 'text'); - - let actualMarkers1 = line.getMarkers().map(toLightWeightMarker); - let expectedMarkers1 = _expectedMarkers1.map(toLightWeightMarker); - assert.deepEqual(actualMarkers1, expectedMarkers1, 'markers'); - - let actualMarkers2 = otherLine.getMarkers().map(toLightWeightMarker); - let expectedMarkers2 = _expectedMarkers2.map(toLightWeightMarker); - assert.deepEqual(actualMarkers2, expectedMarkers2, 'markers'); - - let actualChangedMarkers = changedMarkers.getDecorationIds(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, expectedChangedMarkers, 'changed markers'); } test('split at the beginning', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 1, false, '', 'abcd efgh', - [], - [ - marker(1, 1, true) - ], - [ - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ] ); }); test('split at the beginning 2', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 1, true, '', 'abcd efgh', - [], - [], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ] ); }); test('split at the end', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 10, false, 'abcd efgh', '', - [8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - ], - [ - marker(8, 1, false) - ] ); }); test('split it the middle 1', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 2, false, 'a', 'bcd efgh', - [4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - ], - [ - marker(4, 1, false), - marker(5, 4, true), - marker(6, 4, false), - marker(7, 9, true), - marker(8, 9, false) - ] ); }); test('split it the middle 2', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 3, false, 'ab', 'cd efgh', - [5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - ], - [ - marker(5, 3, true), - marker(6, 3, false), - marker(7, 8, true), - marker(8, 8, false) - ] ); }); test('split it the middle 3', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 5, false, 'abcd', ' efgh', - [6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - ], - [ - marker(6, 1, false), - marker(7, 6, true), - marker(8, 6, false) - ] ); }); test('split it the middle 4', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 6, false, 'abcd ', 'efgh', - [7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - ], - [ - marker(7, 5, true), - marker(8, 5, false) - ] ); }); }); -suite('Editor Model - modelLine.append text & markers', () => { - - function markerOnFirstLine(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(1, column), stickToPreviousCharacter); - } - - function markerOnSecondLine(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(2, column), stickToPreviousCharacter); - } - - function toLightWeightMarker(marker: LineMarker): ILightWeightMarker { - return { - id: marker.id, - lineNumber: marker.position.lineNumber, - column: marker.position.column, - stickToPreviousCharacter: marker.stickToPreviousCharacter - }; - } +suite('Editor Model - modelLine.append', () => { - function testLinePrependMarkers(aText: string, aMarkers: LineMarker[], bText: string, bMarkers: LineMarker[], expectedText: string, expectedChangedMarkers: number[], _expectedMarkers: LineMarker[]): void { + function testLinePrependMarkers(aText: string, bText: string, expectedText: string): void { let a = new ModelLine(aText, NO_TAB_SIZE); - a.addMarkers(aMarkers); - let b = new ModelLine(bText, NO_TAB_SIZE); - b.addMarkers(bMarkers); - let changedMarkers = new MarkersTracker(); - a.append(changedMarkers, 1, b, NO_TAB_SIZE); + a.append(b, NO_TAB_SIZE); assert.equal(a.text, expectedText, 'text'); - - let actualMarkers = a.getMarkers().map(toLightWeightMarker); - let expectedMarkers = _expectedMarkers.map(toLightWeightMarker); - assert.deepEqual(actualMarkers, expectedMarkers, 'markers'); - - let actualChangedMarkers = changedMarkers.getDecorationIds(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, expectedChangedMarkers, 'changed markers'); } test('append to an empty', () => { testLinePrependMarkers( 'abcd efgh', - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false), - ], '', - [ - ], 'abcd efgh', - [], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); test('append an empty', () => { testLinePrependMarkers( '', - [ - ], 'abcd efgh', - [ - markerOnSecondLine(1, 1, true), - markerOnSecondLine(2, 1, false), - markerOnSecondLine(3, 2, true), - markerOnSecondLine(4, 2, false), - markerOnSecondLine(5, 5, true), - markerOnSecondLine(6, 5, false), - markerOnSecondLine(7, 10, true), - markerOnSecondLine(8, 10, false), - ], 'abcd efgh', - [1, 2, 3, 4, 5, 6, 7, 8], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); test('append 1', () => { testLinePrependMarkers( 'abcd', - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false) - ], ' efgh', - [ - markerOnSecondLine(5, 1, true), - markerOnSecondLine(6, 1, false), - markerOnSecondLine(7, 6, true), - markerOnSecondLine(8, 6, false), - ], 'abcd efgh', - [5, 6, 7, 8], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); test('append 2', () => { testLinePrependMarkers( 'abcd e', - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false) - ], 'fgh', - [ - markerOnSecondLine(7, 4, true), - markerOnSecondLine(8, 4, false), - ], 'abcd efgh', - [7, 8], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 1b2264090f0e6..ea402ff32c42f 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, TrackedRangeStickiness, EndOfLineSequence } from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; // --------- utils @@ -27,7 +27,8 @@ function modelHasDecorations(model: Model, decorations: ILightWeightDecoration2[ className: actualDecorations[i].options.className }); } - assert.deepEqual(modelDecorations, decorations, 'Model decorations'); + modelDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + assert.deepEqual(modelDecorations, decorations); } function modelHasDecoration(model: Model, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string) { @@ -168,13 +169,13 @@ suite('Editor Model - Model Decorations', () => { var decId1 = addDecoration(thisModel, 1, 2, 3, 2, 'myType1'); var decId2 = addDecoration(thisModel, 1, 2, 3, 1, 'myType2'); modelHasDecorations(thisModel, [ - { - range: new Range(1, 2, 3, 2), - className: 'myType1' - }, { range: new Range(1, 2, 3, 1), className: 'myType2' + }, + { + range: new Range(1, 2, 3, 2), + className: 'myType1' } ]); thisModel.changeDecorations((changeAccessor) => { @@ -207,9 +208,6 @@ suite('Editor Model - Model Decorations', () => { let listenerCalled = 0; thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 1); - assert.equal(e.changedDecorations.length, 0); - assert.equal(e.removedDecorations.length, 0); }); addDecoration(thisModel, 1, 2, 3, 2, 'myType'); assert.equal(listenerCalled, 1, 'listener called'); @@ -220,10 +218,6 @@ suite('Editor Model - Model Decorations', () => { let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 0); - assert.equal(e.changedDecorations.length, 1); - assert.equal(e.changedDecorations[0], decId); - assert.equal(e.removedDecorations.length, 0); }); thisModel.changeDecorations((changeAccessor) => { changeAccessor.changeDecoration(decId, new Range(1, 1, 1, 2)); @@ -236,10 +230,6 @@ suite('Editor Model - Model Decorations', () => { let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 0); - assert.equal(e.changedDecorations.length, 0); - assert.equal(e.removedDecorations.length, 1); - assert.equal(e.removedDecorations[0], decId); }); thisModel.changeDecorations((changeAccessor) => { changeAccessor.removeDecoration(decId); @@ -249,20 +239,31 @@ suite('Editor Model - Model Decorations', () => { test('decorations emit event when inserting one line text before it', () => { let listenerCalled = 0; - let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); + addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 0); - assert.equal(e.changedDecorations.length, 1); - assert.equal(e.changedDecorations[0], decId); - assert.equal(e.removedDecorations.length, 0); }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'Hallo ')]); assert.equal(listenerCalled, 1, 'listener called'); }); + test('decorations do not emit event on no-op deltaDecorations', () => { + let listenerCalled = 0; + + thisModel.onDidChangeDecorations((e) => { + listenerCalled++; + }); + + thisModel.deltaDecorations([], []); + thisModel.changeDecorations((accessor) => { + accessor.deltaDecorations([], []); + }); + + assert.equal(listenerCalled, 0, 'listener not called'); + }); + // --------- editing text & effects on decorations test('decorations are updated when inserting one line text before it', () => { @@ -365,6 +366,740 @@ suite('Editor Model - Model Decorations', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 3, 1))]); modelHasDecoration(thisModel, 1, 1, 2, 1, 'myType'); }); + + test('decorations are updated when changing EOL', () => { + addDecoration(thisModel, 1, 2, 4, 1, 'myType1'); + addDecoration(thisModel, 1, 3, 4, 1, 'myType2'); + addDecoration(thisModel, 1, 4, 4, 1, 'myType3'); + addDecoration(thisModel, 1, 5, 4, 1, 'myType4'); + addDecoration(thisModel, 1, 6, 4, 1, 'myType5'); + addDecoration(thisModel, 1, 7, 4, 1, 'myType6'); + addDecoration(thisModel, 1, 8, 4, 1, 'myType7'); + addDecoration(thisModel, 1, 9, 4, 1, 'myType8'); + addDecoration(thisModel, 1, 10, 4, 1, 'myType9'); + thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'x')]); + thisModel.setEOL(EndOfLineSequence.CRLF); + thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'x')]); + modelHasDecorations(thisModel, [ + { range: new Range(1, 4, 4, 1), className: 'myType1' }, + { range: new Range(1, 5, 4, 1), className: 'myType2' }, + { range: new Range(1, 6, 4, 1), className: 'myType3' }, + { range: new Range(1, 7, 4, 1), className: 'myType4' }, + { range: new Range(1, 8, 4, 1), className: 'myType5' }, + { range: new Range(1, 9, 4, 1), className: 'myType6' }, + { range: new Range(1, 10, 4, 1), className: 'myType7' }, + { range: new Range(1, 11, 4, 1), className: 'myType8' }, + { range: new Range(1, 12, 4, 1), className: 'myType9' }, + ]); + }); + + test('an apparently simple edit', () => { + addDecoration(thisModel, 1, 2, 4, 1, 'myType1'); + thisModel.applyEdits([EditOperation.replace(new Range(1, 14, 2, 1), 'x')]); + modelHasDecorations(thisModel, [ + { range: new Range(1, 2, 3, 1), className: 'myType1' }, + ]); + }); + + test('removeAllDecorationsWithOwnerId can be called after model dispose', () => { + let model = Model.createFromString('asd'); + model.dispose(); + model.removeAllDecorationsWithOwnerId(1); + }); + + test('removeAllDecorationsWithOwnerId works', () => { + thisModel.deltaDecorations([], [{ range: new Range(1, 2, 4, 1), options: { className: 'myType1' } }], 1); + thisModel.removeAllDecorationsWithOwnerId(1); + modelHasNoDecorations(thisModel); + }); +}); + +suite('Decorations and editing', () => { + + function _runTest(decRange: Range, stickiness: TrackedRangeStickiness, editRange: Range, editText: string, editForceMoveMarkers: boolean, expectedDecRange: Range, msg: string): void { + let model = Model.createFromString([ + 'My First Line', + 'My Second Line', + 'Third Line' + ].join('\n')); + + const id = model.deltaDecorations([], [{ range: decRange, options: { stickiness: stickiness } }])[0]; + model.applyEdits([{ range: editRange, text: editText, forceMoveMarkers: editForceMoveMarkers, identifier: null }]); + const actual = model.getDecorationRange(id); + assert.deepEqual(actual, expectedDecRange, msg); + + model.dispose(); + } + + function runTest(decRange: Range, editRange: Range, editText: string, expectedDecRange: Range[][]): void { + _runTest(decRange, 0, editRange, editText, false, expectedDecRange[0][0], 'no-0-AlwaysGrowsWhenTypingAtEdges'); + _runTest(decRange, 1, editRange, editText, false, expectedDecRange[0][1], 'no-1-NeverGrowsWhenTypingAtEdges'); + _runTest(decRange, 2, editRange, editText, false, expectedDecRange[0][2], 'no-2-GrowsOnlyWhenTypingBefore'); + _runTest(decRange, 3, editRange, editText, false, expectedDecRange[0][3], 'no-3-GrowsOnlyWhenTypingAfter'); + + _runTest(decRange, 0, editRange, editText, true, expectedDecRange[1][0], 'force-0-AlwaysGrowsWhenTypingAtEdges'); + _runTest(decRange, 1, editRange, editText, true, expectedDecRange[1][1], 'force-1-NeverGrowsWhenTypingAtEdges'); + _runTest(decRange, 2, editRange, editText, true, expectedDecRange[1][2], 'force-2-GrowsOnlyWhenTypingBefore'); + _runTest(decRange, 3, editRange, editText, true, expectedDecRange[1][3], 'force-3-GrowsOnlyWhenTypingAfter'); + } + + suite('insert', () => { + suite('collapsed dec', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 3), 'xx', + [ + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('equal', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 4), 'xx', + [ + [new Range(1, 4, 1, 6), new Range(1, 6, 1, 6), new Range(1, 4, 1, 4), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 5), 'xx', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 3), 'xx', + [ + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 4), 'xx', + [ + [new Range(1, 4, 1, 11), new Range(1, 6, 1, 11), new Range(1, 4, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('inside', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 5), 'xx', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + ] + ); + }); + test('end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 9), 'xx', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 11)], + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 10), 'xx', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); + + suite('delete', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), '', + [ + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), '', + [ + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), '', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), '', + [ + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), '', + [ + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), '', + [ + [new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7)], + [new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7)], + ] + ); + }); + + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), '', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), '', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), '', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + ] + ); + }); + + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), '', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), '', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), '', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + ] + ); + }); + + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), '', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), '', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); + + suite('replace short', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'c', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'c', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'c', + [ + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'c', + [ + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'c', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'c', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 5, 1, 8), new Range(1, 5, 1, 8), new Range(1, 5, 1, 8), new Range(1, 5, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'c', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'c', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'c', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'c', + [ + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'c', + [ + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'c', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 10), new Range(1, 4, 1, 10), new Range(1, 4, 1, 10), new Range(1, 4, 1, 10)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'c', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); + + suite('replace long', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Range(1, 4, 1, 6), new Range(1, 6, 1, 6), new Range(1, 4, 1, 4), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 6, 1, 11), new Range(1, 4, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 7, 1, 11), new Range(1, 7, 1, 11), new Range(1, 7, 1, 11), new Range(1, 7, 1, 11)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'cccc', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'cccc', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 8, 1, 11), new Range(1, 8, 1, 11), new Range(1, 8, 1, 11), new Range(1, 8, 1, 11)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'cccc', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'cccc', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 13), new Range(1, 4, 1, 13), new Range(1, 4, 1, 13), new Range(1, 4, 1, 13)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); }); interface ILightWeightDecoration { @@ -420,7 +1155,6 @@ suite('deltaDecorations', () => { assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(initialIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); - assert.equal(2 * initialIds.length, model._getMarkersCount(), 'does not leak markers'); actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); @@ -431,7 +1165,6 @@ suite('deltaDecorations', () => { assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(newIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); - assert.equal(2 * newIds.length, model._getMarkersCount(), 'does not leak markers'); actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index 0f52c5129c333..20bbda92be7e8 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -91,23 +91,23 @@ suite('ViewModelDecorations', () => { let actualDecorations = viewModel.getDecorationsInViewport( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) ).map((dec) => { - return dec.source.id; + return dec.options.className; }); assert.deepEqual(actualDecorations, [ - dec2, - dec3, - dec4, - dec5, - dec6, - dec7, - dec8, - dec9, - dec10, - dec11, - dec12, - dec13, - dec14, + 'dec2', + 'dec3', + 'dec4', + 'dec5', + 'dec6', + 'dec7', + 'dec8', + 'dec9', + 'dec10', + 'dec11', + 'dec12', + 'dec13', + 'dec14', ]); let inlineDecorations1 = viewModel.getViewLineRenderingData( diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8470c6ce84189..9142f59537b36 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1196,10 +1196,6 @@ declare module monaco.editor { * Options associated with this decoration. */ readonly options: IModelDecorationOptions; - /** - * A flag describing if this is a problem decoration (e.g. warning/error). - */ - readonly isForValidation: boolean; } /** @@ -1649,12 +1645,6 @@ declare module monaco.editor { getWordUntilPosition(position: IPosition): IWordAtPosition; } - /** - * A model that can track markers. - */ - export interface ITextModelWithMarkers extends ITextModel { - } - /** * Describes the behavior of decorations when typing/editing near their edges. * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` @@ -1725,12 +1715,18 @@ declare module monaco.editor { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + /** + * Gets all the decorations that should be rendered in the overview ruler as an array. + * @param ownerId If set, it will ignore decorations belonging to other owners. + * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). + */ + getOverviewRulerDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; } /** * An editable text model. */ - export interface IEditableTextModel extends ITextModelWithMarkers { + export interface IEditableTextModel extends ITextModel { /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */ @@ -1774,7 +1770,7 @@ declare module monaco.editor { /** * A model. */ - export interface IModel extends IReadOnlyModel, IEditableTextModel, ITextModelWithMarkers, ITokenizedModel, ITextModelWithDecorations { + export interface IModel extends IReadOnlyModel, IEditableTextModel, ITokenizedModel, ITextModelWithDecorations { /** * An event emitted when the contents of the model have changed. * @event @@ -2490,18 +2486,6 @@ declare module monaco.editor { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { - /** - * Lists of ids for added decorations. - */ - readonly addedDecorations: string[]; - /** - * Lists of ids for changed decorations. - */ - readonly changedDecorations: string[]; - /** - * List of ids for removed decorations. - */ - readonly removedDecorations: string[]; } /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditor.ts b/src/vs/workbench/api/electron-browser/mainThreadEditor.ts index 0da01725b1756..bc23649dbe8ce 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditor.ts @@ -244,6 +244,17 @@ export class MainThreadTextEditor { this._codeEditor.setDecorations(key, ranges); } + public setDecorationsFast(key: string, _ranges: number[]): void { + if (!this._codeEditor) { + return; + } + let ranges: Range[] = []; + for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) { + ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]); + } + this._codeEditor.setDecorationsFast(key, ranges); + } + public revealRange(range: IRange, revealType: TextEditorRevealType): void { if (!this._codeEditor) { return; diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index df3174ae67cec..6787006461b0c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -195,6 +195,14 @@ export class MainThreadEditors implements MainThreadEditorsShape { return TPromise.as(null); } + $trySetDecorationsFast(id: string, key: string, ranges: string): TPromise { + if (!this._documentsAndEditors.getEditor(id)) { + return TPromise.wrapError(disposed(`TextEditor(${id})`)); + } + this._documentsAndEditors.getEditor(id).setDecorationsFast(key, /*TODO: marshaller is too slow*/JSON.parse(ranges)); + return TPromise.as(null); + } + $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index a6c002ae30fc8..2619ab29fcb04 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -220,6 +220,7 @@ export interface MainThreadEditorsShape extends IDisposable { $tryHideEditor(id: string): TPromise; $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): TPromise; $trySetDecorations(id: string, key: string, ranges: editorCommon.IDecorationOptions[]): TPromise; + $trySetDecorationsFast(id: string, key: string, ranges: string): TPromise; $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise; $trySetSelections(id: string, selections: ISelection[]): TPromise; $tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise; diff --git a/src/vs/workbench/api/node/extHostTextEditor.ts b/src/vs/workbench/api/node/extHostTextEditor.ts index 763969acc9696..9872ac721fdaa 100644 --- a/src/vs/workbench/api/node/extHostTextEditor.ts +++ b/src/vs/workbench/api/node/extHostTextEditor.ts @@ -416,11 +416,29 @@ export class ExtHostTextEditor implements vscode.TextEditor { setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { this._runOnProxy( - () => this._proxy.$trySetDecorations( - this._id, - decorationType.key, - TypeConverters.fromRangeOrRangeWithMessage(ranges) - ) + () => { + if (TypeConverters.isDecorationOptionsArr(ranges)) { + return this._proxy.$trySetDecorations( + this._id, + decorationType.key, + TypeConverters.fromRangeOrRangeWithMessage(ranges) + ); + } else { + let _ranges: number[] = new Array(4 * ranges.length); + for (let i = 0, len = ranges.length; i < len; i++) { + const range = ranges[i]; + _ranges[4 * i] = range.start.line + 1; + _ranges[4 * i + 1] = range.start.character + 1; + _ranges[4 * i + 2] = range.end.line + 1; + _ranges[4 * i + 3] = range.end.character + 1; + } + return this._proxy.$trySetDecorationsFast( + this._id, + decorationType.key, + /*TODO: marshaller is too slow*/JSON.stringify(_ranges) + ); + } + } ); } diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 54c80ba84c46d..ca9561d8d251f 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -139,7 +139,7 @@ function isDecorationOptions(something: any): something is vscode.DecorationOpti return (typeof something.range !== 'undefined'); } -function isDecorationOptionsArr(something: vscode.Range[] | vscode.DecorationOptions[]): something is vscode.DecorationOptions[] { +export function isDecorationOptionsArr(something: vscode.Range[] | vscode.DecorationOptions[]): something is vscode.DecorationOptions[] { if (something.length === 0) { return true; } diff --git a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index 6fd5d7c1c2031..55f5e51c46fa8 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -13,7 +13,6 @@ import { IModel, TrackedRangeStickiness, IModelDeltaDecoration, IModelDecoration import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, IBreakpoint, IRawBreakpoint, State } from 'vs/workbench/parts/debug/common/debug'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { MarkdownString } from 'vs/base/common/htmlContent'; interface IDebugEditorModelData { @@ -21,7 +20,7 @@ interface IDebugEditorModelData { toDispose: lifecycle.IDisposable[]; breakpointDecorationIds: string[]; breakpointLines: number[]; - breakpointDecorationsAsMap: Map; + breakpointDecorationsAsMap: Map; currentStackDecorations: string[]; dirty: boolean; topStackFrameRange: Range; @@ -81,11 +80,12 @@ export class DebugEditorModelManager implements IWorkbenchContribution { const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp.uri.toString() === modelUrlStr); const currentStackDecorations = model.deltaDecorations([], this.createCallStackDecorations(modelUrlStr)); - const breakPointDecorations = model.deltaDecorations([], this.createBreakpointDecorations(breakpoints)); + const desiredDecorations = this.createBreakpointDecorations(model, breakpoints); + const breakPointDecorations = model.deltaDecorations([], desiredDecorations); - const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUrlStr, e))]; - const breakpointDecorationsAsMap = new Map(); - breakPointDecorations.forEach(bpd => breakpointDecorationsAsMap.set(bpd, true)); + const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUrlStr))]; + const breakpointDecorationsAsMap = new Map(); + breakPointDecorations.forEach((decorationId, index) => breakpointDecorationsAsMap.set(decorationId, desiredDecorations[index].range)); this.modelDataMap.set(modelUrlStr, { model: model, @@ -185,13 +185,23 @@ export class DebugEditorModelManager implements IWorkbenchContribution { } // breakpoints management. Represent data coming from the debug service and also send data back. - private onModelDecorationsChanged(modelUrlStr: string, e: IModelDecorationsChangedEvent): void { + private onModelDecorationsChanged(modelUrlStr: string): void { const modelData = this.modelDataMap.get(modelUrlStr); if (modelData.breakpointDecorationsAsMap.size === 0) { // I have no decorations return; } - if (!e.changedDecorations.some(decorationId => modelData.breakpointDecorationsAsMap.has(decorationId))) { + let somethingChanged = false; + modelData.breakpointDecorationsAsMap.forEach((breakpointRange, decorationId) => { + if (somethingChanged) { + return; + } + const newBreakpointRange = modelData.model.getDecorationRange(decorationId); + if (newBreakpointRange && !breakpointRange.equalsRange(newBreakpointRange)) { + somethingChanged = true; + } + }); + if (!somethingChanged) { // nothing to do, my decorations did not change. return; } @@ -254,16 +264,19 @@ export class DebugEditorModelManager implements IWorkbenchContribution { } private updateBreakpoints(modelData: IDebugEditorModelData, newBreakpoints: IBreakpoint[]): void { - modelData.breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorationIds, this.createBreakpointDecorations(newBreakpoints)); + const desiredDecorations = this.createBreakpointDecorations(modelData.model, newBreakpoints); + modelData.breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorationIds, desiredDecorations); modelData.breakpointDecorationsAsMap.clear(); - modelData.breakpointDecorationIds.forEach(id => modelData.breakpointDecorationsAsMap.set(id, true)); + modelData.breakpointDecorationIds.forEach((decorationId, index) => modelData.breakpointDecorationsAsMap.set(decorationId, desiredDecorations[index].range)); modelData.breakpointLines = newBreakpoints.map(bp => bp.lineNumber); } - private createBreakpointDecorations(breakpoints: IBreakpoint[]): IModelDeltaDecoration[] { + private createBreakpointDecorations(model: IModel, breakpoints: IBreakpoint[]): { range: Range; options: IModelDecorationOptions; }[] { return breakpoints.map((breakpoint) => { - const range = breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1) - : new Range(breakpoint.lineNumber, 1, breakpoint.lineNumber, Constants.MAX_SAFE_SMALL_INTEGER); // Decoration has to have a width #20688 + const range = model.validateRange( + breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1) + : new Range(breakpoint.lineNumber, 1, breakpoint.lineNumber, Constants.MAX_SAFE_SMALL_INTEGER) // Decoration has to have a width #20688 + ); return { options: this.getBreakpointDecorationOptions(breakpoint), range diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts index 402466a620e12..48d6e46c0c71d 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts @@ -61,6 +61,7 @@ suite('ExtHostTextEditorOptions', () => { $tryShowEditor: undefined, $tryHideEditor: undefined, $trySetDecorations: undefined, + $trySetDecorationsFast: undefined, $tryRevealRange: undefined, $trySetSelections: undefined, $tryApplyEdits: undefined,