Applying custom line heights after the edits are processed#297999
Applying custom line heights after the edits are processed#297999
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes how variable/custom line heights are applied during structural model edits (insert/delete), deferring custom line-height computation until after the view model has processed the full edit batch and projections/coordinate conversion are stable.
Changes:
- Removed “lineHeightsAdded” plumbing from
onLinesInsertedacrossViewLayout→LinesLayout→LineHeightsManager. - Deferred recomputation/application of custom line heights in
ViewModel.onDidChangeContentOrInjectedTextuntil after all raw changes are applied. - Extended
ModelRawLinesDeletedto carrylastUntouchedLinePostEdit, and updated relevant tests.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/editor/test/common/viewLayout/linesLayout.test.ts | Updates tests to match the new onLinesInserted(from,to) signature. |
| src/vs/editor/test/common/viewLayout/lineHeights.test.ts | Updates tests for new signature and adds a new deletion-related test (currently problematic). |
| src/vs/editor/test/common/model/model.test.ts | Updates expected ModelRawLinesDeleted constructor args. |
| src/vs/editor/common/viewModel/viewModelLines.ts | Adds debug console.log statements inside model line insertion handling. |
| src/vs/editor/common/viewModel/viewModelImpl.ts | Defers custom line height application until projections are stable after batch processing. |
| src/vs/editor/common/viewLayout/viewLayout.ts | Updates onLinesInserted API to drop the custom-line-height payload. |
| src/vs/editor/common/viewLayout/linesLayout.ts | Updates onLinesInserted and forwards to LineHeightsManager without payload. |
| src/vs/editor/common/viewLayout/lineHeights.ts | Removes inserted-line height payload handling; insertion now assumes default height until recomputed later. |
| src/vs/editor/common/textModelEvents.ts | Extends ModelRawLinesDeleted with lastUntouchedLinePostEdit. |
| src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts | Adds debug console.log statements (including logging full buffer content). |
| src/vs/editor/common/model/textModel.ts | Computes and emits lastUntouchedLinePostEdit when generating ModelRawLinesDeleted. |
| @@ -370,6 +375,7 @@ export class ViewModel extends Disposable implements IViewModel { | |||
| if (linesDeletedEvent !== null) { | |||
| eventsCollector.emitViewEvent(linesDeletedEvent); | |||
| this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); | |||
| deferredCustomLineHeightRanges.push({ fromLineNumber: change.lastUntouchedLinePostEdit, toLineNumber: change.lastUntouchedLinePostEdit }); | |||
| } | |||
| hadOtherModelChange = true; | |||
| break; | |||
| @@ -379,7 +385,8 @@ export class ViewModel extends Disposable implements IViewModel { | |||
| const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); | |||
| if (linesInsertedEvent !== null) { | |||
| eventsCollector.emitViewEvent(linesInsertedEvent); | |||
| this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.fromLineNumberPostEdit, change.toLineNumberPostEdit)); | |||
| this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); | |||
| deferredCustomLineHeightRanges.push({ fromLineNumber: change.fromLineNumberPostEdit, toLineNumber: change.toLineNumberPostEdit }); | |||
| } | |||
| hadOtherModelChange = true; | |||
| break; | |||
| @@ -394,11 +401,13 @@ export class ViewModel extends Disposable implements IViewModel { | |||
| } | |||
| if (linesInsertedEvent) { | |||
| eventsCollector.emitViewEvent(linesInsertedEvent); | |||
| this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.lineNumberPostEdit, change.lineNumberPostEdit)); | |||
| this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); | |||
| deferredCustomLineHeightRanges.push({ fromLineNumber: change.lineNumberPostEdit, toLineNumber: change.lineNumberPostEdit }); | |||
| } | |||
| if (linesDeletedEvent) { | |||
| eventsCollector.emitViewEvent(linesDeletedEvent); | |||
| this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); | |||
| deferredCustomLineHeightRanges.push({ fromLineNumber: change.lineNumberPostEdit, toLineNumber: change.lineNumberPostEdit }); | |||
| } | |||
| break; | |||
| } | |||
| @@ -412,6 +421,19 @@ export class ViewModel extends Disposable implements IViewModel { | |||
| if (versionId !== null) { | |||
| this._lines.acceptVersionId(versionId); | |||
| } | |||
|
|
|||
| // Apply deferred custom line heights now that projections are stable | |||
| if (deferredCustomLineHeightRanges.length > 0) { | |||
| this.viewLayout.changeSpecialLineHeights((accessor: ILineHeightChangeAccessor) => { | |||
| for (const range of deferredCustomLineHeightRanges) { | |||
| const customLineHeights = this._getCustomLineHeightsForLines(range.fromLineNumber, range.toLineNumber); | |||
| for (const data of customLineHeights) { | |||
| accessor.insertOrChangeCustomLineHeight(data.decorationId, data.startLineNumber, data.endLineNumber, data.lineHeight); | |||
| } | |||
| } | |||
| }); | |||
There was a problem hiding this comment.
deferredCustomLineHeightRanges can accumulate duplicate/overlapping ranges in a single edit batch, causing repeated getCustomLineHeightsDecorationsInRange queries and repeated insertOrChangeCustomLineHeight calls for the same decoration IDs. Consider deduplicating/merging ranges (or collecting decoration IDs) before querying/applying to avoid avoidable work on large edit batches.
| test('deleting line 2 with lineHeightsRemoved re-adding at line 1 moves special line to line 1', () => { | ||
| const manager = new LineHeightsManager(10, []); | ||
| manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 20); | ||
| assert.strictEqual(manager.heightForLineNumber(2), 20); | ||
| manager.onLinesDeleted(2, 2); | ||
| assert.strictEqual(manager.heightForLineNumber(1), 20); |
There was a problem hiding this comment.
This new test’s expectation doesn’t match the current LineHeightsManager.onLinesDeleted behavior: deleting line 2 will not move an existing custom height from line 2 to line 1 (it collapses to fromLineNumber in existing tests/comments in this file). As written, this test should fail. If you intend to verify the “remove + re-add at a different line” scenario, the test needs to explicitly simulate that (e.g., remove the decoration ID and then insert/change it at line 1 after the delete), or adjust the expected line number.
| test('deleting line 2 with lineHeightsRemoved re-adding at line 1 moves special line to line 1', () => { | |
| const manager = new LineHeightsManager(10, []); | |
| manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 20); | |
| assert.strictEqual(manager.heightForLineNumber(2), 20); | |
| manager.onLinesDeleted(2, 2); | |
| assert.strictEqual(manager.heightForLineNumber(1), 20); | |
| test('deleting line 2 keeps special line height at fromLineNumber (line 2)', () => { | |
| const manager = new LineHeightsManager(10, []); | |
| manager.insertOrChangeCustomLineHeight('dec1', 2, 2, 20); | |
| assert.strictEqual(manager.heightForLineNumber(2), 20); | |
| manager.onLinesDeleted(2, 2); | |
| // After deletion, the decoration collapses to fromLineNumber (2), not moved to line 1 | |
| assert.strictEqual(manager.heightForLineNumber(1), 10); | |
| assert.strictEqual(manager.heightForLineNumber(2), 20); |
| assert.strictEqual(manager.getAccumulatedLineHeightsIncludingLineNumber(6), 110); | ||
| }); | ||
|
|
||
| test('deleting line 2 with lineHeightsRemoved re-adding at line 1 moves special line to line 1', () => { |
There was a problem hiding this comment.
The new test title is hard to parse and grammatically off ("with lineHeightsRemoved re-adding..."). Please reword it to clearly describe the scenario and expected outcome (e.g. mention deletion, then decoration removal/re-add, and what line number should end up special).
| test('deleting line 2 with lineHeightsRemoved re-adding at line 1 moves special line to line 1', () => { | |
| test('deleting line 2 and re-adding its custom height on line 1 makes line 1 special', () => { |
fixes #297959
fixes #289877