@gregorlohaus/codemirror-helix 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/commands.d.ts +72 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +506 -0
- package/dist/commands.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/keymap.d.ts +10 -0
- package/dist/keymap.d.ts.map +1 -0
- package/dist/keymap.js +521 -0
- package/dist/keymap.js.map +1 -0
- package/dist/motions.d.ts +29 -0
- package/dist/motions.d.ts.map +1 -0
- package/dist/motions.js +184 -0
- package/dist/motions.js.map +1 -0
- package/dist/prompt.d.ts +13 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +68 -0
- package/dist/prompt.js.map +1 -0
- package/dist/state.d.ts +54 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +50 -0
- package/dist/state.js.map +1 -0
- package/dist/textobjects.d.ts +12 -0
- package/dist/textobjects.d.ts.map +1 -0
- package/dist/textobjects.js +116 -0
- package/dist/textobjects.js.map +1 -0
- package/dist/view.d.ts +10 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +105 -0
- package/dist/view.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @gregorlohaus/codemirror-helix
|
|
2
|
+
|
|
3
|
+
[Helix](https://helix-editor.com/)-style modal editing for [CodeMirror 6](https://codemirror.net/).
|
|
4
|
+
|
|
5
|
+
Selection-first editing with multiple selections, Normal/Insert/Select modes,
|
|
6
|
+
goto & match modes, textobjects, surround, registers, counts, and search.
|
|
7
|
+
|
|
8
|
+
> A **core motions subset** — broad coverage of everyday Helix keys, not full
|
|
9
|
+
> parity. No tree-sitter textobjects, LSP gotos, macros, or jumplist.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @gregorlohaus/codemirror-helix
|
|
15
|
+
# peers: @codemirror/{state,view,commands,language,search}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { EditorView, basicSetup } from "codemirror";
|
|
22
|
+
import { helix } from "@gregorlohaus/codemirror-helix";
|
|
23
|
+
|
|
24
|
+
new EditorView({
|
|
25
|
+
doc: "hello world",
|
|
26
|
+
extensions: [basicSetup, helix()],
|
|
27
|
+
parent: document.body,
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`helix()` needs a selection drawer for multi-cursor rendering — `basicSetup`
|
|
32
|
+
(or `drawSelection()`) covers it.
|
|
33
|
+
|
|
34
|
+
### Options
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
helix({
|
|
38
|
+
startInInsert: false, // start in Normal mode (default)
|
|
39
|
+
statusBar: true, // show the bottom mode line
|
|
40
|
+
// Let an open autocomplete popup eat the first Escape instead of leaving Insert:
|
|
41
|
+
escapeGuard: (state) => completionStatus(state) === "active",
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Keys
|
|
46
|
+
|
|
47
|
+
Starts in **Normal** mode. The status line shows the mode, pending count,
|
|
48
|
+
register, and selection count.
|
|
49
|
+
|
|
50
|
+
| Group | Keys |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| Modes | `i`/`a` insert before/after, `I`/`A` line start/end, `o`/`O` open line, `v` select (extend), `Esc` normal |
|
|
53
|
+
| Motion | `h j k l`, `w W b B e E`, `f t F T {char}`, `Alt-.` repeat find, `Home`/`End`, counts (`3w`) |
|
|
54
|
+
| Goto `g` | `gg`/`Ng` line, `ge` end, `gh`/`gl` line ends, `gs` first non-blank, `gt`/`gc`/`gb` view top/center/bottom |
|
|
55
|
+
| Select | `x` line (repeat extends), `X` line bounds, `%` all, `;` collapse, `Alt-;` flip, `Alt-:` forward, `,` keep primary, `Alt-,` remove primary, `(`/`)` rotate, `_` trim |
|
|
56
|
+
| Multi | `s` select regex, `S` split, `K`/`Alt-K` keep/remove, `C` copy selection below, `Alt-C` above |
|
|
57
|
+
| Match `m` | `mm` matching bracket, `mi{o}`/`ma{o}` inside/around (`w W p ( [ { < " ' \` m`), `ms{c}` surround, `md{c}` delete, `mr{c}{c}` replace |
|
|
58
|
+
| Edit | `d` delete, `c` change, `y` yank, `p`/`P` paste, `R` replace w/ register, `r{c}` replace char, `~`/`` ` ``/`Alt-`` ` `` case, `J` join, `>`/`<` indent, `u`/`U` undo/redo |
|
|
59
|
+
| Registers | `"{c}` select register for the next yank/delete/paste |
|
|
60
|
+
| Search | `/` `?` search, `n`/`N` next/prev, `*` search selection |
|
|
61
|
+
| View | `zz`/`zt`/`zb` center/top/bottom, `Ctrl-d`/`Ctrl-u` half page, `Ctrl-f`/`Ctrl-b` page |
|
|
62
|
+
| Clipboard | `space y` copy, `space p`/`space P` paste (system clipboard) |
|
|
63
|
+
|
|
64
|
+
`s`/`S`/`K`/`Alt-K` open a prompt and preview live as you type the regex;
|
|
65
|
+
`Enter` commits, `Esc` restores the original selection.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { EditorSelection, type EditorState, type SelectionRange } from "@codemirror/state";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import { type HelixMode } from "./state";
|
|
4
|
+
/** After leaving an edit, choose the resting mode. */
|
|
5
|
+
declare function restMode(current: HelixMode, target?: HelixMode): HelixMode;
|
|
6
|
+
export type PointFn = (state: EditorState, pos: number) => number;
|
|
7
|
+
export interface MotionOpts {
|
|
8
|
+
extend: boolean;
|
|
9
|
+
span: boolean;
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function moveByPoint(view: EditorView, fn: PointFn, opts: MotionOpts): void;
|
|
13
|
+
export declare function moveVertical(view: EditorView, forward: boolean, opts: {
|
|
14
|
+
extend: boolean;
|
|
15
|
+
count: number;
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function enterInsert(view: EditorView, posOf: (state: EditorState, r: SelectionRange) => number): void;
|
|
18
|
+
export declare function openLine(view: EditorView, above: boolean): void;
|
|
19
|
+
interface DeleteOpts {
|
|
20
|
+
insert?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function deleteSelection(view: EditorView, opts?: DeleteOpts): void;
|
|
23
|
+
export declare function yankSelection(view: EditorView): void;
|
|
24
|
+
export declare function paste(view: EditorView, before: boolean): void;
|
|
25
|
+
/** R: replace selections with the register contents. */
|
|
26
|
+
export declare function replaceWithYank(view: EditorView): void;
|
|
27
|
+
/** r{char}: replace every selected character with `char`. */
|
|
28
|
+
export declare function replaceChar(view: EditorView, char: string): void;
|
|
29
|
+
type CaseMode = "toggle" | "lower" | "upper";
|
|
30
|
+
export declare function changeCase(view: EditorView, mode: CaseMode): void;
|
|
31
|
+
/** J: join the lines spanned by each selection into one. */
|
|
32
|
+
export declare function joinLines(view: EditorView): void;
|
|
33
|
+
export declare function indent(view: EditorView, less: boolean): void;
|
|
34
|
+
export declare function undoCmd(view: EditorView): void;
|
|
35
|
+
export declare function redoCmd(view: EditorView): void;
|
|
36
|
+
export declare function collapseToCursor(view: EditorView): void;
|
|
37
|
+
export declare function flipSelections(view: EditorView): void;
|
|
38
|
+
export declare function ensureForward(view: EditorView): void;
|
|
39
|
+
export declare function keepPrimary(view: EditorView): void;
|
|
40
|
+
export declare function removePrimary(view: EditorView): void;
|
|
41
|
+
export declare function rotatePrimary(view: EditorView, forward: boolean): void;
|
|
42
|
+
export declare function selectAll(view: EditorView): void;
|
|
43
|
+
export declare function trimSelections(view: EditorView): void;
|
|
44
|
+
/** x: extend each selection to whole line(s); repeat grows downward. */
|
|
45
|
+
export declare function selectLine(view: EditorView, count: number): void;
|
|
46
|
+
/** X: extend selections to cover full lines without crossing into the next. */
|
|
47
|
+
export declare function extendToLineBounds(view: EditorView): void;
|
|
48
|
+
/** C / Alt-C: copy each selection onto the next/previous line at the same columns. */
|
|
49
|
+
export declare function copySelectionToLine(view: EditorView, below: boolean): void;
|
|
50
|
+
/**
|
|
51
|
+
* s: keep only the regex matches found within each selection of `base`.
|
|
52
|
+
* `base` is the selection captured when the prompt opened, so this can be
|
|
53
|
+
* called live on every keystroke without compounding.
|
|
54
|
+
*/
|
|
55
|
+
export declare function selectRegexInSelections(view: EditorView, pattern: string, base?: EditorSelection): void;
|
|
56
|
+
/** S: split each selection of `base` on the regex, keeping the pieces between. */
|
|
57
|
+
export declare function splitOnRegex(view: EditorView, pattern: string, base?: EditorSelection): void;
|
|
58
|
+
/** Keep (or remove) selections of `base` whose text matches the regex. */
|
|
59
|
+
export declare function filterSelections(view: EditorView, pattern: string, keep: boolean, base?: EditorSelection): void;
|
|
60
|
+
/** ms{char}: wrap each selection in the chosen pair. */
|
|
61
|
+
export declare function surroundAdd(view: EditorView, ch: string): void;
|
|
62
|
+
/** md{char}: remove the surrounding pair around each selection. */
|
|
63
|
+
export declare function surroundDelete(view: EditorView, ch: string): void;
|
|
64
|
+
/** mr{from}{to}: replace the surrounding pair. */
|
|
65
|
+
export declare function surroundReplace(view: EditorView, fromCh: string, toCh: string): void;
|
|
66
|
+
export declare function runSearch(view: EditorView, query: string, reverse: boolean): void;
|
|
67
|
+
export declare function searchNext(view: EditorView, reverse: boolean): void;
|
|
68
|
+
export declare function searchSelection(view: EditorView, reverse: boolean): void;
|
|
69
|
+
export declare function scrollTo(view: EditorView, y: "center" | "start" | "end"): void;
|
|
70
|
+
export declare function halfPage(view: EditorView, forward: boolean, extend: boolean): void;
|
|
71
|
+
export { restMode };
|
|
72
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { EditorSelection, } from "@codemirror/state";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import { indentLess, indentMore, redo, undo } from "@codemirror/commands";
|
|
4
|
+
import { findNext, findPrevious, SearchQuery, setSearchQuery } from "@codemirror/search";
|
|
5
|
+
import { clearPending, getHelix, helixEffect, readRegister, setRegister, } from "./state";
|
|
6
|
+
import * as M from "./motions";
|
|
7
|
+
const settle = { ...clearPending() };
|
|
8
|
+
function clamp(n, state) {
|
|
9
|
+
return Math.max(0, Math.min(n, state.doc.length));
|
|
10
|
+
}
|
|
11
|
+
function rangesText(state, ranges) {
|
|
12
|
+
return ranges.map((r) => state.doc.sliceString(r.from, r.to)).join("\n");
|
|
13
|
+
}
|
|
14
|
+
function maybeClipboard(register, text) {
|
|
15
|
+
if ((register === "+" || register === "*") && typeof navigator !== "undefined" && navigator.clipboard) {
|
|
16
|
+
void navigator.clipboard.writeText(text).catch(() => { });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** After leaving an edit, choose the resting mode. */
|
|
20
|
+
function restMode(current, target) {
|
|
21
|
+
if (target)
|
|
22
|
+
return target;
|
|
23
|
+
return current === "select" ? "normal" : current;
|
|
24
|
+
}
|
|
25
|
+
export function moveByPoint(view, fn, opts) {
|
|
26
|
+
const { state } = view;
|
|
27
|
+
const selection = EditorSelection.create(state.selection.ranges.map((r) => {
|
|
28
|
+
let head = r.head;
|
|
29
|
+
for (let i = 0; i < Math.max(1, opts.count); i++)
|
|
30
|
+
head = fn(state, head);
|
|
31
|
+
head = clamp(head, state);
|
|
32
|
+
const anchor = opts.extend ? r.anchor : opts.span ? r.head : head;
|
|
33
|
+
return EditorSelection.range(clamp(anchor, state), head);
|
|
34
|
+
}), state.selection.mainIndex);
|
|
35
|
+
view.dispatch({ selection, scrollIntoView: true });
|
|
36
|
+
}
|
|
37
|
+
export function moveVertical(view, forward, opts) {
|
|
38
|
+
const selection = EditorSelection.create(view.state.selection.ranges.map((r) => {
|
|
39
|
+
let cur = EditorSelection.cursor(r.head);
|
|
40
|
+
for (let i = 0; i < Math.max(1, opts.count); i++)
|
|
41
|
+
cur = view.moveVertically(cur, forward);
|
|
42
|
+
return opts.extend
|
|
43
|
+
? EditorSelection.range(r.anchor, cur.head)
|
|
44
|
+
: EditorSelection.cursor(cur.head);
|
|
45
|
+
}), view.state.selection.mainIndex);
|
|
46
|
+
view.dispatch({ selection, scrollIntoView: true });
|
|
47
|
+
}
|
|
48
|
+
// --- insertion entry points --------------------------------------------------
|
|
49
|
+
export function enterInsert(view, posOf) {
|
|
50
|
+
const { state } = view;
|
|
51
|
+
const selection = EditorSelection.create(state.selection.ranges.map((r) => EditorSelection.cursor(clamp(posOf(state, r), state))), state.selection.mainIndex);
|
|
52
|
+
view.dispatch({ selection, effects: helixEffect.of({ mode: "insert", ...settle }) });
|
|
53
|
+
}
|
|
54
|
+
export function openLine(view, above) {
|
|
55
|
+
const { state } = view;
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const changes = [];
|
|
58
|
+
for (const r of state.selection.ranges) {
|
|
59
|
+
const line = state.doc.lineAt(r.head);
|
|
60
|
+
if (seen.has(line.number))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(line.number);
|
|
63
|
+
const indent = line.text.match(/^\s*/)?.[0] ?? "";
|
|
64
|
+
changes.push(above ? { from: line.from, insert: `${indent}\n` } : { from: line.to, insert: `\n${indent}` });
|
|
65
|
+
}
|
|
66
|
+
const changeSet = state.changes(changes);
|
|
67
|
+
// Place a cursor on each newly opened line.
|
|
68
|
+
const selection = EditorSelection.create([...seen].map((n) => {
|
|
69
|
+
const line = state.doc.line(n);
|
|
70
|
+
const pos = above ? line.from : line.to;
|
|
71
|
+
const mapped = changeSet.mapPos(pos, 1);
|
|
72
|
+
return EditorSelection.cursor(mapped);
|
|
73
|
+
}));
|
|
74
|
+
view.dispatch({
|
|
75
|
+
changes: changeSet,
|
|
76
|
+
selection,
|
|
77
|
+
effects: helixEffect.of({ mode: "insert", ...settle }),
|
|
78
|
+
scrollIntoView: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function deleteSelection(view, opts = {}) {
|
|
82
|
+
const { state } = view;
|
|
83
|
+
const helix = getHelix(state);
|
|
84
|
+
const specs = state.selection.ranges.map((r) => ({
|
|
85
|
+
from: r.from,
|
|
86
|
+
to: r.empty ? Math.min(r.to + 1, state.doc.length) : r.to,
|
|
87
|
+
}));
|
|
88
|
+
const yanked = specs.map((s) => state.doc.sliceString(s.from, s.to)).join("\n");
|
|
89
|
+
const changeSet = state.changes(specs.map((s) => ({ from: s.from, to: s.to, insert: "" })));
|
|
90
|
+
const selection = EditorSelection.create(specs.map((s) => EditorSelection.cursor(changeSet.mapPos(s.from, -1))), state.selection.mainIndex);
|
|
91
|
+
view.dispatch({
|
|
92
|
+
changes: changeSet,
|
|
93
|
+
selection,
|
|
94
|
+
effects: [
|
|
95
|
+
setRegister.of({ name: helix.register ?? '"', text: yanked }),
|
|
96
|
+
helixEffect.of({ mode: opts.insert ? "insert" : restMode(helix.mode), ...settle }),
|
|
97
|
+
],
|
|
98
|
+
scrollIntoView: true,
|
|
99
|
+
});
|
|
100
|
+
maybeClipboard(helix.register, yanked);
|
|
101
|
+
}
|
|
102
|
+
export function yankSelection(view) {
|
|
103
|
+
const { state } = view;
|
|
104
|
+
const helix = getHelix(state);
|
|
105
|
+
const text = rangesText(state, state.selection.ranges);
|
|
106
|
+
view.dispatch({
|
|
107
|
+
effects: [
|
|
108
|
+
setRegister.of({ name: helix.register ?? '"', text }),
|
|
109
|
+
helixEffect.of({ mode: restMode(helix.mode), ...settle }),
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
maybeClipboard(helix.register, text);
|
|
113
|
+
}
|
|
114
|
+
export function paste(view, before) {
|
|
115
|
+
const { state } = view;
|
|
116
|
+
const helix = getHelix(state);
|
|
117
|
+
const text = readRegister(state, helix.register);
|
|
118
|
+
if (!text)
|
|
119
|
+
return;
|
|
120
|
+
const linewise = text.endsWith("\n");
|
|
121
|
+
const changes = [];
|
|
122
|
+
for (const r of state.selection.ranges) {
|
|
123
|
+
if (linewise) {
|
|
124
|
+
const line = state.doc.lineAt(before ? r.from : r.to);
|
|
125
|
+
if (before)
|
|
126
|
+
changes.push({ from: line.from, to: line.from, insert: text });
|
|
127
|
+
else
|
|
128
|
+
changes.push({ from: line.to, to: line.to, insert: `\n${text.replace(/\n$/, "")}` });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const at = before ? r.from : r.to;
|
|
132
|
+
changes.push({ from: at, to: at, insert: text });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const changeSet = state.changes(changes);
|
|
136
|
+
view.dispatch({
|
|
137
|
+
changes: changeSet,
|
|
138
|
+
selection: state.selection.map(changeSet),
|
|
139
|
+
effects: helixEffect.of(settle),
|
|
140
|
+
scrollIntoView: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** R: replace selections with the register contents. */
|
|
144
|
+
export function replaceWithYank(view) {
|
|
145
|
+
const { state } = view;
|
|
146
|
+
const helix = getHelix(state);
|
|
147
|
+
const text = readRegister(state, helix.register);
|
|
148
|
+
if (!text)
|
|
149
|
+
return;
|
|
150
|
+
const changes = state.selection.ranges.map((r) => ({ from: r.from, to: r.to, insert: text }));
|
|
151
|
+
const changeSet = state.changes(changes);
|
|
152
|
+
view.dispatch({
|
|
153
|
+
changes: changeSet,
|
|
154
|
+
selection: state.selection.map(changeSet),
|
|
155
|
+
effects: helixEffect.of(settle),
|
|
156
|
+
scrollIntoView: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** r{char}: replace every selected character with `char`. */
|
|
160
|
+
export function replaceChar(view, char) {
|
|
161
|
+
if (char.length !== 1)
|
|
162
|
+
return;
|
|
163
|
+
const { state } = view;
|
|
164
|
+
const changes = state.selection.ranges
|
|
165
|
+
.map((r) => {
|
|
166
|
+
const from = r.from;
|
|
167
|
+
const to = r.empty ? Math.min(r.to + 1, state.doc.length) : r.to;
|
|
168
|
+
const text = state.doc.sliceString(from, to).replace(/[^\n]/g, char);
|
|
169
|
+
return { from, to, insert: text };
|
|
170
|
+
})
|
|
171
|
+
.filter((c) => c.to > c.from);
|
|
172
|
+
if (!changes.length)
|
|
173
|
+
return;
|
|
174
|
+
view.dispatch({ changes, effects: helixEffect.of(settle) });
|
|
175
|
+
}
|
|
176
|
+
export function changeCase(view, mode) {
|
|
177
|
+
const { state } = view;
|
|
178
|
+
const changes = state.selection.ranges
|
|
179
|
+
.map((r) => {
|
|
180
|
+
const from = r.from;
|
|
181
|
+
const to = r.empty ? Math.min(r.to + 1, state.doc.length) : r.to;
|
|
182
|
+
const text = state.doc.sliceString(from, to);
|
|
183
|
+
const next = mode === "lower"
|
|
184
|
+
? text.toLowerCase()
|
|
185
|
+
: mode === "upper"
|
|
186
|
+
? text.toUpperCase()
|
|
187
|
+
: text.replace(/[a-zA-Z]/g, (c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase()));
|
|
188
|
+
return { from, to, insert: next };
|
|
189
|
+
})
|
|
190
|
+
.filter((c) => c.to > c.from);
|
|
191
|
+
if (!changes.length)
|
|
192
|
+
return;
|
|
193
|
+
view.dispatch({ changes, effects: helixEffect.of(settle) });
|
|
194
|
+
}
|
|
195
|
+
/** J: join the lines spanned by each selection into one. */
|
|
196
|
+
export function joinLines(view) {
|
|
197
|
+
const { state } = view;
|
|
198
|
+
const changes = [];
|
|
199
|
+
for (const r of state.selection.ranges) {
|
|
200
|
+
const startLine = state.doc.lineAt(r.from).number;
|
|
201
|
+
const endLine = state.doc.lineAt(r.to).number;
|
|
202
|
+
const last = Math.max(endLine, startLine + 1);
|
|
203
|
+
for (let n = startLine; n < last && n < state.doc.lines; n++) {
|
|
204
|
+
const line = state.doc.line(n);
|
|
205
|
+
const nextLine = state.doc.line(n + 1);
|
|
206
|
+
const trimmed = nextLine.text.match(/^\s*/)?.[0].length ?? 0;
|
|
207
|
+
changes.push({ from: line.to, to: nextLine.from + trimmed, insert: " " });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!changes.length)
|
|
211
|
+
return;
|
|
212
|
+
view.dispatch({ changes, effects: helixEffect.of(settle), scrollIntoView: true });
|
|
213
|
+
}
|
|
214
|
+
export function indent(view, less) {
|
|
215
|
+
(less ? indentLess : indentMore)(view);
|
|
216
|
+
view.dispatch({ effects: helixEffect.of(settle) });
|
|
217
|
+
}
|
|
218
|
+
export function undoCmd(view) {
|
|
219
|
+
undo(view);
|
|
220
|
+
}
|
|
221
|
+
export function redoCmd(view) {
|
|
222
|
+
redo(view);
|
|
223
|
+
}
|
|
224
|
+
// --- selection manipulation --------------------------------------------------
|
|
225
|
+
function setSelection(view, ranges, mainIndex) {
|
|
226
|
+
if (!ranges.length)
|
|
227
|
+
return;
|
|
228
|
+
view.dispatch({
|
|
229
|
+
selection: EditorSelection.create(ranges, mainIndex ?? ranges.length - 1),
|
|
230
|
+
scrollIntoView: true,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
export function collapseToCursor(view) {
|
|
234
|
+
setSelection(view, view.state.selection.ranges.map((r) => EditorSelection.cursor(r.head)), view.state.selection.mainIndex);
|
|
235
|
+
}
|
|
236
|
+
export function flipSelections(view) {
|
|
237
|
+
setSelection(view, view.state.selection.ranges.map((r) => EditorSelection.range(r.head, r.anchor)), view.state.selection.mainIndex);
|
|
238
|
+
}
|
|
239
|
+
export function ensureForward(view) {
|
|
240
|
+
setSelection(view, view.state.selection.ranges.map((r) => EditorSelection.range(r.from, r.to)), view.state.selection.mainIndex);
|
|
241
|
+
}
|
|
242
|
+
export function keepPrimary(view) {
|
|
243
|
+
const main = view.state.selection.main;
|
|
244
|
+
setSelection(view, [main], 0);
|
|
245
|
+
}
|
|
246
|
+
export function removePrimary(view) {
|
|
247
|
+
const { ranges, mainIndex } = view.state.selection;
|
|
248
|
+
if (ranges.length < 2)
|
|
249
|
+
return;
|
|
250
|
+
const remaining = ranges.filter((_, i) => i !== mainIndex);
|
|
251
|
+
setSelection(view, remaining, Math.min(mainIndex, remaining.length - 1));
|
|
252
|
+
}
|
|
253
|
+
export function rotatePrimary(view, forward) {
|
|
254
|
+
const { ranges, mainIndex } = view.state.selection;
|
|
255
|
+
if (ranges.length < 2)
|
|
256
|
+
return;
|
|
257
|
+
const next = (mainIndex + (forward ? 1 : -1) + ranges.length) % ranges.length;
|
|
258
|
+
setSelection(view, [...ranges], next);
|
|
259
|
+
}
|
|
260
|
+
export function selectAll(view) {
|
|
261
|
+
setSelection(view, [EditorSelection.range(0, view.state.doc.length)], 0);
|
|
262
|
+
}
|
|
263
|
+
export function trimSelections(view) {
|
|
264
|
+
const { state } = view;
|
|
265
|
+
const ranges = [];
|
|
266
|
+
for (const r of state.selection.ranges) {
|
|
267
|
+
const text = state.doc.sliceString(r.from, r.to);
|
|
268
|
+
const lead = text.match(/^\s*/)?.[0].length ?? 0;
|
|
269
|
+
const trail = text.match(/\s*$/)?.[0].length ?? 0;
|
|
270
|
+
const from = r.from + lead;
|
|
271
|
+
const to = Math.max(from, r.to - trail);
|
|
272
|
+
ranges.push(EditorSelection.range(from, to));
|
|
273
|
+
}
|
|
274
|
+
setSelection(view, ranges, state.selection.mainIndex);
|
|
275
|
+
}
|
|
276
|
+
/** x: extend each selection to whole line(s); repeat grows downward. */
|
|
277
|
+
export function selectLine(view, count) {
|
|
278
|
+
const { state } = view;
|
|
279
|
+
const ranges = state.selection.ranges.map((r) => {
|
|
280
|
+
const startLine = state.doc.lineAt(r.from);
|
|
281
|
+
let endLine = state.doc.lineAt(r.to);
|
|
282
|
+
const from = startLine.from;
|
|
283
|
+
let to = Math.min(endLine.to + 1, state.doc.length);
|
|
284
|
+
const whole = r.from === from && r.to === to;
|
|
285
|
+
const steps = whole ? Math.max(1, count) : Math.max(0, count - 1);
|
|
286
|
+
for (let i = 0; i < steps && endLine.number < state.doc.lines; i++) {
|
|
287
|
+
endLine = state.doc.line(endLine.number + 1);
|
|
288
|
+
to = Math.min(endLine.to + 1, state.doc.length);
|
|
289
|
+
}
|
|
290
|
+
return EditorSelection.range(from, to);
|
|
291
|
+
});
|
|
292
|
+
setSelection(view, ranges, state.selection.mainIndex);
|
|
293
|
+
}
|
|
294
|
+
/** X: extend selections to cover full lines without crossing into the next. */
|
|
295
|
+
export function extendToLineBounds(view) {
|
|
296
|
+
const { state } = view;
|
|
297
|
+
const ranges = state.selection.ranges.map((r) => EditorSelection.range(state.doc.lineAt(r.from).from, state.doc.lineAt(r.to).to));
|
|
298
|
+
setSelection(view, ranges, state.selection.mainIndex);
|
|
299
|
+
}
|
|
300
|
+
/** C / Alt-C: copy each selection onto the next/previous line at the same columns. */
|
|
301
|
+
export function copySelectionToLine(view, below) {
|
|
302
|
+
const { state } = view;
|
|
303
|
+
const additions = [];
|
|
304
|
+
for (const r of state.selection.ranges) {
|
|
305
|
+
const anchorLine = state.doc.lineAt(r.anchor);
|
|
306
|
+
const headLine = state.doc.lineAt(r.head);
|
|
307
|
+
const anchorCol = r.anchor - anchorLine.from;
|
|
308
|
+
const headCol = r.head - headLine.from;
|
|
309
|
+
const targetAnchorNo = anchorLine.number + (below ? 1 : -1);
|
|
310
|
+
const targetHeadNo = headLine.number + (below ? 1 : -1);
|
|
311
|
+
if (targetAnchorNo < 1 || targetAnchorNo > state.doc.lines)
|
|
312
|
+
continue;
|
|
313
|
+
if (targetHeadNo < 1 || targetHeadNo > state.doc.lines)
|
|
314
|
+
continue;
|
|
315
|
+
const ta = state.doc.line(targetAnchorNo);
|
|
316
|
+
const th = state.doc.line(targetHeadNo);
|
|
317
|
+
additions.push(EditorSelection.range(Math.min(ta.from + anchorCol, ta.to), Math.min(th.from + headCol, th.to)));
|
|
318
|
+
}
|
|
319
|
+
if (!additions.length)
|
|
320
|
+
return;
|
|
321
|
+
const all = [...state.selection.ranges, ...additions];
|
|
322
|
+
setSelection(view, all, all.length - 1);
|
|
323
|
+
}
|
|
324
|
+
function restoreBase(view, base) {
|
|
325
|
+
view.dispatch({ selection: base, scrollIntoView: true });
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* s: keep only the regex matches found within each selection of `base`.
|
|
329
|
+
* `base` is the selection captured when the prompt opened, so this can be
|
|
330
|
+
* called live on every keystroke without compounding.
|
|
331
|
+
*/
|
|
332
|
+
export function selectRegexInSelections(view, pattern, base = view.state.selection) {
|
|
333
|
+
if (!pattern)
|
|
334
|
+
return restoreBase(view, base);
|
|
335
|
+
let re;
|
|
336
|
+
try {
|
|
337
|
+
re = new RegExp(pattern, "g");
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return; // incomplete/invalid regex: leave the last good preview in place
|
|
341
|
+
}
|
|
342
|
+
const { state } = view;
|
|
343
|
+
const ranges = [];
|
|
344
|
+
for (const r of base.ranges) {
|
|
345
|
+
const text = state.doc.sliceString(r.from, r.to);
|
|
346
|
+
for (const m of text.matchAll(re)) {
|
|
347
|
+
const from = r.from + (m.index ?? 0);
|
|
348
|
+
const to = from + m[0].length;
|
|
349
|
+
ranges.push(EditorSelection.range(from, Math.max(from, to)));
|
|
350
|
+
if (m[0].length === 0)
|
|
351
|
+
re.lastIndex++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (ranges.length)
|
|
355
|
+
setSelection(view, ranges, ranges.length - 1);
|
|
356
|
+
else
|
|
357
|
+
restoreBase(view, base);
|
|
358
|
+
}
|
|
359
|
+
/** S: split each selection of `base` on the regex, keeping the pieces between. */
|
|
360
|
+
export function splitOnRegex(view, pattern, base = view.state.selection) {
|
|
361
|
+
if (!pattern)
|
|
362
|
+
return restoreBase(view, base);
|
|
363
|
+
let re;
|
|
364
|
+
try {
|
|
365
|
+
re = new RegExp(pattern, "g");
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const { state } = view;
|
|
371
|
+
const ranges = [];
|
|
372
|
+
for (const r of base.ranges) {
|
|
373
|
+
const text = state.doc.sliceString(r.from, r.to);
|
|
374
|
+
let last = 0;
|
|
375
|
+
for (const m of text.matchAll(re)) {
|
|
376
|
+
const idx = m.index ?? 0;
|
|
377
|
+
ranges.push(EditorSelection.range(r.from + last, r.from + idx));
|
|
378
|
+
last = idx + m[0].length;
|
|
379
|
+
if (m[0].length === 0)
|
|
380
|
+
re.lastIndex++;
|
|
381
|
+
}
|
|
382
|
+
ranges.push(EditorSelection.range(r.from + last, r.to));
|
|
383
|
+
}
|
|
384
|
+
const filtered = ranges.filter((r) => r.to >= r.from);
|
|
385
|
+
if (filtered.length)
|
|
386
|
+
setSelection(view, filtered, filtered.length - 1);
|
|
387
|
+
else
|
|
388
|
+
restoreBase(view, base);
|
|
389
|
+
}
|
|
390
|
+
/** Keep (or remove) selections of `base` whose text matches the regex. */
|
|
391
|
+
export function filterSelections(view, pattern, keep, base = view.state.selection) {
|
|
392
|
+
if (!pattern)
|
|
393
|
+
return restoreBase(view, base);
|
|
394
|
+
let re;
|
|
395
|
+
try {
|
|
396
|
+
re = new RegExp(pattern);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const { state } = view;
|
|
402
|
+
const ranges = base.ranges.filter((r) => {
|
|
403
|
+
const matches = re.test(state.doc.sliceString(r.from, r.to));
|
|
404
|
+
return keep ? matches : !matches;
|
|
405
|
+
});
|
|
406
|
+
if (ranges.length)
|
|
407
|
+
setSelection(view, ranges, ranges.length - 1);
|
|
408
|
+
else
|
|
409
|
+
restoreBase(view, base);
|
|
410
|
+
}
|
|
411
|
+
// --- surround ----------------------------------------------------------------
|
|
412
|
+
const SURROUND_PAIRS = {
|
|
413
|
+
"(": ["(", ")"],
|
|
414
|
+
")": ["(", ")"],
|
|
415
|
+
"[": ["[", "]"],
|
|
416
|
+
"]": ["[", "]"],
|
|
417
|
+
"{": ["{", "}"],
|
|
418
|
+
"}": ["{", "}"],
|
|
419
|
+
"<": ["<", ">"],
|
|
420
|
+
">": ["<", ">"],
|
|
421
|
+
};
|
|
422
|
+
function surroundPair(ch) {
|
|
423
|
+
return SURROUND_PAIRS[ch] ?? [ch, ch];
|
|
424
|
+
}
|
|
425
|
+
/** ms{char}: wrap each selection in the chosen pair. */
|
|
426
|
+
export function surroundAdd(view, ch) {
|
|
427
|
+
const [open, close] = surroundPair(ch);
|
|
428
|
+
const { state } = view;
|
|
429
|
+
const changes = [];
|
|
430
|
+
for (const r of state.selection.ranges) {
|
|
431
|
+
changes.push({ from: r.from, insert: open });
|
|
432
|
+
changes.push({ from: r.to, insert: close });
|
|
433
|
+
}
|
|
434
|
+
const changeSet = state.changes(changes);
|
|
435
|
+
view.dispatch({
|
|
436
|
+
changes: changeSet,
|
|
437
|
+
selection: state.selection.map(changeSet),
|
|
438
|
+
effects: helixEffect.of(settle),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/** md{char}: remove the surrounding pair around each selection. */
|
|
442
|
+
export function surroundDelete(view, ch) {
|
|
443
|
+
const [open, close] = surroundPair(ch);
|
|
444
|
+
const { state } = view;
|
|
445
|
+
const changes = [];
|
|
446
|
+
for (const r of state.selection.ranges) {
|
|
447
|
+
const openPos = M.findOpen(state, r.head, open, close);
|
|
448
|
+
const closePos = openPos === null ? null : M.findClose(state, openPos, open, close);
|
|
449
|
+
if (openPos === null || closePos === null)
|
|
450
|
+
continue;
|
|
451
|
+
changes.push({ from: openPos, to: openPos + 1, insert: "" });
|
|
452
|
+
changes.push({ from: closePos, to: closePos + 1, insert: "" });
|
|
453
|
+
}
|
|
454
|
+
if (!changes.length)
|
|
455
|
+
return;
|
|
456
|
+
view.dispatch({ changes, effects: helixEffect.of(settle) });
|
|
457
|
+
}
|
|
458
|
+
/** mr{from}{to}: replace the surrounding pair. */
|
|
459
|
+
export function surroundReplace(view, fromCh, toCh) {
|
|
460
|
+
const [open, close] = surroundPair(fromCh);
|
|
461
|
+
const [newOpen, newClose] = surroundPair(toCh);
|
|
462
|
+
const { state } = view;
|
|
463
|
+
const changes = [];
|
|
464
|
+
for (const r of state.selection.ranges) {
|
|
465
|
+
const openPos = M.findOpen(state, r.head, open, close);
|
|
466
|
+
const closePos = openPos === null ? null : M.findClose(state, openPos, open, close);
|
|
467
|
+
if (openPos === null || closePos === null)
|
|
468
|
+
continue;
|
|
469
|
+
changes.push({ from: openPos, to: openPos + 1, insert: newOpen });
|
|
470
|
+
changes.push({ from: closePos, to: closePos + 1, insert: newClose });
|
|
471
|
+
}
|
|
472
|
+
if (!changes.length)
|
|
473
|
+
return;
|
|
474
|
+
view.dispatch({ changes, effects: helixEffect.of(settle) });
|
|
475
|
+
}
|
|
476
|
+
// --- search ------------------------------------------------------------------
|
|
477
|
+
export function runSearch(view, query, reverse) {
|
|
478
|
+
if (!query)
|
|
479
|
+
return;
|
|
480
|
+
view.dispatch({ effects: setSearchQuery.of(new SearchQuery({ search: query, regexp: true })) });
|
|
481
|
+
(reverse ? findPrevious : findNext)(view);
|
|
482
|
+
}
|
|
483
|
+
export function searchNext(view, reverse) {
|
|
484
|
+
(reverse ? findPrevious : findNext)(view);
|
|
485
|
+
}
|
|
486
|
+
export function searchSelection(view, reverse) {
|
|
487
|
+
const text = view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to);
|
|
488
|
+
if (!text)
|
|
489
|
+
return;
|
|
490
|
+
view.dispatch({
|
|
491
|
+
effects: setSearchQuery.of(new SearchQuery({ search: text, regexp: false })),
|
|
492
|
+
});
|
|
493
|
+
(reverse ? findPrevious : findNext)(view);
|
|
494
|
+
}
|
|
495
|
+
// --- view scrolling ----------------------------------------------------------
|
|
496
|
+
export function scrollTo(view, y) {
|
|
497
|
+
const pos = view.state.selection.main.head;
|
|
498
|
+
view.dispatch({ effects: EditorView.scrollIntoView(pos, { y }) });
|
|
499
|
+
}
|
|
500
|
+
export function halfPage(view, forward, extend) {
|
|
501
|
+
const lineHeight = view.defaultLineHeight || 16;
|
|
502
|
+
const lines = Math.max(1, Math.floor(view.dom.clientHeight / lineHeight / 2));
|
|
503
|
+
moveVertical(view, forward, { extend, count: lines });
|
|
504
|
+
}
|
|
505
|
+
export { restMode };
|
|
506
|
+
//# sourceMappingURL=commands.js.map
|