diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 012c3602691d273197cd370f974447d0bf213275..14c8496983bb6591f8acd938e6e7d6afd3c65a03 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -77,6 +77,11 @@ export interface IModelDecorationOptions { * @internal */ showIfCollapsed?: boolean; + /** + * Collapse the decoration if its entire range is being replaced via an edit. + * @internal + */ + collapseOnReplaceEdit?: boolean; /** * Specifies the stack order of a decoration. * A decoration with greater stack order is always in front of a decoration with a lower stack order. diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index ef64df57ad2743b72203b97abe3345d453ced6c9..3839e7b3791864fc8816f3c8e27adda7f0fda573 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -58,6 +58,10 @@ const enum Constants { StickinessMaskInverse = 0b11001111, StickinessOffset = 4, + CollapseOnReplaceEditMask = 0b01000000, + CollapseOnReplaceEditMaskInverse = 0b10111111, + CollapseOnReplaceEditOffset = 6, + /** * 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 @@ -121,6 +125,14 @@ function _setNodeStickiness(node: IntervalNode, stickiness: TrackedRangeStickine (node.metadata & Constants.StickinessMaskInverse) | (stickiness << Constants.StickinessOffset) ); } +function getCollapseOnReplaceEdit(node: IntervalNode): boolean { + return ((node.metadata & Constants.CollapseOnReplaceEditMask) >>> Constants.CollapseOnReplaceEditOffset) === 1; +} +function setCollapseOnReplaceEdit(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.CollapseOnReplaceEditMaskInverse) | ((value ? 1 : 0) << Constants.CollapseOnReplaceEditOffset) + ); +} export function setNodeStickiness(node: IntervalNode, stickiness: ActualTrackedRangeStickiness): void { _setNodeStickiness(node, stickiness); } @@ -170,6 +182,7 @@ export class IntervalNode implements IModelDecoration { setNodeIsForValidation(this, false); _setNodeStickiness(this, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); setNodeIsInOverviewRuler(this, false); + setCollapseOnReplaceEdit(this, false); this.cachedVersionId = 0; this.cachedAbsoluteStart = start; @@ -199,6 +212,7 @@ export class IntervalNode implements IModelDecoration { )); _setNodeStickiness(this, this.options.stickiness); setNodeIsInOverviewRuler(this, (this.options.overviewRuler && this.options.overviewRuler.color) ? true : false); + setCollapseOnReplaceEdit(this, this.options.collapseOnReplaceEdit); } public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { @@ -417,6 +431,15 @@ export function nodeAcceptEdit(node: IntervalNode, start: number, end: number, t const nodeEnd = node.end; let endDone = false; + if (start <= nodeStart && nodeEnd <= end && getCollapseOnReplaceEdit(node)) { + // This edit encompasses the entire decoration range + // and the decoration has asked to become collapsed + node.start = start; + startDone = true; + node.end = start; + endDone = true; + } + { const moveSemantics = forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > 0 ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined); if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, start, moveSemantics)) { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 5df5b9eca13d5cb5886420bcd8570b324589ce57..4c83dab3bc068596390f0c390631701bab83e5a7 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2839,6 +2839,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly glyphMarginHoverMessage: IMarkdownString | IMarkdownString[]; readonly isWholeLine: boolean; readonly showIfCollapsed: boolean; + readonly collapseOnReplaceEdit: boolean; readonly overviewRuler: ModelDecorationOverviewRulerOptions; readonly glyphMarginClassName: string; readonly linesDecorationsClassName: string; @@ -2856,6 +2857,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || null; this.isWholeLine = options.isWholeLine || false; this.showIfCollapsed = options.showIfCollapsed || false; + this.collapseOnReplaceEdit = options.collapseOnReplaceEdit || false; this.overviewRuler = options.overviewRuler ? new ModelDecorationOverviewRulerOptions(options.overviewRuler) : null; this.glyphMarginClassName = options.glyphMarginClassName ? cleanClassName(options.glyphMarginClassName) : null; this.linesDecorationsClassName = options.linesDecorationsClassName ? cleanClassName(options.linesDecorationsClassName) : null; diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index eb65d4e2661d0d468ce95aab500ecbc81ec30d36..240d6955c3bae362467951f83972a270d15ee71b 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -54,41 +54,49 @@ const HOVER_MESSAGE_COMMAND_ALT = new MarkdownString().appendText( const decoration = { meta: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link', hoverMessage: HOVER_MESSAGE_GENERAL_META }), metaActive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link-active', hoverMessage: HOVER_MESSAGE_GENERAL_META }), alt: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link', hoverMessage: HOVER_MESSAGE_GENERAL_ALT }), altActive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link-active', hoverMessage: HOVER_MESSAGE_GENERAL_ALT }), altCommand: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link', hoverMessage: HOVER_MESSAGE_COMMAND_ALT }), altCommandActive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link-active', hoverMessage: HOVER_MESSAGE_COMMAND_ALT }), metaCommand: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link', hoverMessage: HOVER_MESSAGE_COMMAND_META }), metaCommandActive: ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + collapseOnReplaceEdit: true, inlineClassName: 'detected-link-active', hoverMessage: HOVER_MESSAGE_COMMAND_META }), diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index a64fb889d98d13278ea46d2bd11c5031192d1d51..4f1f4f7f060a5f8f976fd9dfe6e16eb1a7927a57 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -1370,4 +1370,22 @@ suite('deltaDecorations', () => { model.dispose(); }); + + test('issue #41492: URL highlighting persists after pasting over url', () => { + + let model = TextModel.createFromString([ + 'My First Line' + ].join('\n')); + + const id = model.deltaDecorations([], [{ range: new Range(1, 2, 1, 14), options: { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, collapseOnReplaceEdit: true } }])[0]; + model.applyEdits([{ + range: new Range(1, 1, 1, 14), + text: 'Some new text that is longer than the previous one', + forceMoveMarkers: false + }]); + const actual = model.getDecorationRange(id); + assert.deepEqual(actual, new Range(1, 1, 1, 1)); + + model.dispose(); + }); });