Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/vs/editor/browser/gpu/gpuUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions src/vs/editor/browser/gpu/rectangleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -57,7 +57,6 @@ export class RectangleRenderer extends ViewEventHandler {

constructor(
private readonly _context: ViewContext,
private readonly _contentLeft: IObservable<number>,
private readonly _devicePixelRatio: IObservable<number>,
private readonly _canvas: HTMLCanvasElement,
private readonly _ctx: GPUCanvasContext,
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/browser/gpu/viewGpuContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 4 additions & 0 deletions src/vs/editor/browser/viewParts/viewLines/viewLines.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/vs/editor/browser/viewParts/viewLines/viewLines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 20 additions & 3 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2974,7 +2974,11 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption<EditorOption.
decorationsLeft += minimapLayout.minimapWidth;
contentLeft += minimapLayout.minimapWidth;
}
const contentWidth = remainingWidth - minimapLayout.minimapWidth;
const availableCanvasWidth = remainingWidth - minimapLayout.minimapWidth;
const contentWidth = padding.maxEditorCanvasWidth > 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));
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -3619,7 +3628,7 @@ class EditorPadding extends BaseEditorOption<EditorOption.padding, IEditorPaddin

constructor() {
super(
EditorOption.padding, 'padding', { top: 0, bottom: 0 },
EditorOption.padding, 'padding', { top: 0, bottom: 0, maxEditorCanvasWidth: 0 },
{
'editor.padding.top': {
type: 'number',
Expand All @@ -3634,6 +3643,13 @@ class EditorPadding extends BaseEditorOption<EditorOption.padding, IEditorPaddin
minimum: 0,
maximum: 1000,
description: nls.localize('padding.bottom', "Controls the amount of space between the bottom edge of the editor and the last line.")
},
'editor.padding.maxEditorCanvasWidth': {
type: 'number',
default: 0,
minimum: 0,
maximum: 10000,
description: nls.localize('padding.maxEditorCanvasWidth', "Controls the maximum width of the text area in pixels, excluding gutters, minimap, and scrollbar. Extra space is split evenly on either side of the text. Set to 0 to use the full available width.")
}
}
);
Expand All @@ -3647,7 +3663,8 @@ class EditorPadding extends BaseEditorOption<EditorOption.padding, IEditorPaddin

return {
top: EditorIntOption.clampedInt(input.top, 0, 0, 1000),
bottom: EditorIntOption.clampedInt(input.bottom, 0, 0, 1000)
bottom: EditorIntOption.clampedInt(input.bottom, 0, 0, 1000),
maxEditorCanvasWidth: EditorIntOption.clampedInt(input.maxEditorCanvasWidth, 0, 0, 10000)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
if (e.hasChanged(EditorOption.stickyScroll)) {
updateScrollLeftPosition();
}
if (e.hasChanged(EditorOption.padding)) {
this._updateWidgetWidth();
}
}));
this._register(this._editor.onDidScrollChange((e) => {
if (e.scrollLeftChanged) {
Expand Down Expand Up @@ -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`;
}
Expand Down
83 changes: 79 additions & 4 deletions src/vs/editor/test/browser/config/editorLayoutProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,25 @@ interface IEditorLayoutProviderOpts {
readonly minimapMaxColumn: number;
minimapSize?: 'proportional' | 'fill' | 'fit';
readonly pixelRatio: number;
readonly maxEditorCanvasWidth?: number;
readonly wordWrap?: 'off' | 'on';
}

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',
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading