@bhsd/codemirror-mediawiki 2.28.2 → 2.29.1

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/dist/config.js ADDED
@@ -0,0 +1,178 @@
1
+ // src/config.ts
2
+ import { tags, Tag } from "@lezer/highlight";
3
+
4
+ // ../wikiparser-node/config/default.json
5
+ var html = [
6
+ [
7
+ "b",
8
+ "bdi",
9
+ "del",
10
+ "i",
11
+ "ins",
12
+ "u",
13
+ "font",
14
+ "big",
15
+ "small",
16
+ "sub",
17
+ "sup",
18
+ "h1",
19
+ "h2",
20
+ "h3",
21
+ "h4",
22
+ "h5",
23
+ "h6",
24
+ "cite",
25
+ "code",
26
+ "em",
27
+ "s",
28
+ "strike",
29
+ "strong",
30
+ "tt",
31
+ "var",
32
+ "div",
33
+ "center",
34
+ "blockquote",
35
+ "ol",
36
+ "ul",
37
+ "dl",
38
+ "table",
39
+ "caption",
40
+ "pre",
41
+ "ruby",
42
+ "rb",
43
+ "rp",
44
+ "rt",
45
+ "rtc",
46
+ "p",
47
+ "span",
48
+ "abbr",
49
+ "dfn",
50
+ "kbd",
51
+ "samp",
52
+ "data",
53
+ "time",
54
+ "mark",
55
+ "tr",
56
+ "td",
57
+ "th",
58
+ "q",
59
+ "bdo"
60
+ ],
61
+ [
62
+ "li",
63
+ "dt",
64
+ "dd"
65
+ ],
66
+ [
67
+ "br",
68
+ "wbr",
69
+ "hr",
70
+ "meta",
71
+ "link",
72
+ "img"
73
+ ]
74
+ ];
75
+
76
+ // src/config.ts
77
+ /**
78
+ * @file Configuration for the MediaWiki highlighting mode for CodeMirror.
79
+ * @author MusikAnimal and others
80
+ * @license GPL-2.0-or-later
81
+ * @see https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror
82
+ */
83
+ var htmlTags = html.flat();
84
+ var voidHtmlTags = html[2];
85
+ var selfClosingTags = html[1];
86
+ var tokens = {
87
+ apostrophes: "mw-apostrophes",
88
+ comment: "mw-comment",
89
+ convertBracket: "mw-convert-bracket",
90
+ convertDelimiter: "mw-convert-delimiter",
91
+ convertFlag: "mw-convert-flag",
92
+ convertLang: "mw-convert-lang",
93
+ doubleUnderscore: "mw-double-underscore",
94
+ em: "mw-em",
95
+ error: "mw-error",
96
+ extLink: "mw-extlink",
97
+ extLinkBracket: "mw-extlink-bracket",
98
+ extLinkProtocol: "mw-extlink-protocol",
99
+ extLinkText: "mw-extlink-text",
100
+ extTag: "mw-exttag",
101
+ extTagAttribute: "mw-exttag-attribute",
102
+ extTagAttributeValue: "mw-exttag-attribute-value",
103
+ extTagBracket: "mw-exttag-bracket",
104
+ extTagName: "mw-exttag-name",
105
+ fileDelimiter: "mw-file-delimiter",
106
+ fileText: "mw-file-text",
107
+ freeExtLink: "mw-free-extlink",
108
+ freeExtLinkProtocol: "mw-free-extlink-protocol",
109
+ hr: "mw-hr",
110
+ htmlEntity: "mw-entity",
111
+ htmlTagAttribute: "mw-htmltag-attribute",
112
+ htmlTagAttributeValue: "mw-htmltag-attribute-value",
113
+ htmlTagBracket: "mw-htmltag-bracket",
114
+ htmlTagName: "mw-htmltag-name",
115
+ imageParameter: "mw-image-parameter",
116
+ linkBracket: "mw-link-bracket",
117
+ linkDelimiter: "mw-link-delimiter",
118
+ linkPageName: "mw-link-pagename",
119
+ linkText: "mw-link-text",
120
+ linkToSection: "mw-link-tosection",
121
+ list: "mw-list",
122
+ magicLink: "mw-magic-link",
123
+ pageName: "mw-pagename",
124
+ parserFunction: "mw-parserfunction",
125
+ parserFunctionBracket: "mw-parserfunction-bracket",
126
+ parserFunctionDelimiter: "mw-parserfunction-delimiter",
127
+ parserFunctionName: "mw-parserfunction-name",
128
+ redirect: "mw-redirect",
129
+ section: "mw-section",
130
+ sectionHeader: "mw-section-header",
131
+ signature: "mw-signature",
132
+ skipFormatting: "mw-skipformatting",
133
+ strong: "mw-strong",
134
+ tableBracket: "mw-table-bracket",
135
+ tableCaption: "mw-table-caption",
136
+ tableDefinition: "mw-table-definition",
137
+ tableDefinitionValue: "mw-table-definition-value",
138
+ tableDelimiter: "mw-table-delimiter",
139
+ tableDelimiter2: "mw-table-delimiter2",
140
+ tableTd: "mw-table-td",
141
+ tableTh: "mw-table-th",
142
+ template: "mw-template",
143
+ templateArgumentName: "mw-template-argument-name",
144
+ templateBracket: "mw-template-bracket",
145
+ templateDelimiter: "mw-template-delimiter",
146
+ templateName: "mw-template-name",
147
+ templateVariable: "mw-templatevariable",
148
+ templateVariableBracket: "mw-templatevariable-bracket",
149
+ templateVariableDelimiter: "mw-templatevariable-delimiter",
150
+ templateVariableName: "mw-templatevariable-name"
151
+ };
152
+ var tokenTable = (() => {
153
+ const table = {
154
+ variable: tags.variableName,
155
+ "variable-2": tags.special(tags.variableName),
156
+ "string-2": tags.special(tags.string),
157
+ def: tags.definition(tags.variableName),
158
+ tag: tags.tagName,
159
+ attribute: tags.attributeName,
160
+ type: tags.typeName,
161
+ builtin: tags.standard(tags.variableName),
162
+ qualifier: tags.modifier,
163
+ error: tags.invalid,
164
+ header: tags.heading,
165
+ property: tags.propertyName
166
+ };
167
+ for (const className of Object.values(tokens)) {
168
+ table[className] = Tag.define();
169
+ }
170
+ return table;
171
+ })();
172
+ export {
173
+ htmlTags,
174
+ selfClosingTags,
175
+ tokenTable,
176
+ tokens,
177
+ voidHtmlTags
178
+ };
package/dist/css.js ADDED
@@ -0,0 +1,30 @@
1
+ import { cssLanguage, cssCompletionSource } from '@codemirror/lang-css';
2
+ import { LanguageSupport, syntaxTree } from '@codemirror/language';
3
+ export default (dialect) => new LanguageSupport(cssLanguage, cssLanguage.data.of({
4
+ autocomplete(context) {
5
+ const { state, pos } = context, node = syntaxTree(state).resolveInner(pos, -1), result = cssCompletionSource(context);
6
+ if (result) {
7
+ if (node.name === 'ValueName') {
8
+ const options = [{ label: 'revert', type: 'keyword' }, ...result.options];
9
+ let { prevSibling } = node;
10
+ while (prevSibling && prevSibling.name !== 'PropertyName') {
11
+ ({ prevSibling } = prevSibling);
12
+ }
13
+ if (prevSibling) {
14
+ for (let i = 0; i < options.length; i++) {
15
+ const option = options[i];
16
+ if (CSS.supports(state.sliceDoc(prevSibling.from, node.from) + option.label)) {
17
+ options.splice(i, 1, { ...option, boost: 50 });
18
+ }
19
+ }
20
+ }
21
+ result.options = options;
22
+ }
23
+ else if (dialect === 'sanitized-css') {
24
+ result.options = result.options.filter(({ type, label }) => type !== 'property'
25
+ || !label.startsWith('-') || label.endsWith('-user-select'));
26
+ }
27
+ }
28
+ return result;
29
+ },
30
+ }));
package/dist/escape.js ADDED
@@ -0,0 +1,35 @@
1
+ import { indentMore, indentLess } from '@codemirror/commands';
2
+ import { CodeMirror6 } from './codemirror';
3
+ const entity = { '"': 'quot', "'": 'apos', '<': 'lt', '>': 'gt', '&': 'amp', ' ': 'nbsp' };
4
+ /**
5
+ * 根据函数转换选中文本
6
+ * @param func 转换函数
7
+ * @param cmd 原命令
8
+ */
9
+ const convert = (func, cmd) => (view) => {
10
+ if (view.state.selection.ranges.some(range => !range.empty)) {
11
+ CodeMirror6.replaceSelections(view, func);
12
+ return true;
13
+ }
14
+ return cmd(view);
15
+ };
16
+ // eslint-disable-next-line @typescript-eslint/no-misused-spread
17
+ export const escapeHTML = (str) => [...str].map(c => {
18
+ if (c in entity) {
19
+ return `&${entity[c]};`;
20
+ }
21
+ const code = c.codePointAt(0);
22
+ return code < 256 ? `&#${code};` : `&#x${code.toString(16)};`;
23
+ }).join(''), escapeURI = (str) => {
24
+ if (str.includes('%')) {
25
+ try {
26
+ return decodeURIComponent(str);
27
+ }
28
+ catch { }
29
+ }
30
+ return encodeURIComponent(str);
31
+ };
32
+ export default [
33
+ { key: 'Mod-[', run: convert(escapeHTML, indentLess) },
34
+ { key: 'Mod-]', run: convert(escapeURI, indentMore) },
35
+ ];
package/dist/fold.js ADDED
@@ -0,0 +1,445 @@
1
+ import { showTooltip, keymap, GutterMarker, gutter, ViewPlugin } from '@codemirror/view';
2
+ import { StateField, RangeSetBuilder, RangeSet } from '@codemirror/state';
3
+ import { syntaxTree, ensureSyntaxTree, foldEffect, unfoldEffect, foldedRanges, unfoldAll, codeFolding, foldGutter, foldKeymap, foldState, language, } from '@codemirror/language';
4
+ import { getRegex } from '@bhsd/common';
5
+ import { tokens } from './config';
6
+ import { matchTag } from './matchTag';
7
+ const getExtRegex = getRegex(tag => new RegExp(`mw-tag-${tag}(?![a-z])`, 'u'));
8
+ const updateSelection = (pos, { to }) => Math.max(pos, to), updateAll = (pos, { from, to }) => from <= pos && to > pos ? to : pos;
9
+ /**
10
+ * Check if a SyntaxNode is among the specified components
11
+ * @param keys The keys of the tokens to check
12
+ */
13
+ const isComponent = (keys) => ({ name }) => keys.some(key => name.includes(tokens[key])),
14
+ /** Check if a SyntaxNode is a template bracket (`{{` or `}}`) */
15
+ isTemplateBracket = isComponent(['templateBracket', 'parserFunctionBracket']),
16
+ /** Check if a SyntaxNode is a template delimiter (`|` or `:`) */
17
+ isDelimiter = isComponent(['templateDelimiter', 'parserFunctionDelimiter']),
18
+ /**
19
+ * Check if a SyntaxNode is part of a template, except for the brackets
20
+ * @param node 语法树节点
21
+ */
22
+ isTemplate = (node) => /-(?:template|ext)[a-z\d-]+ground/u.test(node.name) && !isTemplateBracket(node),
23
+ /** Check if a SyntaxNode is an extension tag bracket (`<` or `>`) */
24
+ isExtBracket = isComponent(['extTagBracket']),
25
+ /**
26
+ * Check if a SyntaxNode is part of a extension tag
27
+ * @param node 语法树节点
28
+ */
29
+ isExt = (node) => node.name.includes('mw-tag-');
30
+ /**
31
+ * Update the stack of opening (+) or closing (-) brackets
32
+ * @param state
33
+ * @param node 语法树节点
34
+ */
35
+ export const braceStackUpdate = (state, node) => {
36
+ const brackets = state.sliceDoc(node.from, node.to);
37
+ return [brackets.split('{{').length - 1, 1 - brackets.split('}}').length];
38
+ };
39
+ /**
40
+ * 寻找可折叠的范围
41
+ * @param state
42
+ * @param posOrNode 字符位置或语法树节点
43
+ * @param tree 语法树
44
+ */
45
+ export const foldable = (state, posOrNode, tree) => {
46
+ if (typeof posOrNode === 'number') {
47
+ tree = ensureSyntaxTree(state, posOrNode); // eslint-disable-line no-param-reassign
48
+ }
49
+ if (!tree) {
50
+ return false;
51
+ }
52
+ let node;
53
+ if (typeof posOrNode === 'number') {
54
+ // Find the initial template node on both sides of the position
55
+ const left = tree.resolve(posOrNode, -1);
56
+ if (isTemplate(left)) {
57
+ node = left;
58
+ }
59
+ else {
60
+ const right = tree.resolve(posOrNode, 1);
61
+ node = isExt(left)
62
+ && left.name.split('mw-tag-').length > right.name.split('mw-tag-').length
63
+ ? left
64
+ : right;
65
+ }
66
+ }
67
+ else {
68
+ node = posOrNode;
69
+ }
70
+ if (!isTemplate(node)) {
71
+ // Not a template
72
+ if (isExt(node)) {
73
+ const { name } = node, [tag] = /^[a-z]+/u.exec(name.slice(name.lastIndexOf('mw-tag-') + 7)), regex = getExtRegex(tag);
74
+ let { nextSibling } = node;
75
+ while (nextSibling && !(isExtBracket(nextSibling) && !regex.test(nextSibling.name))) {
76
+ ({ nextSibling } = nextSibling);
77
+ }
78
+ if (nextSibling) { // The closing bracket of the current extension tag
79
+ return { from: matchTag(state, nextSibling.to).end.to, to: nextSibling.from };
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ let { prevSibling, nextSibling } = node,
85
+ /** The stack of opening (+) or closing (-) brackets */ stack = 1,
86
+ /** The first delimiter */ delimiter = isDelimiter(node) ? node : null,
87
+ /** The start of the closing bracket */ to = 0;
88
+ while (nextSibling) {
89
+ if (isTemplateBracket(nextSibling)) {
90
+ const [lbrace, rbrace] = braceStackUpdate(state, nextSibling);
91
+ stack += rbrace;
92
+ if (stack <= 0) {
93
+ // The closing bracket of the current template
94
+ to = nextSibling.from
95
+ + state.sliceDoc(nextSibling.from, nextSibling.to)
96
+ .split('}}').slice(0, stack - 1).join('}}').length;
97
+ break;
98
+ }
99
+ stack += lbrace;
100
+ }
101
+ else if (!delimiter && stack === 1 && isDelimiter(nextSibling)) {
102
+ // The first delimiter of the current template so far
103
+ delimiter = nextSibling;
104
+ }
105
+ ({ nextSibling } = nextSibling);
106
+ }
107
+ if (!nextSibling) {
108
+ // The closing bracket of the current template is missing
109
+ return false;
110
+ }
111
+ stack = -1;
112
+ while (prevSibling) {
113
+ if (isTemplateBracket(prevSibling)) {
114
+ const [lbrace, rbrace] = braceStackUpdate(state, prevSibling);
115
+ stack += lbrace;
116
+ if (stack >= 0) {
117
+ // The opening bracket of the current template
118
+ break;
119
+ }
120
+ stack += rbrace;
121
+ }
122
+ else if (stack === -1 && isDelimiter(prevSibling)) {
123
+ // The first delimiter of the current template so far
124
+ delimiter = prevSibling;
125
+ }
126
+ ({ prevSibling } = prevSibling);
127
+ }
128
+ const /** The end of the first delimiter */ from = delimiter?.to;
129
+ return from && from < to ? { from, to } : false;
130
+ };
131
+ /**
132
+ * 创建折叠提示
133
+ * @param state
134
+ */
135
+ const create = (state) => {
136
+ const { selection: { main: { head } } } = state, range = foldable(state, head);
137
+ if (range) {
138
+ const { from, to } = range;
139
+ let folded = false;
140
+ foldedRanges(state).between(from, to, (i, j) => {
141
+ if (i === from && j === to) {
142
+ folded = true;
143
+ }
144
+ });
145
+ return folded // eslint-disable-line @typescript-eslint/no-unnecessary-condition
146
+ ? null
147
+ : {
148
+ pos: head,
149
+ above: true,
150
+ create() {
151
+ const dom = document.createElement('div');
152
+ dom.className = 'cm-tooltip-fold';
153
+ dom.textContent = '\uff0d';
154
+ dom.title = state.phrase('Fold template or extension tag');
155
+ dom.dataset['from'] = String(from);
156
+ dom.dataset['to'] = String(to);
157
+ return { dom };
158
+ },
159
+ };
160
+ }
161
+ return null;
162
+ };
163
+ /**
164
+ * 执行折叠
165
+ * @param view
166
+ * @param effects 折叠
167
+ * @param anchor 光标位置
168
+ */
169
+ const execute = (view, effects, anchor) => {
170
+ if (effects.length > 0) {
171
+ view.dom.querySelector('.cm-tooltip-fold')?.remove();
172
+ // Fold the template(s) and update the cursor position
173
+ view.dispatch({ effects, selection: { anchor } });
174
+ return true;
175
+ }
176
+ return false;
177
+ };
178
+ /**
179
+ * The rightmost position of all selections, to be updated with folding
180
+ * @param state
181
+ */
182
+ const getAnchor = (state) => Math.max(...state.selection.ranges.map(({ to }) => to));
183
+ /**
184
+ * 折叠所有模板
185
+ * @param state
186
+ * @param tree 语法树
187
+ * @param effects 折叠
188
+ * @param node 语法树节点
189
+ * @param end 终止位置
190
+ * @param anchor 光标位置
191
+ * @param update 更新光标位置
192
+ */
193
+ const traverse = (state, tree, effects, node, end, anchor, update) => {
194
+ while (node && node.from <= end) {
195
+ /* eslint-disable no-param-reassign */
196
+ const range = foldable(state, node, tree);
197
+ if (range) {
198
+ effects.push(foldEffect.of(range));
199
+ node = tree.resolve(range.to, 1);
200
+ // Update the anchor with the end of the last folded range
201
+ anchor = update(anchor, range);
202
+ continue;
203
+ }
204
+ node = node.nextSibling;
205
+ /* eslint-enable no-param-reassign */
206
+ }
207
+ return anchor;
208
+ };
209
+ class FoldMarker extends GutterMarker {
210
+ constructor(open) {
211
+ super();
212
+ this.open = open;
213
+ }
214
+ eq(other) {
215
+ return this.open === other.open;
216
+ }
217
+ toDOM({ state }) {
218
+ const span = document.createElement('span');
219
+ span.textContent = this.open ? '⌄' : '›';
220
+ span.title = state.phrase(this.open ? 'Fold line' : 'Unfold line');
221
+ return span;
222
+ }
223
+ }
224
+ const canFold = new FoldMarker(true), canUnfold = new FoldMarker(false);
225
+ const findFold = ({ state }, line) => {
226
+ let found;
227
+ state.field(foldState, false)?.between(line.from, line.to, (from, to) => {
228
+ if (!found && to === line.to) {
229
+ found = { from, to };
230
+ }
231
+ });
232
+ return found;
233
+ };
234
+ export const foldableLine = ({ state, viewport: { to: end }, viewportLineBlocks }, { from: f, to: t }) => {
235
+ const tree = syntaxTree(state);
236
+ /**
237
+ * 获取标题层级
238
+ * @param pos 行首位置
239
+ */
240
+ const getLevel = (pos) => {
241
+ const { name } = tree.resolve(pos, 1);
242
+ return name.includes(tokens.sectionHeader) ? Number(/mw-section--(\d)/u.exec(name)[1]) : 7;
243
+ },
244
+ /**
245
+ * 获取表格语法
246
+ * @param from 行首位置
247
+ * @param to 行尾位置
248
+ */
249
+ getTable = (from, to) => {
250
+ const line = state.sliceDoc(from, to), bracket = /^\s*(?:(?::+\s*)?\{\||\|\})/u.exec(line)?.[0];
251
+ if (bracket) {
252
+ const { name } = tree.resolve(from + bracket.length, -1);
253
+ if (name.includes(tokens.tableBracket)) {
254
+ return bracket.endsWith('|}') ? -1 : 1;
255
+ }
256
+ }
257
+ return 0;
258
+ };
259
+ const level = getLevel(f);
260
+ if (level < 7) {
261
+ for (const { from } of viewportLineBlocks) {
262
+ if (from > f && getLevel(from) <= level) {
263
+ return t < from - 1 && { from: t, to: from - 1 };
264
+ }
265
+ }
266
+ return end === state.doc.length && end > t && { from: t, to: end };
267
+ }
268
+ else if (getTable(f, t) === 1) {
269
+ for (const { from, to } of viewportLineBlocks) {
270
+ if (from > f) {
271
+ const bracket = getTable(from, to);
272
+ if (bracket === -1) {
273
+ return t < from - 1 && { from: t, to: from - 1 };
274
+ }
275
+ else if (bracket === 1 || getLevel(from) < 7) {
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ }
281
+ return false;
282
+ };
283
+ const buildMarkers = (view) => {
284
+ const builder = new RangeSetBuilder();
285
+ for (const line of view.viewportLineBlocks) {
286
+ let mark;
287
+ if (findFold(view, line)) {
288
+ mark = canUnfold;
289
+ }
290
+ else if (foldableLine(view, line)) {
291
+ mark = canFold;
292
+ }
293
+ if (mark) {
294
+ builder.add(line.from, line.from, mark);
295
+ }
296
+ }
297
+ return builder.finish();
298
+ };
299
+ const markers = ViewPlugin.fromClass(class {
300
+ constructor(view) {
301
+ this.markers = buildMarkers(view);
302
+ }
303
+ update({ docChanged, viewportChanged, startState, state, view }) {
304
+ if (docChanged
305
+ || viewportChanged
306
+ || startState.facet(language) !== state.facet(language)
307
+ || startState.field(foldState, false) !== state.field(foldState, false)
308
+ || syntaxTree(startState) !== syntaxTree(state)) {
309
+ this.markers = buildMarkers(view);
310
+ }
311
+ }
312
+ });
313
+ const defaultFoldExtension = [foldGutter(), keymap.of(foldKeymap)];
314
+ export default [
315
+ (e = defaultFoldExtension) => e,
316
+ {
317
+ mediawiki: [
318
+ codeFolding({
319
+ placeholderDOM(view) {
320
+ const element = document.createElement('span');
321
+ element.textContent = '…';
322
+ element.setAttribute('aria-label', 'folded code');
323
+ element.title = view.state.phrase('unfold');
324
+ element.className = 'cm-foldPlaceholder';
325
+ element.addEventListener('click', ({ target }) => {
326
+ const pos = view.posAtDOM(target), { state } = view, { selection } = state;
327
+ foldedRanges(state).between(pos, pos, (from, to) => {
328
+ if (from === pos) {
329
+ // Unfold the template and redraw the selections
330
+ view.dispatch({ effects: unfoldEffect.of({ from, to }), selection });
331
+ }
332
+ });
333
+ });
334
+ return element;
335
+ },
336
+ }),
337
+ /** @see https://codemirror.net/examples/tooltip/ */
338
+ StateField.define({
339
+ create,
340
+ update(tooltip, { state, docChanged, selection }) {
341
+ if (docChanged) {
342
+ return null;
343
+ }
344
+ return selection ? create(state) : tooltip;
345
+ },
346
+ provide(f) {
347
+ return showTooltip.from(f);
348
+ },
349
+ }),
350
+ keymap.of([
351
+ {
352
+ // Fold the template at the selection/cursor
353
+ key: 'Ctrl-Shift-[',
354
+ mac: 'Cmd-Alt-[',
355
+ run(view) {
356
+ const { state } = view, tree = syntaxTree(state), effects = [];
357
+ let anchor = getAnchor(state);
358
+ for (const { from, to, empty } of state.selection.ranges) {
359
+ let node;
360
+ if (empty) {
361
+ // No selection, try both sides of the cursor position
362
+ node = tree.resolve(from, -1);
363
+ }
364
+ if (!node || node.name === 'Document') {
365
+ node = tree.resolve(from, 1);
366
+ }
367
+ anchor = traverse(state, tree, effects, node, to, anchor, updateSelection);
368
+ }
369
+ return execute(view, effects, anchor);
370
+ },
371
+ },
372
+ {
373
+ // Fold all templates in the document
374
+ key: 'Ctrl-Alt-[',
375
+ run(view) {
376
+ const { state } = view, tree = syntaxTree(state), effects = [], anchor = traverse(state, tree, effects, tree.topNode.firstChild, Infinity, getAnchor(state), updateAll);
377
+ return execute(view, effects, anchor);
378
+ },
379
+ },
380
+ {
381
+ // Unfold the template at the selection/cursor
382
+ key: 'Ctrl-Shift-]',
383
+ mac: 'Cmd-Alt-]',
384
+ run(view) {
385
+ const { state } = view, { selection } = state, effects = [], folded = foldedRanges(state);
386
+ for (const { from, to } of selection.ranges) {
387
+ // Unfold any folded range at the selection
388
+ folded.between(from, to, (i, j) => {
389
+ effects.push(unfoldEffect.of({ from: i, to: j }));
390
+ });
391
+ }
392
+ if (effects.length > 0) {
393
+ // Unfold the template(s) and redraw the selections
394
+ view.dispatch({ effects, selection });
395
+ return true;
396
+ }
397
+ return false;
398
+ },
399
+ },
400
+ { key: 'Ctrl-Alt-]', run: unfoldAll },
401
+ ]),
402
+ markers,
403
+ gutter({
404
+ class: 'cm-foldGutter',
405
+ markers(view) {
406
+ return view.plugin(markers)?.markers ?? RangeSet.empty;
407
+ },
408
+ initialSpacer() {
409
+ return new FoldMarker(false);
410
+ },
411
+ domEventHandlers: {
412
+ click(view, line) {
413
+ const folded = findFold(view, line);
414
+ if (folded) {
415
+ view.dispatch({ effects: unfoldEffect.of(folded) });
416
+ return true;
417
+ }
418
+ const range = foldableLine(view, line);
419
+ if (range) {
420
+ view.dispatch({ effects: foldEffect.of(range) });
421
+ return true;
422
+ }
423
+ return false;
424
+ },
425
+ },
426
+ }),
427
+ ],
428
+ },
429
+ ];
430
+ /**
431
+ * 点击提示折叠模板参数
432
+ * @param view
433
+ */
434
+ export const foldHandler = (view) => (e) => {
435
+ const dom = e.target.closest('.cm-tooltip-fold');
436
+ if (dom) {
437
+ e.preventDefault();
438
+ const { dataset } = dom, from = Number(dataset['from']), to = Number(dataset['to']);
439
+ view.dispatch({
440
+ effects: foldEffect.of({ from, to }),
441
+ selection: { anchor: to },
442
+ });
443
+ dom.remove();
444
+ }
445
+ };