wikilint 2.16.1 → 2.16.3

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/lib/lsp.js CHANGED
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LanguageService = exports.tasks = void 0;
4
4
  const path = require("path");
5
5
  const common_1 = require("@bhsd/common");
6
- const debug_1 = require("../util/debug");
7
6
  const sharable_1 = require("../util/sharable");
8
7
  const lint_1 = require("../util/lint");
9
8
  const index_1 = require("../index");
10
9
  const element_1 = require("../lib/element");
11
10
  exports.tasks = new WeakMap();
11
+ const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']), nameAttrs = new Set(['name', 'extends', 'follow']), groupAttrs = new Set(['group']);
12
12
  /**
13
13
  * Check if all child nodes are plain text or comments.
14
14
  * @param childNodes child nodes
@@ -41,7 +41,7 @@ const createNodeRange = (token) => {
41
41
  const { top, left, height, width } = token.getBoundingClientRect();
42
42
  return {
43
43
  start: { line: top, character: left },
44
- end: (0, lint_1.getEndPos)(top, left, width, height),
44
+ end: (0, lint_1.getEndPos)(top, left, height, width),
45
45
  };
46
46
  };
47
47
  /**
@@ -53,7 +53,7 @@ const createNodeRange = (token) => {
53
53
  * @param pos.line line number
54
54
  * @param pos.character character number
55
55
  */
