@difizen/libro-codemirror 0.0.2-alpha.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/LICENSE +21 -0
- package/README.md +0 -0
- package/es/auto-complete/closebrackets.d.ts +12 -0
- package/es/auto-complete/closebrackets.d.ts.map +1 -0
- package/es/auto-complete/closebrackets.js +408 -0
- package/es/auto-complete/completion.d.ts +57 -0
- package/es/auto-complete/completion.d.ts.map +1 -0
- package/es/auto-complete/completion.js +265 -0
- package/es/auto-complete/config.d.ts +22 -0
- package/es/auto-complete/config.d.ts.map +1 -0
- package/es/auto-complete/config.js +44 -0
- package/es/auto-complete/filter.d.ts +13 -0
- package/es/auto-complete/filter.d.ts.map +1 -0
- package/es/auto-complete/filter.js +191 -0
- package/es/auto-complete/index.d.ts +17 -0
- package/es/auto-complete/index.d.ts.map +1 -0
- package/es/auto-complete/index.js +107 -0
- package/es/auto-complete/snippet.d.ts +14 -0
- package/es/auto-complete/snippet.d.ts.map +1 -0
- package/es/auto-complete/snippet.js +447 -0
- package/es/auto-complete/state.d.ts +63 -0
- package/es/auto-complete/state.d.ts.map +1 -0
- package/es/auto-complete/state.js +452 -0
- package/es/auto-complete/theme.d.ts +6 -0
- package/es/auto-complete/theme.d.ts.map +1 -0
- package/es/auto-complete/theme.js +151 -0
- package/es/auto-complete/tooltip.d.ts +5 -0
- package/es/auto-complete/tooltip.d.ts.map +1 -0
- package/es/auto-complete/tooltip.js +365 -0
- package/es/auto-complete/view.d.ts +43 -0
- package/es/auto-complete/view.d.ts.map +1 -0
- package/es/auto-complete/view.js +372 -0
- package/es/auto-complete/word.d.ts +3 -0
- package/es/auto-complete/word.d.ts.map +1 -0
- package/es/auto-complete/word.js +119 -0
- package/es/completion.d.ts +6 -0
- package/es/completion.d.ts.map +1 -0
- package/es/completion.js +84 -0
- package/es/config.d.ts +184 -0
- package/es/config.d.ts.map +1 -0
- package/es/config.js +473 -0
- package/es/editor.d.ts +361 -0
- package/es/editor.d.ts.map +1 -0
- package/es/editor.js +1126 -0
- package/es/factory.d.ts +3 -0
- package/es/factory.d.ts.map +1 -0
- package/es/factory.js +12 -0
- package/es/hyperlink.d.ts +15 -0
- package/es/hyperlink.d.ts.map +1 -0
- package/es/hyperlink.js +120 -0
- package/es/indent.d.ts +8 -0
- package/es/indent.d.ts.map +1 -0
- package/es/indent.js +58 -0
- package/es/indentation-markers/config.d.ts +17 -0
- package/es/indentation-markers/config.d.ts.map +1 -0
- package/es/indentation-markers/config.js +10 -0
- package/es/indentation-markers/index.d.ts +3 -0
- package/es/indentation-markers/index.d.ts.map +1 -0
- package/es/indentation-markers/index.js +160 -0
- package/es/indentation-markers/map.d.ts +77 -0
- package/es/indentation-markers/map.d.ts.map +1 -0
- package/es/indentation-markers/map.js +265 -0
- package/es/indentation-markers/utils.d.ts +27 -0
- package/es/indentation-markers/utils.d.ts.map +1 -0
- package/es/indentation-markers/utils.js +91 -0
- package/es/index.d.ts +11 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +10 -0
- package/es/libro-icon.d.ts +3 -0
- package/es/libro-icon.d.ts.map +1 -0
- package/es/libro-icon.js +2 -0
- package/es/mimetype.d.ts +22 -0
- package/es/mimetype.d.ts.map +1 -0
- package/es/mimetype.js +59 -0
- package/es/mode.d.ts +86 -0
- package/es/mode.d.ts.map +1 -0
- package/es/mode.js +284 -0
- package/es/monitor.d.ts +32 -0
- package/es/monitor.d.ts.map +1 -0
- package/es/monitor.js +129 -0
- package/es/python-lang.d.ts +3 -0
- package/es/python-lang.d.ts.map +1 -0
- package/es/python-lang.js +7 -0
- package/es/style/base.css +131 -0
- package/es/style/theme.css +12 -0
- package/es/style/variables.css +403 -0
- package/es/theme.d.ts +35 -0
- package/es/theme.d.ts.map +1 -0
- package/es/theme.js +225 -0
- package/es/tooltip.d.ts +10 -0
- package/es/tooltip.d.ts.map +1 -0
- package/es/tooltip.js +170 -0
- package/package.json +74 -0
- package/src/auto-complete/README.md +71 -0
- package/src/auto-complete/closebrackets.ts +423 -0
- package/src/auto-complete/completion.ts +345 -0
- package/src/auto-complete/config.ts +101 -0
- package/src/auto-complete/filter.ts +215 -0
- package/src/auto-complete/index.ts +112 -0
- package/src/auto-complete/snippet.ts +394 -0
- package/src/auto-complete/state.ts +472 -0
- package/src/auto-complete/theme.ts +126 -0
- package/src/auto-complete/tooltip.ts +386 -0
- package/src/auto-complete/view.ts +343 -0
- package/src/auto-complete/word.ts +118 -0
- package/src/completion.ts +61 -0
- package/src/config.ts +689 -0
- package/src/editor.ts +1078 -0
- package/src/factory.ts +10 -0
- package/src/hyperlink.ts +95 -0
- package/src/indent.ts +69 -0
- package/src/indentation-markers/config.ts +31 -0
- package/src/indentation-markers/index.ts +192 -0
- package/src/indentation-markers/map.ts +273 -0
- package/src/indentation-markers/utils.ts +84 -0
- package/src/index.ts +11 -0
- package/src/libro-icon.ts +4 -0
- package/src/mimetype.ts +49 -0
- package/src/mode.ts +269 -0
- package/src/monitor.ts +105 -0
- package/src/python-lang.ts +7 -0
- package/src/style/base.css +129 -0
- package/src/style/theme.css +12 -0
- package/src/style/variables.css +405 -0
- package/src/theme.ts +231 -0
- package/src/tooltip.ts +145 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
import type { Extension, EditorState, StateEffect } from '@codemirror/state';
|
|
3
|
+
import { Prec } from '@codemirror/state';
|
|
4
|
+
import type { KeyBinding } from '@codemirror/view';
|
|
5
|
+
import { keymap } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
import { indentOrCompletion, indentOrTooltip } from '../indent.js';
|
|
8
|
+
|
|
9
|
+
import type { Completion, Option } from './completion.js';
|
|
10
|
+
import type { CompletionConfig } from './config.js';
|
|
11
|
+
import { completionConfig } from './config.js';
|
|
12
|
+
import { completionState, State, setSelectedEffect } from './state.js';
|
|
13
|
+
import { baseTheme } from './theme.js';
|
|
14
|
+
import {
|
|
15
|
+
completionPlugin,
|
|
16
|
+
moveCompletionSelection,
|
|
17
|
+
acceptCompletion,
|
|
18
|
+
closeCompletion,
|
|
19
|
+
} from './view.js';
|
|
20
|
+
|
|
21
|
+
export * from './snippet.js';
|
|
22
|
+
export * from './completion.js';
|
|
23
|
+
export * from './view.js';
|
|
24
|
+
export * from './word.js';
|
|
25
|
+
export * from './closebrackets.js';
|
|
26
|
+
|
|
27
|
+
/// Returns an extension that enables autocompletion.
|
|
28
|
+
export function autocompletion(config: CompletionConfig = {}): Extension {
|
|
29
|
+
return [
|
|
30
|
+
completionState,
|
|
31
|
+
completionConfig.of(config),
|
|
32
|
+
completionPlugin,
|
|
33
|
+
completionKeymapExt,
|
|
34
|
+
baseTheme,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Basic keybindings for autocompletion.
|
|
39
|
+
///
|
|
40
|
+
/// - Ctrl-Space: [`startCompletion`](#autocomplete.startCompletion)
|
|
41
|
+
/// - Escape: [`closeCompletion`](#autocomplete.closeCompletion)
|
|
42
|
+
/// - ArrowDown: [`moveCompletionSelection`](#autocomplete.moveCompletionSelection)`(true)`
|
|
43
|
+
/// - ArrowUp: [`moveCompletionSelection`](#autocomplete.moveCompletionSelection)`(false)`
|
|
44
|
+
/// - PageDown: [`moveCompletionSelection`](#autocomplete.moveCompletionSelection)`(true, "page")`
|
|
45
|
+
/// - PageDown: [`moveCompletionSelection`](#autocomplete.moveCompletionSelection)`(true, "page")`
|
|
46
|
+
/// - Enter: [`acceptCompletion`](#autocomplete.acceptCompletion)
|
|
47
|
+
export const completionKeymap: readonly KeyBinding[] = [
|
|
48
|
+
{ key: 'Tab', run: indentOrCompletion, shift: indentOrTooltip },
|
|
49
|
+
{ key: 'Escape', run: closeCompletion },
|
|
50
|
+
{ key: 'ArrowDown', run: moveCompletionSelection(true) },
|
|
51
|
+
{ key: 'ArrowUp', run: moveCompletionSelection(false) },
|
|
52
|
+
{ key: 'PageDown', run: moveCompletionSelection(true, 'page') },
|
|
53
|
+
{ key: 'PageUp', run: moveCompletionSelection(false, 'page') },
|
|
54
|
+
{ key: 'Enter', run: acceptCompletion },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const completionKeymapExt = Prec.highest(
|
|
58
|
+
keymap.computeN([completionConfig], (state) =>
|
|
59
|
+
state.facet(completionConfig).defaultKeymap ? [completionKeymap] : [],
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/// Get the current completion status. When completions are available,
|
|
64
|
+
/// this will return `"active"`. When completions are pending (in the
|
|
65
|
+
/// process of being queried), this returns `"pending"`. Otherwise, it
|
|
66
|
+
/// returns `null`.
|
|
67
|
+
export function completionStatus(state: EditorState): null | 'active' | 'pending' {
|
|
68
|
+
const cState = state.field(completionState, false);
|
|
69
|
+
return cState && cState.active.some((a) => a.state === State.Pending)
|
|
70
|
+
? 'pending'
|
|
71
|
+
: cState && cState.active.some((a) => a.state !== State.Inactive)
|
|
72
|
+
? 'active'
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const completionArrayCache: WeakMap<readonly Option[], readonly Completion[]> =
|
|
77
|
+
new WeakMap();
|
|
78
|
+
|
|
79
|
+
/// Returns the available completions as an array.
|
|
80
|
+
export function currentCompletions(state: EditorState): readonly Completion[] {
|
|
81
|
+
const open = state.field(completionState, false)?.open;
|
|
82
|
+
if (!open) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
let completions = completionArrayCache.get(open.options);
|
|
86
|
+
if (!completions) {
|
|
87
|
+
completionArrayCache.set(
|
|
88
|
+
open.options,
|
|
89
|
+
(completions = open.options.map((o) => o.completion)),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return completions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Return the currently selected completion, if any.
|
|
96
|
+
export function selectedCompletion(state: EditorState): Completion | null {
|
|
97
|
+
const open = state.field(completionState, false)?.open;
|
|
98
|
+
return open && open.selected >= 0 ? open.options[open.selected].completion : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Returns the currently selected position in the active completion
|
|
102
|
+
/// list, or null if no completions are active.
|
|
103
|
+
export function selectedCompletionIndex(state: EditorState): number | null {
|
|
104
|
+
const open = state.field(completionState, false)?.open;
|
|
105
|
+
return open && open.selected >= 0 ? open.selected : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Create an effect that can be attached to a transaction to change
|
|
109
|
+
/// the currently selected completion.
|
|
110
|
+
export function setSelectedCompletion(index: number): StateEffect<unknown> {
|
|
111
|
+
return setSelectedEffect.of(index);
|
|
112
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
3
|
+
/* eslint-disable prefer-const */
|
|
4
|
+
/* eslint-disable no-param-reassign */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-parameter-properties */
|
|
6
|
+
/* eslint-disable @typescript-eslint/parameter-properties */
|
|
7
|
+
import { indentUnit } from '@codemirror/language';
|
|
8
|
+
import type {
|
|
9
|
+
ChangeDesc,
|
|
10
|
+
EditorState,
|
|
11
|
+
Transaction,
|
|
12
|
+
TransactionSpec,
|
|
13
|
+
StateCommand,
|
|
14
|
+
} from '@codemirror/state';
|
|
15
|
+
import {
|
|
16
|
+
StateField,
|
|
17
|
+
StateEffect,
|
|
18
|
+
EditorSelection,
|
|
19
|
+
Text,
|
|
20
|
+
Prec,
|
|
21
|
+
Facet,
|
|
22
|
+
MapMode,
|
|
23
|
+
} from '@codemirror/state';
|
|
24
|
+
import type { DecorationSet, KeyBinding } from '@codemirror/view';
|
|
25
|
+
import { Decoration, WidgetType, EditorView, keymap } from '@codemirror/view';
|
|
26
|
+
|
|
27
|
+
import type { Completion } from './completion.js';
|
|
28
|
+
import { baseTheme } from './theme.js';
|
|
29
|
+
|
|
30
|
+
class FieldPos {
|
|
31
|
+
constructor(
|
|
32
|
+
public field: number,
|
|
33
|
+
readonly line: number,
|
|
34
|
+
public from: number,
|
|
35
|
+
public to: number,
|
|
36
|
+
) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class FieldRange {
|
|
40
|
+
constructor(
|
|
41
|
+
readonly field: number,
|
|
42
|
+
readonly from: number,
|
|
43
|
+
readonly to: number,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
map(changes: ChangeDesc) {
|
|
47
|
+
const from = changes.mapPos(this.from, -1, MapMode.TrackDel);
|
|
48
|
+
const to = changes.mapPos(this.to, 1, MapMode.TrackDel);
|
|
49
|
+
return from === null || to === null ? null : new FieldRange(this.field, from, to);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class Snippet {
|
|
54
|
+
constructor(
|
|
55
|
+
readonly lines: readonly string[],
|
|
56
|
+
readonly fieldPositions: readonly FieldPos[],
|
|
57
|
+
) {}
|
|
58
|
+
|
|
59
|
+
instantiate(state: EditorState, pos: number) {
|
|
60
|
+
const text = [],
|
|
61
|
+
lineStart = [pos];
|
|
62
|
+
const lineObj = state.doc.lineAt(pos),
|
|
63
|
+
baseIndent = /^\s*/.exec(lineObj.text)![0];
|
|
64
|
+
for (let line of this.lines) {
|
|
65
|
+
if (text.length) {
|
|
66
|
+
let indent = baseIndent,
|
|
67
|
+
tabs = /^\t*/.exec(line)![0].length;
|
|
68
|
+
for (let i = 0; i < tabs; i++) {
|
|
69
|
+
indent += state.facet(indentUnit);
|
|
70
|
+
}
|
|
71
|
+
lineStart.push(pos + indent.length - tabs);
|
|
72
|
+
line = indent + line.slice(tabs);
|
|
73
|
+
}
|
|
74
|
+
text.push(line);
|
|
75
|
+
pos += line.length + 1;
|
|
76
|
+
}
|
|
77
|
+
const ranges = this.fieldPositions.map(
|
|
78
|
+
(pos) =>
|
|
79
|
+
new FieldRange(
|
|
80
|
+
pos.field,
|
|
81
|
+
lineStart[pos.line] + pos.from,
|
|
82
|
+
lineStart[pos.line] + pos.to,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
return { text, ranges };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static parse(template: string) {
|
|
89
|
+
const fields: { seq: number | null; name: string }[] = [];
|
|
90
|
+
let lines = [],
|
|
91
|
+
positions = [],
|
|
92
|
+
m;
|
|
93
|
+
for (let line of template.split(/\r\n?|\n/)) {
|
|
94
|
+
while ((m = /[#$]\{(?:(\d+)(?::([^}]*))?|([^}]*))\}/.exec(line))) {
|
|
95
|
+
let seq = m[1] ? +m[1] : null,
|
|
96
|
+
name = m[2] || m[3] || '',
|
|
97
|
+
found = -1;
|
|
98
|
+
for (let i = 0; i < fields.length; i++) {
|
|
99
|
+
if (
|
|
100
|
+
seq !== null
|
|
101
|
+
? fields[i].seq === seq
|
|
102
|
+
: name
|
|
103
|
+
? fields[i].name === name
|
|
104
|
+
: false
|
|
105
|
+
) {
|
|
106
|
+
found = i;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (found < 0) {
|
|
110
|
+
let i = 0;
|
|
111
|
+
while (
|
|
112
|
+
i < fields.length &&
|
|
113
|
+
(seq === null || (fields[i].seq !== null && fields[i].seq! < seq))
|
|
114
|
+
) {
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
fields.splice(i, 0, { seq, name });
|
|
118
|
+
found = i;
|
|
119
|
+
for (const pos of positions) {
|
|
120
|
+
if (pos.field >= found) {
|
|
121
|
+
pos.field++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
positions.push(
|
|
126
|
+
new FieldPos(found, lines.length, m.index, m.index + name.length),
|
|
127
|
+
);
|
|
128
|
+
line = line.slice(0, m.index) + name + line.slice(m.index + m[0].length);
|
|
129
|
+
}
|
|
130
|
+
for (let esc; (esc = /([$#])\\{/.exec(line)); ) {
|
|
131
|
+
line =
|
|
132
|
+
line.slice(0, esc.index) +
|
|
133
|
+
esc[1] +
|
|
134
|
+
'{' +
|
|
135
|
+
line.slice(esc.index + esc[0].length);
|
|
136
|
+
for (const pos of positions) {
|
|
137
|
+
if (pos.line === lines.length && pos.from > esc.index) {
|
|
138
|
+
pos.from--;
|
|
139
|
+
pos.to--;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push(line);
|
|
144
|
+
}
|
|
145
|
+
return new Snippet(lines, positions);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fieldMarker = Decoration.widget({
|
|
150
|
+
widget: new (class extends WidgetType {
|
|
151
|
+
toDOM() {
|
|
152
|
+
const span = document.createElement('span');
|
|
153
|
+
span.className = 'cm-snippetFieldPosition';
|
|
154
|
+
return span;
|
|
155
|
+
}
|
|
156
|
+
override ignoreEvent() {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
})(),
|
|
160
|
+
});
|
|
161
|
+
const fieldRange = Decoration.mark({ class: 'cm-snippetField' });
|
|
162
|
+
|
|
163
|
+
class ActiveSnippet {
|
|
164
|
+
deco: DecorationSet;
|
|
165
|
+
|
|
166
|
+
constructor(
|
|
167
|
+
readonly ranges: readonly FieldRange[],
|
|
168
|
+
readonly active: number,
|
|
169
|
+
) {
|
|
170
|
+
this.deco = Decoration.set(
|
|
171
|
+
ranges.map((r) =>
|
|
172
|
+
(r.from === r.to ? fieldMarker : fieldRange).range(r.from, r.to),
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
map(changes: ChangeDesc) {
|
|
178
|
+
const ranges = [];
|
|
179
|
+
for (const r of this.ranges) {
|
|
180
|
+
const mapped = r.map(changes);
|
|
181
|
+
if (!mapped) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
ranges.push(mapped);
|
|
185
|
+
}
|
|
186
|
+
return new ActiveSnippet(ranges, this.active);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
selectionInsideField(sel: EditorSelection) {
|
|
190
|
+
return sel.ranges.every((range) =>
|
|
191
|
+
this.ranges.some(
|
|
192
|
+
(r) => r.field === this.active && r.from <= range.from && r.to >= range.to,
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const setActive = StateEffect.define<ActiveSnippet | null>({
|
|
199
|
+
map(value, changes) {
|
|
200
|
+
return value && value.map(changes);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const moveToField = StateEffect.define<number>();
|
|
205
|
+
|
|
206
|
+
const snippetState = StateField.define<ActiveSnippet | null>({
|
|
207
|
+
create() {
|
|
208
|
+
return null;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
update(value, tr) {
|
|
212
|
+
for (const effect of tr.effects) {
|
|
213
|
+
if (effect.is(setActive)) {
|
|
214
|
+
return effect.value;
|
|
215
|
+
}
|
|
216
|
+
if (effect.is(moveToField) && value) {
|
|
217
|
+
return new ActiveSnippet(value.ranges, effect.value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (value && tr.docChanged) {
|
|
221
|
+
value = value.map(tr.changes);
|
|
222
|
+
}
|
|
223
|
+
if (value && tr.selection && !value.selectionInsideField(tr.selection)) {
|
|
224
|
+
value = null;
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
provide: (f) =>
|
|
230
|
+
EditorView.decorations.from(f, (val) => (val ? val.deco : Decoration.none)),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
function fieldSelection(ranges: readonly FieldRange[], field: number) {
|
|
234
|
+
return EditorSelection.create(
|
|
235
|
+
ranges
|
|
236
|
+
.filter((r) => r.field === field)
|
|
237
|
+
.map((r) => EditorSelection.range(r.from, r.to)),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Convert a snippet template to a function that can
|
|
242
|
+
/// [apply](#autocomplete.Completion.apply) it. Snippets are written
|
|
243
|
+
/// using syntax like this:
|
|
244
|
+
///
|
|
245
|
+
/// "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}"
|
|
246
|
+
///
|
|
247
|
+
/// Each `${}` placeholder (you may also use `#{}`) indicates a field
|
|
248
|
+
/// that the user can fill in. Its name, if any, will be the default
|
|
249
|
+
/// content for the field.
|
|
250
|
+
///
|
|
251
|
+
/// When the snippet is activated by calling the returned function,
|
|
252
|
+
/// the code is inserted at the given position. Newlines in the
|
|
253
|
+
/// template are indented by the indentation of the start line, plus
|
|
254
|
+
/// one [indent unit](#language.indentUnit) per tab character after
|
|
255
|
+
/// the newline.
|
|
256
|
+
///
|
|
257
|
+
/// On activation, (all instances of) the first field are selected.
|
|
258
|
+
/// The user can move between fields with Tab and Shift-Tab as long as
|
|
259
|
+
/// the fields are active. Moving to the last field or moving the
|
|
260
|
+
/// cursor out of the current field deactivates the fields.
|
|
261
|
+
///
|
|
262
|
+
/// The order of fields defaults to textual order, but you can add
|
|
263
|
+
/// numbers to placeholders (`${1}` or `${1:defaultText}`) to provide
|
|
264
|
+
/// a custom order.
|
|
265
|
+
///
|
|
266
|
+
/// To include a literal `${` or `#{` in your template, put a
|
|
267
|
+
/// backslash after the dollar or hash and before the brace (`$\\{`).
|
|
268
|
+
/// This will be removed and the sequence will not be interpreted as a
|
|
269
|
+
/// placeholder.
|
|
270
|
+
export function snippet(template: string) {
|
|
271
|
+
const snippet = Snippet.parse(template);
|
|
272
|
+
return (
|
|
273
|
+
editor: { state: EditorState; dispatch: (tr: Transaction) => void },
|
|
274
|
+
_completion: Completion,
|
|
275
|
+
from: number,
|
|
276
|
+
to: number,
|
|
277
|
+
) => {
|
|
278
|
+
const { text, ranges } = snippet.instantiate(editor.state, from);
|
|
279
|
+
const spec: TransactionSpec = {
|
|
280
|
+
changes: { from, to, insert: Text.of(text) },
|
|
281
|
+
scrollIntoView: true,
|
|
282
|
+
};
|
|
283
|
+
if (ranges.length) {
|
|
284
|
+
spec.selection = fieldSelection(ranges, 0);
|
|
285
|
+
}
|
|
286
|
+
if (ranges.length > 1) {
|
|
287
|
+
const active = new ActiveSnippet(ranges, 0);
|
|
288
|
+
const effects: StateEffect<unknown>[] = (spec.effects = [setActive.of(active)]);
|
|
289
|
+
if (editor.state.field(snippetState, false) === undefined) {
|
|
290
|
+
effects.push(
|
|
291
|
+
StateEffect.appendConfig.of([
|
|
292
|
+
snippetState,
|
|
293
|
+
addSnippetKeymap,
|
|
294
|
+
snippetPointerHandler,
|
|
295
|
+
baseTheme,
|
|
296
|
+
]),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
editor.dispatch(editor.state.update(spec));
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function moveField(dir: 1 | -1): StateCommand {
|
|
305
|
+
return ({ state, dispatch }) => {
|
|
306
|
+
const active = state.field(snippetState, false);
|
|
307
|
+
if (!active || (dir < 0 && active.active === 0)) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const next = active.active + dir,
|
|
311
|
+
last = dir > 0 && !active.ranges.some((r) => r.field === next + dir);
|
|
312
|
+
dispatch(
|
|
313
|
+
state.update({
|
|
314
|
+
selection: fieldSelection(active.ranges, next),
|
|
315
|
+
effects: setActive.of(last ? null : new ActiveSnippet(active.ranges, next)),
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
return true;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// A command that clears the active snippet, if any.
|
|
323
|
+
export const clearSnippet: StateCommand = ({ state, dispatch }) => {
|
|
324
|
+
const active = state.field(snippetState, false);
|
|
325
|
+
if (!active) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
dispatch(state.update({ effects: setActive.of(null) }));
|
|
329
|
+
return true;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/// Move to the next snippet field, if available.
|
|
333
|
+
export const nextSnippetField = moveField(1);
|
|
334
|
+
|
|
335
|
+
/// Move to the previous snippet field, if available.
|
|
336
|
+
export const prevSnippetField = moveField(-1);
|
|
337
|
+
|
|
338
|
+
const defaultSnippetKeymap = [
|
|
339
|
+
{ key: 'Tab', run: nextSnippetField, shift: prevSnippetField },
|
|
340
|
+
{ key: 'Escape', run: clearSnippet },
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
/// A facet that can be used to configure the key bindings used by
|
|
344
|
+
/// snippets. The default binds Tab to
|
|
345
|
+
/// [`nextSnippetField`](#autocomplete.nextSnippetField), Shift-Tab to
|
|
346
|
+
/// [`prevSnippetField`](#autocomplete.prevSnippetField), and Escape
|
|
347
|
+
/// to [`clearSnippet`](#autocomplete.clearSnippet).
|
|
348
|
+
export const snippetKeymap = Facet.define<readonly KeyBinding[], readonly KeyBinding[]>(
|
|
349
|
+
{
|
|
350
|
+
combine(maps) {
|
|
351
|
+
return maps.length ? maps[0] : defaultSnippetKeymap;
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const addSnippetKeymap = Prec.highest(
|
|
357
|
+
keymap.compute([snippetKeymap], (state) => state.facet(snippetKeymap)),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
/// Create a completion from a snippet. Returns an object with the
|
|
361
|
+
/// properties from `completion`, plus an `apply` function that
|
|
362
|
+
/// applies the snippet.
|
|
363
|
+
export function snippetCompletion(
|
|
364
|
+
template: string,
|
|
365
|
+
completion: Completion,
|
|
366
|
+
): Completion {
|
|
367
|
+
return { ...completion, apply: snippet(template) };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const snippetPointerHandler = EditorView.domEventHandlers({
|
|
371
|
+
mousedown(event, view) {
|
|
372
|
+
let active = view.state.field(snippetState, false),
|
|
373
|
+
pos: number | null;
|
|
374
|
+
if (
|
|
375
|
+
!active ||
|
|
376
|
+
(pos = view.posAtCoords({ x: event.clientX, y: event.clientY })) === null
|
|
377
|
+
) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
const match = active.ranges.find((r) => r.from <= pos! && r.to >= pos!);
|
|
381
|
+
if (!match || match.field === active.active) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
view.dispatch({
|
|
385
|
+
selection: fieldSelection(active.ranges, match.field),
|
|
386
|
+
effects: setActive.of(
|
|
387
|
+
active.ranges.some((r) => r.field > match.field)
|
|
388
|
+
? new ActiveSnippet(active.ranges, match.field)
|
|
389
|
+
: null,
|
|
390
|
+
),
|
|
391
|
+
});
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
});
|