diff --git a/src/vs/editor/browser/gpu/gpuUtils.ts b/src/vs/editor/browser/gpu/gpuUtils.ts index 4647d8adc33299..f5422f2a0147f0 100644 --- a/src/vs/editor/browser/gpu/gpuUtils.ts +++ b/src/vs/editor/browser/gpu/gpuUtils.ts @@ -5,6 +5,15 @@ import { BugIndicatingError } from '../../../base/common/errors.js'; import { toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; +import type { EditorLayoutInfo } from '../../common/config/editorOptions.js'; + +export function getContentScissorRect(layout: EditorLayoutInfo, devicePixelRatio: number, canvasWidth: number, canvasHeight: number, constrainWidth: boolean): [number, number, number, number] { + const left = Math.min(canvasWidth, Math.max(0, Math.ceil(layout.contentLeft * devicePixelRatio))); + const right = constrainWidth + ? Math.min(canvasWidth, Math.floor((layout.contentLeft + layout.contentWidth - layout.verticalScrollbarWidth) * devicePixelRatio)) + : canvasWidth; + return [left, 0, Math.max(0, right - left), canvasHeight]; +} export const quadVertices = new Float32Array([ 1, 0, diff --git a/src/vs/editor/browser/gpu/rectangleRenderer.ts b/src/vs/editor/browser/gpu/rectangleRenderer.ts index 09e68a80be8e2f..dac8e709c32d66 100644 --- a/src/vs/editor/browser/gpu/rectangleRenderer.ts +++ b/src/vs/editor/browser/gpu/rectangleRenderer.ts @@ -13,7 +13,7 @@ import type { ViewScrollChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import { GPULifecycle } from './gpuDisposable.js'; -import { observeDevicePixelDimensions, quadVertices } from './gpuUtils.js'; +import { getContentScissorRect, observeDevicePixelDimensions, quadVertices } from './gpuUtils.js'; import { createObjectCollectionBuffer, type IObjectCollectionBuffer, type IObjectCollectionBufferEntry } from './objectCollectionBuffer.js'; import { RectangleRendererBindingId, rectangleRendererWgsl } from './rectangleRenderer.wgsl.js'; @@ -57,7 +57,6 @@ export class RectangleRenderer extends ViewEventHandler { constructor( private readonly _context: ViewContext, - private readonly _contentLeft: IObservable, private readonly _devicePixelRatio: IObservable, private readonly _canvas: HTMLCanvasElement, private readonly _ctx: GPUCanvasContext, @@ -285,8 +284,12 @@ export class RectangleRenderer extends ViewEventHandler { pass.setBindGroup(0, this._bindGroup); // Only draw the content area - const contentLeft = Math.ceil(this._contentLeft.get() * this._devicePixelRatio.get()); - pass.setScissorRect(contentLeft, 0, this._canvas.width - contentLeft, this._canvas.height); + pass.setScissorRect(...getContentScissorRect( + this._context.configuration.options.get(EditorOption.layoutInfo), + this._devicePixelRatio.get(), + this._canvas.width, this._canvas.height, + this._context.configuration.options.get(EditorOption.padding).maxEditorCanvasWidth > 0 + )); pass.draw(quadVertices.length / 2, this._shapeCollection.entryCount); pass.end(); diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 9d520abdb3b077..8d84a8a279190a 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -150,7 +150,7 @@ export class ViewGpuContext extends Disposable { })); this.contentLeft = contentLeft; - this.rectangleRenderer = this._register(this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device)); + this.rectangleRenderer = this._register(this._instantiationService.createInstance(RectangleRenderer, context, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device)); } /** diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 816dc313885dc7..ff54d34a7a2e71 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -112,13 +112,7 @@ export class EditorScrollbar extends ViewPart { this.scrollbarDomNode.setLeft(layoutInfo.contentLeft); - const minimap = options.get(EditorOption.minimap); - const side = minimap.side; - if (side === 'right') { - this.scrollbarDomNode.setWidth(layoutInfo.contentWidth + layoutInfo.minimap.minimapWidth); - } else { - this.scrollbarDomNode.setWidth(layoutInfo.contentWidth); - } + this.scrollbarDomNode.setWidth(layoutInfo.width - layoutInfo.contentLeft); this.scrollbarDomNode.setHeight(layoutInfo.height); } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.css b/src/vs/editor/browser/viewParts/viewLines/viewLines.css index 2e2e717307bf2e..5cdc8084e4ac20 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.css +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.css @@ -58,6 +58,10 @@ white-space: nowrap; } +.monaco-editor .lines-content > :not(.contentWidgets) { + clip-path: var(--editor-canvas-clip, none); +} + .monaco-editor .view-line { box-sizing: border-box; position: absolute; diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index cb81982129107e..34190735927f0b 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -674,6 +674,13 @@ export class ViewLines extends ViewPart implements IViewLines { const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta; this._linesContent.setTop(-adjustedScrollTop); this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); + const options = this._context.configuration.options; + const layoutInfo = options.get(EditorOption.layoutInfo); + const scrollLeft = this._context.viewLayout.getCurrentScrollLeft(); + const clip = options.get(EditorOption.padding).maxEditorCanvasWidth > 0 + ? `inset(${adjustedScrollTop}px calc(100% - ${scrollLeft + Math.max(0, layoutInfo.contentWidth - layoutInfo.verticalScrollbarWidth)}px) calc(100% - ${adjustedScrollTop + layoutInfo.height}px) ${scrollLeft}px)` + : 'none'; + this._linesContent.domNode.style.setProperty('--editor-canvas-clip', clip); } // --- width diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 53d6115a4956b1..c5047f6c079ce9 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -16,7 +16,7 @@ import type { ViewContext } from '../../../common/viewModel/viewContext.js'; import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js'; import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js'; import { GPULifecycle } from '../../gpu/gpuDisposable.js'; -import { quadVertices } from '../../gpu/gpuUtils.js'; +import { getContentScissorRect, quadVertices } from '../../gpu/gpuUtils.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; import { FloatHorizontalRange, HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, RenderingContext, RestrictedRenderingContext, VisibleRanges } from '../../view/renderingContext.js'; import { ViewPart } from '../../view/viewPart.js'; @@ -498,8 +498,12 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { pass.setVertexBuffer(0, this._vertexBuffer); // Only draw the content area - const contentLeft = Math.ceil(this._viewGpuContext.contentLeft.get() * this._viewGpuContext.devicePixelRatio.get()); - pass.setScissorRect(contentLeft, 0, this.canvas.width - contentLeft, this.canvas.height); + pass.setScissorRect(...getContentScissorRect( + this._context.configuration.options.get(EditorOption.layoutInfo), + this._viewGpuContext.devicePixelRatio.get(), + this.canvas.width, this.canvas.height, + this._context.configuration.options.get(EditorOption.padding).maxEditorCanvasWidth > 0 + )); pass.setBindGroup(0, this._bindGroup); diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 29d8394571114b..2590970e7b9588 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -1675,7 +1675,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const layoutInfo = options.get(EditorOption.layoutInfo); const top = CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, position.lineNumber, position.column) - this.getScrollTop(); - const left = this._modelData.view.getOffsetForColumn(position.lineNumber, position.column) + layoutInfo.glyphMarginWidth + layoutInfo.lineNumbersWidth + layoutInfo.decorationsWidth - this.getScrollLeft(); + const left = this._modelData.view.getOffsetForColumn(position.lineNumber, position.column) + layoutInfo.contentLeft - this.getScrollLeft(); const height = this.getLineHeightForPosition(position); return { top: top, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 0a283d7abc78a0..8df6a671d9ee7e 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2974,7 +2974,11 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption 0 + ? Math.min(availableCanvasWidth, padding.maxEditorCanvasWidth + verticalScrollbarWidth) + : availableCanvasWidth; + contentLeft += Math.floor(Math.max(0, availableCanvasWidth - contentWidth) / 2); // (leaving 2px for the cursor to have space after the last character) const viewportColumn = Math.max(1, Math.floor((contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth)); @@ -3608,6 +3612,11 @@ export interface IEditorPaddingOptions { * Spacing between bottom edge of editor and last line. */ bottom?: number; + /** + * Maximum text viewport width in CSS pixels, excluding gutters, minimap, and scrollbar. + * Extra space is split evenly on either side of the text. Defaults to 0 (no limit). + */ + maxEditorCanvasWidth?: number; } /** @@ -3619,7 +3628,7 @@ class EditorPadding extends BaseEditorOption { if (e.scrollLeftChanged) { @@ -189,6 +192,9 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const layoutInfo = this._editor.getLayoutInfo(); const lineNumbersWidth = layoutInfo.contentLeft; this._lineNumbersDomNode.style.width = `${lineNumbersWidth}px`; + this._linesDomNodeScrollable.style.maxWidth = this._editor.getOption(EditorOption.padding).maxEditorCanvasWidth > 0 + ? `${Math.max(0, layoutInfo.contentWidth - layoutInfo.verticalScrollbarWidth)}px` + : ''; this._linesDomNodeScrollable.style.setProperty('--vscode-editorStickyScroll-scrollableWidth', `${this._editor.getScrollWidth() - layoutInfo.verticalScrollbarWidth}px`); this._rootDomNode.style.width = `${layoutInfo.width - layoutInfo.verticalScrollbarWidth}px`; } diff --git a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index 0d516e35a14318..2be3befa75eda0 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -36,6 +36,8 @@ interface IEditorLayoutProviderOpts { readonly minimapMaxColumn: number; minimapSize?: 'proportional' | 'fill' | 'fit'; readonly pixelRatio: number; + readonly maxEditorCanvasWidth?: number; + readonly wordWrap?: 'off' | 'on'; } suite('Editor ViewLayout - EditorLayoutProvider', () => { @@ -43,12 +45,16 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { ensureNoDisposablesAreLeakedInTestSuite(); function doTest(input: IEditorLayoutProviderOpts, expected: EditorLayoutInfo): void { + assert.deepStrictEqual(computeLayout(input), expected); + } + + function computeLayout(input: IEditorLayoutProviderOpts): EditorLayoutInfo { const options = new ComputedEditorOptions(); options._write(EditorOption.glyphMargin, input.showGlyphMargin); options._write(EditorOption.lineNumbersMinChars, input.lineNumbersMinChars); options._write(EditorOption.lineDecorationsWidth, input.lineDecorationsWidth); options._write(EditorOption.folding, false); - options._write(EditorOption.padding, { top: 0, bottom: 0 }); + options._write(EditorOption.padding, EditorOptions.padding.validate({ maxEditorCanvasWidth: input.maxEditorCanvasWidth })); const minimapOptions: EditorMinimapOptions = { enabled: input.minimap, autohide: 'none', @@ -88,13 +94,13 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { }; options._write(EditorOption.lineNumbers, lineNumbersOptions); - options._write(EditorOption.wordWrap, 'off'); + options._write(EditorOption.wordWrap, input.wordWrap ?? 'off'); options._write(EditorOption.wordWrapColumn, 80); options._write(EditorOption.wordWrapOverride1, 'inherit'); options._write(EditorOption.wordWrapOverride2, 'inherit'); options._write(EditorOption.accessibilitySupport, 'auto'); - const actual = EditorLayoutInfoComputer.computeLayout(options, { + return EditorLayoutInfoComputer.computeLayout(options, { memory: null, outerWidth: input.outerWidth, outerHeight: input.outerHeight, @@ -107,9 +113,78 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { pixelRatio: input.pixelRatio, glyphMarginDecorationLaneCount: 1, }); - assert.deepStrictEqual(actual, expected); } + suite('canvas padding', () => { + const input: IEditorLayoutProviderOpts = { + outerWidth: 1200, + outerHeight: 800, + showGlyphMargin: true, + lineHeight: 20, + showLineNumbers: true, + lineNumbersMinChars: 3, + lineNumbersDigitCount: 3, + lineDecorationsWidth: 10, + typicalHalfwidthCharacterWidth: 10, + maxDigitWidth: 10, + verticalScrollbarWidth: 14, + verticalScrollbarHasArrows: false, + scrollbarArrowSize: 0, + horizontalScrollbarHeight: 10, + minimap: false, + minimapSide: 'right', + minimapRenderCharacters: true, + minimapMaxColumn: 150, + pixelRatio: 1, + wordWrap: 'on' + }; + + test('validates the cap and preserves vertical padding', () => { + assert.deepStrictEqual([ + EditorOptions.padding.validate(undefined), + EditorOptions.padding.validate({ maxEditorCanvasWidth: -1 }), + EditorOptions.padding.validate({ top: 12, bottom: 18, maxEditorCanvasWidth: 600.9 }), + EditorOptions.padding.validate({ maxEditorCanvasWidth: 20000 }) + ], [ + { top: 0, bottom: 0, maxEditorCanvasWidth: 0 }, + { top: 0, bottom: 0, maxEditorCanvasWidth: 0 }, + { top: 12, bottom: 18, maxEditorCanvasWidth: 600 }, + { top: 0, bottom: 0, maxEditorCanvasWidth: 10000 } + ]); + }); + + for (const minimap of [false, true]) { + for (const minimapSide of ['left', 'right'] as const) { + test(`centers the text without moving gutters or minimap (${minimap}, ${minimapSide})`, () => { + const baseline = computeLayout({ ...input, minimap, minimapSide }); + const padded = computeLayout({ ...input, minimap, minimapSide, maxEditorCanvasWidth: 600 }); + assert.deepStrictEqual(padded, { + ...baseline, + contentLeft: baseline.contentLeft + Math.floor((baseline.contentWidth - 614) / 2), + contentWidth: 614, + viewportColumn: 59, + wrappingColumn: 59 + }); + }); + } + } + + test('uses available width below the cap and restores the default when disabled', () => { + for (const outerWidth of [0, 400, 674, 1200]) { + const baseline = computeLayout({ ...input, outerWidth }); + assert.deepStrictEqual(computeLayout({ ...input, outerWidth, maxEditorCanvasWidth: 0 }), baseline); + if (outerWidth <= 674) { + assert.deepStrictEqual(computeLayout({ ...input, outerWidth, maxEditorCanvasWidth: 600 }), baseline); + } + } + }); + + test('does not enable word wrapping', () => { + const padded = computeLayout({ ...input, wordWrap: 'off', maxEditorCanvasWidth: 600 }); + assert.deepStrictEqual([padded.contentWidth, padded.isViewportWrapping, padded.wrappingColumn], [614, false, -1]); + }); + }); + test('EditorLayoutProvider 1', () => { doTest({ outerWidth: 1000, diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts index 821496ef86e6af..ac61b2ce7801e1 100644 --- a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -4,17 +4,142 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { ContentWidgetPositionPreference } from '../../../browser/editorBrowser.js'; +import { getContentScissorRect } from '../../../browser/gpu/gpuUtils.js'; +import { CodeEditorWidget } from '../../../browser/widget/codeEditor/codeEditorWidget.js'; +import { IEditorOptions } from '../../../common/config/editorOptions.js'; import { Range } from '../../../common/core/range.js'; import { Selection } from '../../../common/core/selection.js'; import { ILanguageService } from '../../../common/languages/language.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; -import { withTestCodeEditor } from '../testCodeEditor.js'; +import { StickyScrollWidget } from '../../../contrib/stickyScroll/browser/stickyScrollWidget.js'; +import { createTextModel } from '../../common/testTextModel.js'; +import { createCodeEditorServices, withTestCodeEditor } from '../testCodeEditor.js'; suite('CodeEditorWidget', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('canvas padding rendering', () => { + function createEditor(options: IEditorOptions = {}): CodeEditorWidget { + const host = document.createElement('div'); + host.style.width = '1200px'; + host.style.height = '400px'; + document.body.appendChild(host); + store.add(toDisposable(() => host.remove())); + const services = createCodeEditorServices(store); + const model = store.add(createTextModel('Readable text. '.repeat(100))); + const editor = store.add(services.createInstance(CodeEditorWidget, host, { + minimap: { enabled: false }, + padding: { maxEditorCanvasWidth: 600 }, + wordWrap: 'on', + ...options + }, { contributions: [] })); + editor.setModel(model); + editor.render(true); + return editor; + } + + for (const side of ['left', 'right'] as const) { + test(`keeps caret coordinates and the scrollbar aligned with a ${side} minimap`, () => { + const editor = createEditor({ minimap: { enabled: true, side } }); + const layout = editor.getLayoutInfo(); + const root = editor.getDomNode(); + const scrollbar = root.querySelector('.scrollbar.vertical')!; + const bounds = root.getBoundingClientRect(); + const position = editor.getScrolledVisiblePosition({ lineNumber: 1, column: 5 })!; + const target = editor.getTargetAtClientPoint(bounds.left + position.left, bounds.top + position.top + position.height / 2); + assert.deepStrictEqual({ + textWidth: layout.contentWidth - layout.verticalScrollbarWidth, + caretLeft: position.left, + column: target?.position?.column, + scrollbarRight: Math.round(scrollbar.getBoundingClientRect().right - bounds.left) + }, { + textWidth: 600, + caretLeft: layout.contentLeft + editor.getOffsetForColumn(1, 5), + column: 5, + scrollbarRight: 1200 + }); + }); + } + + test('updates clipping and layout when resized or disabled', () => { + const editor = createEditor(); + editor.layout({ width: 400, height: 400 }); + editor.render(true); + const narrow = editor.getLayoutInfo(); + editor.updateOptions({ padding: { maxEditorCanvasWidth: 0 } }); + editor.render(true); + assert.deepStrictEqual(editor.getLayoutInfo(), narrow); + assert.strictEqual(getComputedStyle(editor.getDomNode().querySelector('.view-lines')!).clipPath, 'none'); + editor.layout({ width: 1200, height: 400 }); + editor.updateOptions({ padding: { maxEditorCanvasWidth: 300 } }); + editor.render(true); + const wide = editor.getLayoutInfo(); + assert.strictEqual(wide.contentWidth - wide.verticalScrollbarWidth, 300); + }); + + test('bounds GPU clipping for scaling, resize, and disabled padding', () => { + const editor = createEditor(); + const layout = editor.getLayoutInfo(); + for (const ratio of [1, 1.25, 2]) { + const left = Math.ceil(layout.contentLeft * ratio); + const right = Math.floor((layout.contentLeft + 600) * ratio); + assert.deepStrictEqual(getContentScissorRect(layout, ratio, 1200 * ratio, 400 * ratio, true), [left, 0, right - left, 400 * ratio]); + assert.deepStrictEqual(getContentScissorRect(layout, ratio, 1200 * ratio, 400 * ratio, false), [left, 0, 1200 * ratio - left, 400 * ratio]); + } + assert.deepStrictEqual(getContentScissorRect(layout, 1, 10, 400, true), [10, 0, 0, 400]); + editor.layout({ width: 0, height: 400 }); + assert.deepStrictEqual(getContentScissorRect(editor.getLayoutInfo(), 1, 0, 400, true), [0, 0, 0, 400]); + }); + + test('updates sticky-scroll width when resized or disabled', () => { + const editor = createEditor(); + const widget = store.add(new StickyScrollWidget(editor)); + const text = widget.getDomNode().querySelector('.sticky-widget-lines-scrollable')!; + assert.strictEqual(text.style.maxWidth, '600px'); + editor.layout({ width: 400, height: 400 }); + const layout = editor.getLayoutInfo(); + assert.strictEqual(text.style.maxWidth, `${layout.contentWidth - layout.verticalScrollbarWidth}px`); + editor.updateOptions({ padding: { maxEditorCanvasWidth: 0 } }); + assert.strictEqual(text.style.maxWidth, ''); + }); + + test('does not clip the caret in its zero-height layer', async () => { + const editor = createEditor({ cursorBlinking: 'solid', cursorWidth: 6 }); + editor.focus(); + editor.setPosition({ lineNumber: 1, column: 4 }); + editor.render(true); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + editor.render(true); + const cursor = editor.getDomNode().querySelector('.cursor')!; + cursor.style.pointerEvents = 'auto'; + const bounds = cursor.getBoundingClientRect(); + assert.ok(bounds.width > 0 && bounds.height > 0); + assert.strictEqual(document.elementFromPoint(bounds.left + bounds.width / 2, bounds.top + bounds.height / 2), cursor); + }); + + test('does not clip content widgets at the text boundary', async () => { + const editor = createEditor({ padding: { maxEditorCanvasWidth: 300 } }); + const node = document.createElement('div'); + node.style.width = '500px'; + node.style.height = '30px'; + const widget = { + getId: () => 'canvas-padding-test', + getDomNode: () => node, + getPosition: () => ({ position: { lineNumber: 1, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] }) + }; + editor.addContentWidget(widget); + store.add(toDisposable(() => editor.removeContentWidget(widget))); + editor.render(true); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + editor.render(true); + const bounds = node.getBoundingClientRect(); + assert.strictEqual(node.contains(document.elementFromPoint(bounds.left + 305, bounds.top + 10)), true); + }); + }); test('onDidChangeModelDecorations', () => { withTestCodeEditor('', {}, (editor, viewModel) => { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 09915a982eb45f..76b2dcef19c743 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4589,6 +4589,11 @@ declare namespace monaco.editor { * Spacing between bottom edge of editor and last line. */ bottom?: number; + /** + * Maximum text viewport width in CSS pixels, excluding gutters, minimap, and scrollbar. + * Extra space is split evenly on either side of the text. Defaults to 0 (no limit). + */ + maxEditorCanvasWidth?: number; } /**