56
- const getCompletion = (words, kind, mt, { line, character }) => [...new Set(words)].map(w => ({
56
+ const getCompletion = (words, kind, mt, { line, character }) => [...new Set(words)].map((w) => ({
57
57
  label: w,
58
58
  kind,
59
59
  textEdit: {
@@ -86,59 +86,34 @@ const getUrl = (page, ns) => {
86
86
  : articlePath + (articlePath.endsWith('/') ? '' : '/') + encoded;
87
87
  };
88
88
  /**
89
- * Get the token at the position.
89
+ * Get the token at the position from a word.
90
90
  * @param root root token
91
91
  * @param pos position
92
92
  */
93
- const elementFromPoint = (root, pos) => {
94
- const { line, character } = pos;
95
- let offset = root.indexFromPos(line, character), node = root;
96
- while (true) { // eslint-disable-line no-constant-condition
97
- // eslint-disable-next-line @typescript-eslint/no-loop-func
98
- const child = node.childNodes.find(ch => {
99
- const i = ch.getRelativeIndex();
100
- if (i < offset && i + ch.toString().length >= offset) {
101
- offset -= i;
102
- return true;
103
- }
104
- return false;
105
- });
106
- if (!child || child.type === 'text') {
107
- break;
108
- }
109
- node = child;
110
- }
111
- return node;
93
+ const elementFromWord = (root, pos) => {
94
+ const { line, character } = pos, index = root.indexFromPos(line, character), offset = Number(/[\w!#]/u.test(root.toString().charAt(index)));
95
+ return root.elementFromIndex(index + offset);
112
96
  };
113
97
  /**
114
- * Get the token at the position from a word.
115
- * @param root root token
116
- * @param pos position
98
+ * Get the attribute of a `<ref>` tag.
99
+ * @param token attribute token
100
+ * @param tags tag names
101
+ * @param names attribute names
117
102
  */
118
- const elementFromWord = (root, pos) => {
119
- const { line, character } = pos, offset = Number(/\w/u.test(root.toString().charAt(root.indexFromPos(line, character))));
120
- return elementFromPoint(root, { line, character: character + offset });
103
+ const getRefAttr = (token, tags, names) => {
104
+ const { type, parentNode = {} } = token, { name, tag } = parentNode;
105
+ return type === 'attr-value' && tags.has(tag) && names.has(name) ? token.toString().trim() : NaN;
121
106
  };
122
107
  /**
123
108
  * Get the `name` attribute of a `<ref>` tag.
124
109
  * @param token `name` attribute token
125
110
  */
126
- const getRefName = (token) => {
127
- const { type, parentNode = {} } = token, { name, tag } = parentNode;
128
- return type === 'attr-value' && tag === 'ref' && ['name', 'extends', 'follow'].includes(name)
129
- ? token.toString().trim()
130
- : NaN;
131
- };
111
+ const getRefName = (token) => getRefAttr(token, refTags, nameAttrs);
132
112
  /**
133
113
  * Get the `group` attribute of a `<ref>` or `<references>` tag.
134
114
  * @param token `group` attribute token
135
115
  */
136
- const getRefGroup = (token) => {
137
- const { type, parentNode = {} } = token, { name, tag } = parentNode;
138
- return type === 'attr-value' && name === 'group' && (tag === 'ref' || tag === 'references')
139
- ? token.toString().trim()
140
- : NaN;
141
- };
116
+ const getRefGroup = (token) => getRefAttr(token, referencesTags, groupAttrs);
142
117
  /**
143
118
  * Get the effective name of a token.
144
119
  * @param token
@@ -169,7 +144,8 @@ const getName = (token) => {
169
144
  */
170
145
  const getSectionEnd = (section, lines, line) => {
171
146
  if (section) {
172
- section.range.end = { line, character: lines[line].length };
147
+ const [, start, end] = lines[line];
148
+ section.range.end = { line, character: end - start };
173
149
  }
174
150
  };
175
151
  /* NOT FOR BROWSER ONLY END */
@@ -178,6 +154,7 @@ class LanguageService {
178
154
  #text;
179
155
  #running;
180
156
  #done;
157
+ #config;
181
158
  #completionConfig;
182
159
  #signature;
183
160
  /** @private */
@@ -201,14 +178,11 @@ class LanguageService {
201
178
  * - 否则开始新的解析
202
179
  */
203
180
  async #queue(text) {
204
- /* istanbul ignore if */
205
- if (typeof text !== 'string') {
206
- return (0, debug_1.typeError)(this.constructor, 'queue', 'String');
207
- }
208
- else if (this.#text === text && !this.#running && this.#done) {
181
+ text = text.replace(/\r$/gmu, '');
182
+ if (this.#text === text && this.#config === index_1.default.config && !this.#running) {
209
183
  return this.#done;
210
184
  }
211
- this.#text = text.replace(/\r$/gmu, '');
185
+ this.#text = text;
212
186
  this.#running ??= this.#parse(); // 不要提交多个解析任务
213
187
  return this.#running;
214
188
  }
@@ -221,8 +195,10 @@ class LanguageService {
221
195
  async #parse() {
222
196
  return new Promise(resolve => {
223
197
  (typeof setImmediate === 'function' ? setImmediate : /* istanbul ignore next */ setTimeout)(() => {
224
- const text = this.#text, root = index_1.default.parse(text, true);
225
- if (this.#text === text) {
198
+ const config = index_1.default.getConfig();
199
+ this.#config = index_1.default.config;
200
+ const text = this.#text, root = index_1.default.parse(text, true, undefined, config);
201
+ if (this.#text === text && this.#config === index_1.default.config) {
226
202
  this.#done = root;
227
203
  this.#running = undefined;
228
204
  resolve(root);
@@ -235,6 +211,16 @@ class LanguageService {
235
211
  }, 0);
236
212
  });
237
213
  }
214
+ /**
215
+ * 检查是否为签名语言服务器
216
+ * @throws `Error` 是签名语言服务器
217
+ */
218
+ #checkSignature() {
219
+ /* istanbul ignore if */
220
+ if (this.#signature) {
221
+ throw new Error('This is a signature language server!');
222
+ }
223
+ }
238
224
  /**
239
225
  * 提供颜色指示
240
226
  * @param rgba 颜色解析函数
@@ -242,16 +228,15 @@ class LanguageService {
242
228
  * @param hsl 是否允许HSL颜色
243
229
  */
244
230
  async provideDocumentColors(rgba, text, hsl = true) {
245
- /* istanbul ignore if */
246
- if (this.#signature) {
247
- throw new Error('This is a signature language server!');
248
- }
231
+ this.#checkSignature();
249
232
  const root = await this.#queue(text);
250
- return root.querySelectorAll('attr-value,parameter-value,arg-default').flatMap(({ type, childNodes }) => {
233
+ return root.querySelectorAll('attr-value,parameter-value,arg-default')
234
+ .flatMap(({ type, childNodes }) => {
251
235
  if (type !== 'attr-value' && !isPlain(childNodes)) {
252
236
  return [];
253
237
  }
254
- return childNodes.filter((child) => child.type === 'text').flatMap(child => {
238
+ return childNodes.filter((child) => child.type === 'text')
239
+ .flatMap(child => {
255
240
  const parts = (0, common_1.splitColors)(child.data, hsl).filter(([, , , isColor]) => isColor);
256
241
  if (parts.length === 0) {
257
242
  return [];
@@ -287,21 +272,22 @@ class LanguageService {
287
272
  #prepareCompletionConfig() {
288
273
  if (!this.#completionConfig) {
289
274
  const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = index_1.default.getConfig(), tags = new Set([ext, html].flat(2));
275
+ const re = new RegExp('(?:' // eslint-disable-line prefer-template
276
+ + String.raw `<\/?(\w*)` // tag
277
+ + '|'
278
+ + String.raw `(\{{2,4}|\[\[)\s*([^|{}<>[\]\s][^|{}<>[\]#]*)?` // braces and brackets
279
+ + '|'
280
+ + String.raw `(__(?:(?!__)[\p{L}\d_])*)` // behavior switch
281
+ + '|'
282
+ + String.raw `(?<!\[)\[([a-z:/]*)` // protocol
283
+ + '|'
284
+ + String.raw `\[\[\s*(?:${Object.entries(nsid).filter(([, v]) => v === 6).map(([k]) => k).join('|')})\s*:[^[\]{}<>]+\|([^[\]{}<>|=]*)` // image parameter
285
+ + '|'
286
+ // attribute key
287
+ + String.raw `<(\w+)(?:\s(?:[^<>{}|=\s]+(?:\s*=\s*(?:[^\s"']\S*|(["']).*?\8))?(?=\s))*)?\s(\w*)`
288
+ + ')$', 'iu');
290
289
  this.#completionConfig = {
291
- re: new RegExp('(?:' // eslint-disable-line prefer-template
292
- + String.raw `<\/?(\w+)` // tag
293
- + '|'
294
- + String.raw `(\{{2,4}|\[\[)\s*([^|{}<>[\]\s][^|{}<>[\]#]*)` // braces and brackets
295
- + '|'
296
- + String.raw `(__(?:(?!__)[\p{L}\d_])+)` // behavior switch
297
- + '|'
298
- + String.raw `(?<!\[)\[([a-z:/]+)` // protocol
299
- + '|'
300
- + String.raw `\[\[\s*(?:${Object.entries(nsid).filter(([, v]) => v === 6).map(([k]) => k).join('|')})\s*:[^[\]{}<>]+\|([^[\]{}<>|=]+)` // image parameter
301
- + '|'
302
- // attribute key
303
- + String.raw `<(\w+)(?:\s(?:[^<>{}|=\s]+(?:\s*=\s*(?:[^\s"']\S*|(["']).*?\8))?(?=\s))*)?\s(\w+)`
304
- + ')$', 'iu'),
290
+ re,
305
291
  ext,
306
292
  tags,
307
293
  allTags: [...tags, 'onlyinclude', 'includeonly', 'noinclude'],
@@ -324,39 +310,37 @@ class LanguageService {
324
310
  * @param position 位置
325
311
  */
326
312
  async provideCompletionItems(text, position) {
327
- /* istanbul ignore if */
328
- if (this.#signature) {
329
- throw new Error('This is a signature language server!');
330
- }
331
- const { re, allTags, functions, switches, protocols, params, tags, ext } = this.#prepareCompletionConfig(), { line, character } = position, mt = re.exec(text.split(/\r?\n/u)[line]?.slice(0, character) ?? '');
332
- if (mt?.[1]) { // tag
313
+ this.#checkSignature();
314
+ const { re, allTags, functions, switches, protocols, params, tags, ext } = this.#prepareCompletionConfig(), { line, character } = position, mt = re.exec(text.split(/\r?\n/u, line + 1)[line]?.slice(0, character) ?? '');
315
+ if (mt?.[1] !== undefined) { // tag
333
316
  return getCompletion(allTags, 'Class', mt[1], position);
334
317
  }
335
318
  else if (mt?.[4]) { // behavior switch
336
319
  return getCompletion(switches, 'Constant', mt[4], position);
337
320
  }
338
- else if (mt?.[5]) { // protocol
321
+ else if (mt?.[5] !== undefined) { // protocol
339
322
  return getCompletion(protocols, 'Reference', mt[5], position);
340
323
  }
341
324
  const root = await this.#queue(text);
342
- if (mt?.[2] === '{{{') { // argument
343
- return getCompletion(root.querySelectorAll('arg').map(({ name }) => name), 'Variable', mt[3], position);
344
- }
345
- else if (mt?.[3]) { // parser function, template or link
346
- const colon = mt[3].startsWith(':'), str = colon ? mt[3].slice(1).trimStart() : mt[3];
347
- if (mt[2] === '[[') {
348
- return getCompletion(root.querySelectorAll('link,file,category').map(({ name }) => name), 'Folder', str, position);
325
+ if (mt?.[2]) {
326
+ const match = mt[3] ?? '';
327
+ if (mt[2] === '{{{') { // argument
328
+ return getCompletion(root.querySelectorAll('arg').map(({ name }) => name).filter(Boolean), 'Variable', match, position);
349
329
  }
350
- return [
351
- ...getCompletion(functions, 'Function', mt[3], position),
352
- ...mt[3].startsWith('#')
353
- ? []
354
- : getCompletion(root.querySelectorAll('template')
355
- .map(({ name }) => colon ? name : name.replace(/^Template:/u, '')), 'Folder', str, position),
356
- ];
330
+ const colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
331
+ return mt[2] === '[['
332
+ ? getCompletion(// link
333
+ root.querySelectorAll('link,file,category,redirect-target').map(({ name }) => name), 'Folder', str, position)
334
+ : [
335
+ ...getCompletion(functions, 'Function', match, position),
336
+ ...match.startsWith('#')
337
+ ? []
338
+ : getCompletion(root.querySelectorAll('template')
339
+ .map(({ name }) => colon ? name : name.replace(/^Template:/u, '')), 'Folder', str, position),
340
+ ];
357
341
  }
358
- const token = elementFromPoint(root, position), { type, parentNode } = token;
359
- if (mt?.[6]?.trim() || type === 'image-parameter') { // image parameter
342
+ const token = root.elementFromPoint(character, line), { type, parentNode } = token;
343
+ if (mt?.[6] !== undefined || type === 'image-parameter') { // image parameter
360
344
  const match = mt?.[6]?.trimStart()
361
345
  ?? this.#text.slice(token.getAbsoluteIndex(), root.indexFromPos(position.line, position.character)).trimStart();
362
346
  return [
@@ -364,12 +348,12 @@ class LanguageService {
364
348
  ...getCompletion(root.querySelectorAll('image-parameter#width').map(width => width.text()), 'Unit', match, position),
365
349
  ];
366
350
  }
367
- else if (mt?.[7] || type === 'attr-key') { // attribute key
368
- const tag = mt?.[7]?.toLowerCase() ?? parentNode.tag;
351
+ else if (mt?.[7] !== undefined || type === 'attr-key') { // attribute key
352
+ const tag = mt?.[7]?.toLowerCase() ?? parentNode.tag, key = mt?.[9] ?? token.toString();
369
353
  if (!tags.has(tag)) {
370
354
  return undefined;
371
355
  }
372
- const key = mt?.[9] ?? token.toString(), thisHtmlAttrs = sharable_1.htmlAttrs[tag], thisExtAttrs = sharable_1.extAttrs[tag], extCompletion = thisExtAttrs && getCompletion(thisExtAttrs, 'Field', key, position);
356
+ const thisHtmlAttrs = sharable_1.htmlAttrs[tag], thisExtAttrs = sharable_1.extAttrs[tag], extCompletion = thisExtAttrs && getCompletion(thisExtAttrs, 'Field', key, position);
373
357
  return ext.includes(tag) && !thisHtmlAttrs
374
358
  ? extCompletion
375
359
  : [
@@ -386,22 +370,23 @@ class LanguageService {
386
370
  }
387
371
  else if ((type === 'parameter-key' || type === 'parameter-value' && parentNode.anon)
388
372
  && parentNode.parentNode.type === 'template') { // parameter key
389
- return getCompletion(root.querySelectorAll('parameter').filter(({ anon, parentNode: parent }) => !anon && parent.type === 'template'
390
- && parent.name === parentNode.parentNode.name).map(({ name }) => name), 'Variable', token.toString().trimStart(), position);
373
+ const key = token.toString().trimStart();
374
+ return key
375
+ ? getCompletion(root.querySelectorAll('parameter').filter(({ anon, parentNode: parent }) => !anon && parent.type === 'template'
376
+ && parent.name === parentNode.parentNode.name).map(({ name }) => name), 'Variable', key, position)
377
+ : undefined;
391
378
  }
392
379
  return undefined;
393
380
  }
394
381
  /**
395
382
  * 提供语法诊断
396
383
  * @param wikitext 源代码
384
+ * @param warning 是否提供警告
397
385
  */
398
- async provideDiagnostics(wikitext) {
399
- /* istanbul ignore if */
400
- if (this.#signature) {
401
- throw new Error('This is a signature language server!');
402
- }
403
- const root = await this.#queue(wikitext);
404
- return root.lint().filter(({ severity }) => severity === 'error')
386
+ async provideDiagnostics(wikitext, warning = true) {
387
+ this.#checkSignature();
388
+ const root = await this.#queue(wikitext), errors = root.lint();
389
+ return (warning ? errors : errors.filter(({ severity }) => severity === 'error'))
405
390
  .map(({ startLine, startCol, endLine, endCol, severity, rule, message, fix, suggestions }) => ({
406
391
  range: {
407
392
  start: { line: startLine, character: startCol },
@@ -434,15 +419,12 @@ class LanguageService {
434
419
  }));
435
420
  }
436
421
  async #provideFoldingRangesOrDocumentSymbols(text, fold = true) {
437
- /* istanbul ignore if */
438
- if (this.#signature) {
439
- throw new Error('This is a signature language server!');
440
- }
422
+ this.#checkSignature();
441
423
  const ranges = [], symbols = [],
442
424
  /* NOT FOR BROWSER ONLY */
443
425
  names = new Set(), sections = new Array(6),
444
426
  /* NOT FOR BROWSER ONLY END */
445
- root = await this.#queue(text), lines = this.#text.split('\n'), { length } = lines, levels = new Array(6), tokens = root.querySelectorAll(fold ? 'heading-title,table,template,magic-word' : 'heading-title');
427
+ root = await this.#queue(text), lines = root.getLines(), { length } = lines, levels = new Array(6), tokens = root.querySelectorAll(fold ? 'heading-title,table,template,magic-word' : 'heading-title');
446
428
  for (const token of tokens) {
447
429
  const { top, height,
448
430
  /* NOT FOR BROWSER ONLY */
@@ -474,7 +456,7 @@ class LanguageService {
474
456
  .find(s => !names.has(s))
475
457
  : section, container = sections.slice(0, level - 1).reverse().find(Boolean), selectionRange = {
476
458
  start: { line: top, character: left - level },
477
- end: (0, lint_1.getEndPos)(top, left, width + level, height),
459
+ end: (0, lint_1.getEndPos)(top, left, height, width + level),
478
460
  }, info = {
479
461
  name,
480
462
  kind: 15,
@@ -533,13 +515,10 @@ class LanguageService {
533
515
  * @param text 源代码
534
516
  */
535
517
  async provideLinks(text) {
536
- /* istanbul ignore if */
537
- if (this.#signature) {
538
- throw new Error('This is a signature language server!');
539
- }
518
+ this.#checkSignature();
540
519
  const protocolRegex = new RegExp(`^(?:${index_1.default.getConfig().protocol}|//)`, 'iu'), selector = 'link-target,template-name,invoke-module,magic-link,ext-link-url,free-ext-link,attr-value,'
541
520
  + 'image-parameter#link';
542
- return (await this.#queue(text)).querySelectorAll(selector).flatMap(token => {
521
+ return (await this.#queue(text)).querySelectorAll(selector).flatMap((token) => {
543
522
  const { type, parentNode, firstChild, lastChild, childNodes } = token, { name, tag } = parentNode;
544
523
  if (!(type !== 'attr-value'
545
524
  || name === 'src' && ['templatestyles', 'img'].includes(tag)
@@ -586,7 +565,7 @@ class LanguageService {
586
565
  {
587
566
  range: {
588
567
  start: { line: rect.top, character: rect.left },
589
- end: (0, lint_1.getEndPos)(top, left, width, height),
568
+ end: (0, lint_1.getEndPos)(top, left, height, width),
590
569
  },
591
570
  target,
592
571
  },
@@ -599,11 +578,8 @@ class LanguageService {
599
578
  }
600
579
  });
601
580
  }
602
- async #provideReferencesOrDefinition(text, position, usage, newName) {
603
- /* istanbul ignore if */
604
- if (this.#signature) {
605
- throw new Error('This is a signature language server!');
606
- }
581
+ async #provideReferencesOrDefinition(text, position, usage) {
582
+ this.#checkSignature();
607
583
  const renameTypes = [
608
584
  'arg-name',
609
585
  'template-name',
@@ -619,40 +595,30 @@ class LanguageService {
619
595
  'heading',
620
596
  ...renameTypes,
621
597
  ], root = await this.#queue(text), node = elementFromWord(root, position), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
622
- if (usage > 1 && type === 'parameter-key' && /^[1-9]\d*$/u.test(node.parentNode.name)
598
+ if (usage === 2 && type === 'parameter-key' && /^[1-9]\d*$/u.test(node.parentNode.name)
623
599
  || !refName && (usage === 1
624
600
  || !refGroup && (usage === 0
625
601
  ? !types.includes(type)
626
602
  : !renameTypes.includes(type)
627
- || type === 'link-target' && ['link', 'redirect-target'].includes(node.parentNode.type)))) {
603
+ || type === 'link-target' && !['link', 'redirect-target'].includes(node.parentNode.type)))) {
628
604
  return undefined;
629
605
  }
630
606
  else if (usage === 2) {
631
607
  return createNodeRange(node);
632
608
  }
633
609
  const name = getName(node), refs = root.querySelectorAll(type === 'heading-title' ? 'heading' : type).filter(token => {
610
+ const { name: n, parentNode } = token.parentNode;
634
611
  if (usage === 1) {
635
- const { name: n, parentNode } = token.parentNode;
636
612
  return getRefName(token) === refName
637
613
  && n === 'name' && parentNode.parentNode.innerText;
638
614
  }
639
615
  return type === 'attr-value'
640
616
  ? getRefName(token) === refName || getRefGroup(token) === refGroup
641
617
  : getName(token) === name;
642
- }).map(token => usage !== 3 && token.type === 'parameter-key' ? token.parentNode : token);
643
- if (refs.length === 0) {
644
- return undefined;
645
- }
646
- return usage === 3
647
- ? {
648
- changes: {
649
- '': refs.map(ref => ({
650
- range: createNodeRange(ref),
651
- newText: newName,
652
- })),
653
- },
654
- }
655
- : refs.map((ref) => ({ range: createNodeRange(ref) }));
618
+ }).map((token) => ({
619
+ range: createNodeRange(token.type === 'parameter-key' ? token.parentNode : token),
620
+ }));
621
+ return refs.length === 0 ? undefined : refs;
656
622
  }
657
623
  /**
658
624
  * 提供引用
@@ -685,7 +651,28 @@ class LanguageService {
685
651
  * @param newName 新名称
686
652
  */
687
653
  async provideRenameEdits(text, position, newName) {
688
- return this.#provideReferencesOrDefinition(text, position, 3, newName);
654
+ this.#checkSignature();
655
+ const root = await this.#queue(text), node = elementFromWord(root, position), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
656
+ const name = getName(node), refs = root.querySelectorAll(type).filter(token => {
657
+ const { type: t } = token.parentNode;
658
+ if (type === 'link-target' && t !== 'link' && t !== 'redirect-target') {
659
+ return false;
660
+ }
661
+ return type === 'attr-value'
662
+ ? getRefName(token) === refName || getRefGroup(token) === refGroup
663
+ : getName(token) === name;
664
+ });
665
+ if (refs.length === 0) {
666
+ return undefined;
667
+ }
668
+ return {
669
+ changes: {
670
+ '': refs.map((ref) => ({
671
+ range: createNodeRange(ref),
672
+ newText: newName,
673
+ })),
674
+ },
675
+ };
689
676
  }
690
677
  /**
691
678
  * 检索解析器函数
@@ -704,12 +691,10 @@ class LanguageService {
704
691
  if (!this.data) {
705
692
  return undefined;
706
693
  }
707
- else if (this.#signature) {
708
- throw new Error('This is a signature language server!');
709
- }
694
+ this.#checkSignature();
710
695
  const token = elementFromWord(await this.#queue(text), position);
711
696
  let info, f;
712
- if (token.type === 'double-underscore') {
697
+ if (token.is('double-underscore')) {
713
698
  info = this.data.behaviorSwitches.find(({ aliases }) => aliases.includes(token.innerText.toLowerCase()));
714
699
  }
715
700
  else if (token.type === 'magic-word-name') {
@@ -727,7 +712,6 @@ class LanguageService {
727
712
  range: createNodeRange(token),
728
713
  };
729
714
  }
730
- /* NOT FOR BROWSER ONLY */
731
715
  /**
732
716
  * 提供魔术字帮助
733
717
  * @param text 源代码
@@ -743,7 +727,7 @@ class LanguageService {
743
727
  throw new Error('This is a regular language server!');
744
728
  }
745
729
  this.#signature = true;
746
- const { line, character } = position, curLine = text.split(/\r?\n/u)[line], { lastChild } = await this.#queue(`${curLine.slice(0, character + /^[^{}<]*/u.exec(curLine.slice(character))[0].length)}}}`), { type, name, childNodes, firstChild } = lastChild;
730
+ const { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], { lastChild } = await this.#queue(`${curLine.slice(0, character + /^[^{}<]*/u.exec(curLine.slice(character))[0].length)}}}`), { type, name, childNodes, firstChild } = lastChild;
747
731
  if (type !== 'magic-word') {
748
732
  return undefined;
749
733
  }
@@ -751,7 +735,12 @@ class LanguageService {
751
735
  if (!info?.signatures) {
752
736
  return undefined;
753
737
  }
754
- const f = firstChild.text().trim(), n = childNodes.length - 1, start = lastChild.getAbsoluteIndex(), activeParameter = childNodes.findLastIndex(child => child.getRelativeIndex() <= character - start) - 1, signatures = info.signatures.filter(params => (params.length >= n || params.at(-1)?.rest)
738
+ const f = firstChild.text().trim(), n = childNodes.length - 1, start = lastChild.getAbsoluteIndex();
739
+ let activeParameter = childNodes.findIndex(child => child.getRelativeIndex() > character - start) - 2;
740
+ if (activeParameter === -3) {
741
+ activeParameter = n - 1;
742
+ }
743
+ const signatures = info.signatures.filter(params => (params.length >= n || params[params.length - 1]?.rest)
755
744
  && params.every(({ label, const: c }, i) => {
756
745
  const p = c && i < n && childNodes[i + 1]?.text().trim();
757
746
  return !p || label.startsWith(p) || label.startsWith(p.toLowerCase());
@@ -765,10 +754,29 @@ class LanguageService {
765
754
  }));
766
755
  return { signatures, activeParameter };
767
756
  }
757
+ /**
758
+ * 提供 CodeLens
759
+ * @param text 源代码
760
+ */
761
+ async provideInlayHints(text) {
762
+ this.#checkSignature();
763
+ const hints = [], root = await this.#queue(text);
764
+ for (const template of root.querySelectorAll('template,magic-word#invoke')) {
765
+ const { type, childNodes } = template;
766
+ hints.push(...childNodes.slice(type === 'template' ? 1 : 3).filter(({ anon }) => anon)
767
+ .map((parameter) => ({
768
+ position: positionAt(root, parameter.getAbsoluteIndex()),
769
+ label: `${parameter.name}=`,
770
+ kind: 2,
771
+ })));
772
+ }
773
+ return hints;
774
+ }
775
+ /* NOT FOR BROWSER ONLY */
768
776
  /** @implements */
769
777
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
770
778
  provideCodeAction(diagnostics) {
771
- return diagnostics.filter(({ data }) => data).flatMap(diagnostic => diagnostic.data.map((data) => ({
779
+ return diagnostics.flatMap(diagnostic => diagnostic.data.map((data) => ({
772
780
  title: data.title,
773
781
  kind: 'quickfix',
774
782
  diagnostics: [diagnostic],
@@ -64,4 +64,6 @@ export declare abstract class AstNode implements AstNodeBase {
64
64
  * @param type 节点类型
65
65
  */
66
66
  is<T extends Token>(type: string): this is T;
67
+ /** 获取所有行的wikitext和起止位置 */
68
+ getLines(): [string, number, number][];
67
69
  }