Skip to content

Commit 34ad008

Browse files
committed
feat(editor): select line on CodeMirror line number gutter click
1 parent bb7ebec commit 34ad008

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

src/cm/lineNumberSelection.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { EditorSelection } from "@codemirror/state";
2+
import type { BlockInfo, EditorView } from "@codemirror/view";
3+
4+
type LineInfo = Pick<BlockInfo, "from" | "to"> | null | undefined;
5+
6+
type LineNumberClickEvent = Pick<
7+
MouseEvent,
8+
| "button"
9+
| "shiftKey"
10+
| "altKey"
11+
| "ctrlKey"
12+
| "metaKey"
13+
| "preventDefault"
14+
| "defaultPrevented"
15+
>;
16+
17+
/**
18+
* Resolve the selection range for a clicked document line.
19+
* Includes the trailing line break when one exists to mirror Ace's
20+
* full-line selection behavior.
21+
*/
22+
export function getLineSelectionRange(
23+
state: EditorView["state"],
24+
line: LineInfo,
25+
): { from: number; to: number } | null {
26+
if (!line) return null;
27+
const from = Math.max(0, Number(line.from) || 0);
28+
const to = Math.max(from, Number(line.to) || from);
29+
return {
30+
from,
31+
to: Math.min(to + 1, state.doc.length),
32+
};
33+
}
34+
35+
/**
36+
* Select the clicked line from the line-number gutter.
37+
* Ignores modified and non-primary clicks so it doesn't interfere with
38+
* context menus or alternate selection gestures.
39+
*/
40+
export function handleLineNumberClick(
41+
view: EditorView | null | undefined,
42+
line: LineInfo,
43+
event: LineNumberClickEvent | null | undefined,
44+
): boolean {
45+
if (!view || !event || event.defaultPrevented) return false;
46+
if ((event.button ?? 0) !== 0) return false;
47+
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
48+
return false;
49+
}
50+
51+
const range = getLineSelectionRange(view.state, line);
52+
if (!range) return false;
53+
54+
event.preventDefault();
55+
view.dispatch({
56+
selection: EditorSelection.single(range.from, range.to),
57+
userEvent: "select.pointer",
58+
});
59+
view.focus();
60+
return true;
61+
}
62+
63+
export default handleLineNumberClick;

src/lib/editorManager.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
registerExternalCommand,
3232
removeExternalCommand,
3333
} from "cm/commandRegistry";
34+
import { handleLineNumberClick } from "cm/lineNumberSelection";
3435
import lspApi from "cm/lsp/api";
3536
import lspClientManager from "cm/lsp/clientManager";
3637
import {
@@ -289,6 +290,13 @@ async function EditorManager($header, $body) {
289290
function makeLineNumberExtension() {
290291
const { linenumbers = true, relativeLineNumbers = false } =
291292
appSettings?.value || {};
293+
const lineNumberConfig = {
294+
domEventHandlers: {
295+
click(view, line, event) {
296+
return handleLineNumberClick(view, line, event);
297+
},
298+
},
299+
};
292300
if (!linenumbers)
293301
return EditorView.theme({
294302
".cm-gutter": {
@@ -299,9 +307,13 @@ async function EditorManager($header, $body) {
299307
},
300308
});
301309
if (!relativeLineNumbers)
302-
return Prec.highest([lineNumbers(), highlightActiveLineGutter()]);
310+
return Prec.highest([
311+
lineNumbers(lineNumberConfig),
312+
highlightActiveLineGutter(),
313+
]);
303314
return Prec.highest([
304315
lineNumbers({
316+
...lineNumberConfig,
305317
formatNumber: (lineNo, state) => {
306318
try {
307319
const cur = state.doc.lineAt(state.selection.main.head).number;

0 commit comments

Comments
 (0)