Skip to content
Merged
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
22 changes: 22 additions & 0 deletions scripts/editorOptionWiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,25 @@ test('space-delimited languages are left exactly as they were', () => {
);
}
});

test('turning line numbers off keeps the gutter the TOC button stands on', () => {
// #810: the floating table-of-contents button is positioned against the
// pane, and what it floats over is the line-number gutter. `'off'` is the
// one `lineNumbers` value that collapses that gutter, which dropped the
// button onto the text; a render function validates as `Custom` instead, so
// Monaco keeps the width and this draws nothing in it. The assertion is
// therefore that 'off' does NOT reach Monaco as 'off'.
const off = editorOptionsFromSettings({ ...SETTINGS, lineNumbers: 'off' }, 100).lineNumbers;
assert.equal(typeof off, 'function');
assert.equal((off as (lineNumber: number) => string)(7), '');

// One value, not a closure per call: `editorOptionsFromSettings` runs in an
// effect, and a fresh arrow would be a changed option on every
// `updateOptions`.
assert.equal(off, editorOptionsFromSettings({ ...SETTINGS, lineNumbers: 'off' }, 100).lineNumbers);

// The modes that already reserved the gutter are passed through untouched.
for (const lineNumbers of ['on', 'relative', 'interval']) {
assert.equal(editorOptionsFromSettings({ ...SETTINGS, lineNumbers }, 100).lineNumbers, lineNumbers);
}
});
20 changes: 19 additions & 1 deletion src/lib/utils/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import type { editor as MonacoEditor } from "monaco-editor";

import { animatesCursor, animatesJumpScroll } from "./motion.js";

/**
* "Line Numbers: off" without giving up the gutter they sit in.
*
* Monaco reserves that gutter on `renderType !== Off`, and a render function
* validates as `Custom` — so an empty string keeps the width and draws nothing
* in it. `'off'` is the one value that collapses it, and the floating table of
* contents button stands on what it collapsed (#810).
*
* A module constant rather than an inline arrow because
* `editorOptionsFromSettings` runs inside an effect: a new closure per call is
* a new option value, and Monaco would see this option change on every
* `updateOptions`.
*/
const BLANK_LINE_NUMBER = () => "";

/**
* The Monaco options derived from the settings store, in one place.
*
Expand Down Expand Up @@ -41,7 +56,10 @@ export function editorOptionsFromSettings(
// of the window edge (#758). 'advanced' measures in the DOM and is slow
// on large files, so only a proportional font pays for it.
wrappingStrategy: fontIsMonospace ? "simple" : "advanced",
lineNumbers: settings.lineNumbers as "on" | "off" | "relative" | "interval",
lineNumbers:
settings.lineNumbers === "off"
? BLANK_LINE_NUMBER
: (settings.lineNumbers as "on" | "relative" | "interval"),
// A Monaco string enum, not a flag. Any non-empty string is truthy, so
// a ternary on it can only ever produce "line" — which defeats both the
// line-highlight toggle and Zen mode, whose whole effect is 'none'.
Expand Down
Loading