wikilint 2.16.2 → 2.16.4

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
@@ -5,15 +5,30 @@ const path = require("path");
5
5
  const common_1 = require("@bhsd/common");
6
6
  const sharable_1 = require("../util/sharable");
7
7
  const lint_1 = require("../util/lint");
8
+ const string_1 = require("../util/string");
8
9
  const index_1 = require("../index");
9
- const element_1 = require("../lib/element");
10
+ /* NOT FOR BROWSER ONLY END */
10
11
  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
+ const refTags = new Set(['ref']), referencesTags = new Set(['ref', 'references']), nameAttrs = new Set(['name', 'extends', 'follow']), groupAttrs = new Set(['group']), renameTypes = new Set([
13
+ 'arg-name',
14
+ 'template-name',
15
+ 'magic-word-name',
16
+ 'link-target',
17
+ 'parameter-key',
18
+ ]), referenceTypes = new Set([
19
+ 'ext',
20
+ 'html',
21
+ 'attr-key',
22
+ 'image-parameter',
23
+ 'heading-title',
24
+ 'heading',
25
+ ...renameTypes,
26
+ ]), plainTypes = new Set(['text', 'comment', 'noinclude', 'include']);
12
27
  /**
13
28
  * Check if all child nodes are plain text or comments.
14
29
  * @param childNodes child nodes
15
30
  */
16
- const isPlain = (childNodes) => childNodes.every(({ type }) => ['text', 'comment', 'noinclude', 'include'].includes(type));
31
+ const isPlain = (childNodes) => childNodes.every(({ type }) => plainTypes.has(type));
17
32
  /**
18
33
  * Get the position of a character in the document.
19
34
  * @param root root token
@@ -52,8 +67,9 @@ const createNodeRange = (token) => {
52
67
  * @param pos position
53
68
  * @param pos.line line number
54
69
  * @param pos.character character number
70
+ * @param extra extra text
55
71
  */
56
- const getCompletion = (words, kind, mt, { line, character }) => [...new Set(words)].map((w) => ({
72
+ const getCompletion = (words, kind, mt, { line, character }, extra) => [...new Set(words)].map((w) => ({
57
73
  label: w,
58
74
  kind,
59
75
  textEdit: {
@@ -61,38 +77,19 @@ const getCompletion = (words, kind, mt, { line, character }) => [...new Set(word
61
77
  start: { line, character: character - mt.length },
62
78
  end: { line, character },
63
79
  },
64
- newText: w,
80
+ newText: w + (extra ?? ''),
65
81
  },
66
82
  }));
67
83
  /**
68
- * Create the URL of a page.
69
- * @param page page name
70
- * @param ns namespace
71
- * @throws `RangeError` Invalid page name.
72
- * @throws `Error` Article path is not set.
73
- */
74
- const getUrl = (page, ns) => {
75
- const { title, fragment, valid } = index_1.default.normalizeTitle(page, ns), { articlePath } = index_1.default.getConfig();
76
- /* istanbul ignore next */
77
- if (!valid) {
78
- throw new RangeError('Invalid page name.');
79
- }
80
- else if (!articlePath) {
81
- throw new Error('Article path is not set.');
82
- }
83
- const encoded = encodeURIComponent(title) + (fragment === undefined ? '' : `#${encodeURIComponent(fragment)}`);
84
- return articlePath.includes('$1')
85
- ? articlePath.replace('$1', encoded)
86
- : articlePath + (articlePath.endsWith('/') ? '' : '/') + encoded;
87
- };
88
- /**
89
- * Get the token at the position from a word.
84
+ * Get the caret position at the position from a word.
90
85
  * @param root root token
91
86
  * @param pos position
87
+ * @param pos.line line number
88
+ * @param pos.character character number
92
89
  */
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);
90
+ const caretPositionFromWord = (root, { line, character }) => {
91
+ const index = root.indexFromPos(line, character);
92
+ return root.caretPositionFromIndex(index + Number(/\w/u.test(root.toString().charAt(index))));
96
93
  };
97
94
  /**
98
95
  * Get the attribute of a `<ref>` tag.
@@ -114,6 +111,15 @@ const getRefName = (token) => getRefAttr(token, refTags, nameAttrs);
114
111
  * @param token `group` attribute token
115
112
  */
116
113
  const getRefGroup = (token) => getRefAttr(token, referencesTags, groupAttrs);
114
+ /**
115
+ * Get the attribute of a `<ref>` tag.
116
+ * @param token extension token
117
+ * @param target attribute name
118
+ */
119
+ const getRefTagAttr = (token, target) => {
120
+ const attr = token?.getAttr(target);
121
+ return attr !== true && attr || false;
122
+ };
117
123
  /**
118
124
  * Get the effective name of a token.
119
125
  * @param token
@@ -136,6 +142,18 @@ const getName = (token) => {
136
142
  }
137
143
  };
138
144
  /* NOT FOR BROWSER ONLY */
145
+ /**
146
+ * Get the quick fix data.
147
+ * @param root root token
148
+ * @param fix lint error fix
149
+ * @param preferred whether it is a preferred fix
150
+ */
151
+ const getQuickFix = (root, fix, preferred = false) => ({
152
+ range: createRange(root, ...fix.range),
153
+ newText: fix.text,
154
+ title: `${preferred ? 'Fix' : 'Suggestion'}: ${fix.desc}`,
155
+ fix: preferred,
156
+ });
139
157
  /**
140
158
  * Get the end position of a section.
141
159
  * @param section section
@@ -178,7 +196,7 @@ class LanguageService {
178
196
  * - 否则开始新的解析
179
197
  */
180
198
  async #queue(text) {
181
- text = text.replace(/\r$/gmu, '');
199
+ text = (0, string_1.tidy)(text);
182
200
  if (this.#text === text && this.#config === index_1.default.config && !this.#running) {
183
201
  return this.#done;
184
202
  }
@@ -235,7 +253,7 @@ class LanguageService {
235
253
  if (type !== 'attr-value' && !isPlain(childNodes)) {
236
254
  return [];
237
255
  }
238
- return childNodes.filter((child) => child.type === 'text')
256
+ return childNodes.filter((child) => child.type === 'text').reverse()
239
257
  .flatMap(child => {
240
258
  const parts = (0, common_1.splitColors)(child.data, hsl).filter(([, , , isColor]) => isColor);
241
259
  if (parts.length === 0) {
@@ -273,7 +291,7 @@ class LanguageService {
273
291
  if (!this.#completionConfig) {
274
292
  const { nsid, ext, html, parserFunction: [insensitive, sensitive, ...other], doubleUnderscore, protocol, img, } = index_1.default.getConfig(), tags = new Set([ext, html].flat(2));
275
293
  const re = new RegExp('(?:' // eslint-disable-line prefer-template
276
- + String.raw `<\/?(\w*)` // tag
294
+ + String.raw `<(\/?\w*)` // tag
277
295
  + '|'
278
296
  + String.raw `(\{{2,4}|\[\[)\s*([^|{}<>[\]\s][^|{}<>[\]#]*)?` // braces and brackets
279
297
  + '|'
@@ -311,9 +329,10 @@ class LanguageService {
311
329
  */
312
330
  async provideCompletionItems(text, position) {
313
331
  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) ?? '');
332
+ const { re, allTags, functions, switches, protocols, params, tags, ext } = this.#prepareCompletionConfig(), { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], mt = re.exec(curLine?.slice(0, character) ?? '');
315
333
  if (mt?.[1] !== undefined) { // tag
316
- return getCompletion(allTags, 'Class', mt[1], position);
334
+ const closing = mt[1].startsWith('/');
335
+ return getCompletion(allTags, 'Class', mt[1].slice(closing ? 1 : 0), position, closing && !curLine?.slice(character).trim().startsWith('>') ? '>' : '');
317
336
  }
318
337
  else if (mt?.[4]) { // behavior switch
319
338
  return getCompletion(switches, 'Constant', mt[4], position);
@@ -322,34 +341,53 @@ class LanguageService {
322
341
  return getCompletion(protocols, 'Reference', mt[5], position);
323
342
  }
324
343
  const root = await this.#queue(text);
344
+ let cur;
325
345
  if (mt?.[2]) {
346
+ cur = root.elementFromPoint(mt.index + mt[2].length - 1, line);
326
347
  const match = mt[3] ?? '';
327
348
  if (mt[2] === '{{{') { // argument
328
- return getCompletion(root.querySelectorAll('arg').map(({ name }) => name).filter(Boolean), 'Variable', match, position);
349
+ return getCompletion(root.querySelectorAll('arg').filter(token => token.name && token !== cur)
350
+ .map(({ name }) => name), 'Variable', match, position);
329
351
  }
330
352
  const colon = match.startsWith(':'), str = colon ? match.slice(1).trimStart() : match;
331
353
  return mt[2] === '[['
332
354
  ? getCompletion(// link
333
- root.querySelectorAll('link,file,category,redirect-target').map(({ name }) => name), 'Folder', str, position)
355
+ root.querySelectorAll('link,file,category,redirect-target').filter(token => token !== cur).map(({ name }) => name), 'Folder', str, position)
334
356
  : [
335
357
  ...getCompletion(functions, 'Function', match, position),
336
358
  ...match.startsWith('#')
337
359
  ? []
338
- : getCompletion(root.querySelectorAll('template')
339
- .map(({ name }) => colon ? name : name.replace(/^Template:/u, '')), 'Folder', str, position),
360
+ : getCompletion(root.querySelectorAll('template').filter(token => token !== cur)
361
+ .map(token => {
362
+ const { name } = token;
363
+ if (colon) {
364
+ return name;
365
+ }
366
+ const { ns } = token.getAttribute('title');
367
+ if (ns === 0) {
368
+ return `:${name}`;
369
+ }
370
+ return ns === 10 ? name.slice(9) : name;
371
+ }), 'Folder', str, position),
340
372
  ];
341
373
  }
342
- const token = root.elementFromPoint(character, line), { type, parentNode } = token;
374
+ let type, parentNode;
375
+ if (mt?.[7] === undefined) {
376
+ cur = root.elementFromPoint(character, line);
377
+ ({ type, parentNode } = cur);
378
+ }
343
379
  if (mt?.[6] !== undefined || type === 'image-parameter') { // image parameter
344
- const match = mt?.[6]?.trimStart()
345
- ?? this.#text.slice(token.getAbsoluteIndex(), root.indexFromPos(position.line, position.character)).trimStart();
380
+ const index = root.indexFromPos(line, character), match = mt?.[6]?.trimStart()
381
+ ?? this.#text.slice(cur.getAbsoluteIndex(), index).trimStart(), equal = this.#text.charAt(index) === '=';
346
382
  return [
347
- ...getCompletion(params, 'Property', match, position),
348
- ...getCompletion(root.querySelectorAll('image-parameter#width').map(width => width.text()), 'Unit', match, position),
383
+ ...getCompletion(params, 'Property', match, position)
384
+ .filter(({ label }) => !equal || !/[= ]$/u.test(label)),
385
+ ...getCompletion(root.querySelectorAll('image-parameter#width').filter(token => token !== cur)
386
+ .map(width => width.text()), 'Unit', match, position),
349
387
  ];
350
388
  }
351
389
  else if (mt?.[7] !== undefined || type === 'attr-key') { // attribute key
352
- const tag = mt?.[7]?.toLowerCase() ?? parentNode.tag, key = mt?.[9] ?? token.toString();
390
+ const tag = mt?.[7]?.toLowerCase() ?? parentNode.tag, key = mt?.[9] ?? cur.toString();
353
391
  if (!tags.has(tag)) {
354
392
  return undefined;
355
393
  }
@@ -368,12 +406,27 @@ class LanguageService {
368
406
  ...getCompletion(['xmlns:'], 'Interface', key, position),
369
407
  ];
370
408
  }
371
- else if ((type === 'parameter-key' || type === 'parameter-value' && parentNode.anon)
372
- && parentNode.parentNode.type === 'template') { // parameter key
373
- const key = token.toString().trimStart();
409
+ else if (type === 'parameter-key' || type === 'parameter-value' && parentNode.anon) {
410
+ // parameter key
411
+ const transclusion = parentNode.parentNode;
412
+ if (transclusion.type === 'magic-word' && transclusion.name !== 'invoke') {
413
+ return undefined;
414
+ }
415
+ const key = cur.toString().trimStart(), [module, func] = transclusion.type === 'magic-word' ? transclusion.getModule() : [];
374
416
  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)
417
+ ? getCompletion(root.querySelectorAll('parameter').filter(token => {
418
+ if (token === parentNode
419
+ || token.anon
420
+ || token.parentNode.type !== transclusion.type
421
+ || token.parentNode.name !== transclusion.name) {
422
+ return false;
423
+ }
424
+ else if (transclusion.type === 'template') {
425
+ return true;
426
+ }
427
+ const [m, f] = token.parentNode.getModule();
428
+ return m === module && f === func;
429
+ }).map(({ name }) => name), 'Variable', key, position, type === 'parameter-value' ? '=' : '')
377
430
  : undefined;
378
431
  }
379
432
  return undefined;
@@ -396,86 +449,41 @@ class LanguageService {
396
449
  source: 'WikiLint',
397
450
  code: rule,
398
451
  message,
452
+ /* NOT FOR BROWSER ONLY */
399
453
  data: [
400
- ...fix
401
- ? [
402
- {
403
- range: createRange(root, ...fix.range),
404
- newText: fix.text,
405
- title: `Fix: ${fix.desc}`,
406
- fix: true,
407
- },
408
- ]
409
- : [],
410
- ...suggestions
411
- ? suggestions.map(({ range, text, desc }) => ({
412
- range: createRange(root, ...range),
413
- newText: text,
414
- title: `Suggestion: ${desc}`,
415
- fix: false,
416
- }))
417
- : [],
454
+ ...fix ? [getQuickFix(root, fix, true)] : [],
455
+ ...suggestions ? suggestions.map(suggestion => getQuickFix(root, suggestion)) : [],
418
456
  ],
419
457
  }));
420
458
  }
421
- async #provideFoldingRangesOrDocumentSymbols(text, fold = true) {
459
+ /**
460
+ * 提供折叠范围
461
+ * @param text 源代码
462
+ */
463
+ async provideFoldingRanges(text) {
422
464
  this.#checkSignature();
423
- const ranges = [], symbols = [],
424
- /* NOT FOR BROWSER ONLY */
425
- names = new Set(), sections = new Array(6),
426
- /* NOT FOR BROWSER ONLY END */
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');
465
+ const root = await this.#queue(text), { length } = root.getLines(), ranges = [], levels = new Array(6), tokens = root.querySelectorAll('heading-title,table,template,magic-word');
466
+ for (const token of [...tokens].reverse()) { // 提高 getBoundingClientRect 的性能
467
+ token.getRelativeIndex();
468
+ }
428
469
  for (const token of tokens) {
429
- const { top, height,
430
- /* NOT FOR BROWSER ONLY */
431
- left, width, } = token.getBoundingClientRect();
470
+ const { top, height } = token.getBoundingClientRect();
432
471
  if (token.type === 'heading-title') {
433
472
  const { level } = token.parentNode;
434
- if (fold) {
435
- for (let i = level - 1; i < 6; i++) {
436
- const startLine = levels[i];
437
- if (startLine !== undefined && startLine < top - 1) {
438
- ranges.push({
439
- startLine,
440
- endLine: top - 1,
441
- kind: 'region',
442
- });
443
- }
444
- levels[i] = undefined;
473
+ for (let i = level - 1; i < 6; i++) {
474
+ const startLine = levels[i];
475
+ if (startLine !== undefined && startLine < top - 1) {
476
+ ranges.push({
477
+ startLine,
478
+ endLine: top - 1,
479
+ kind: 'region',
480
+ });
445
481
  }
446
- levels[level - 1] = top + height - 1; // 从标题的最后一行开始折叠
447
- /* NOT FOR BROWSER ONLY */
448
- }
449
- else {
450
- for (let i = level - 1; i < 6; i++) {
451
- getSectionEnd(sections[i], lines, top - 1);
452
- sections[i] = undefined;
453
- }
454
- const section = token.text().trim() || ' ', name = names.has(section)
455
- ? new Array(names.size).fill('').map((_, i) => `${section.trim()}_${i + 2}`)
456
- .find(s => !names.has(s))
457
- : section, container = sections.slice(0, level - 1).reverse().find(Boolean), selectionRange = {
458
- start: { line: top, character: left - level },
459
- end: (0, lint_1.getEndPos)(top, left, height, width + level),
460
- }, info = {
461
- name,
462
- kind: 15,
463
- range: { start: selectionRange.start },
464
- selectionRange,
465
- };
466
- names.add(name);
467
- sections[level - 1] = info;
468
- if (container) {
469
- container.children ??= [];
470
- container.children.push(info);
471
- }
472
- else {
473
- symbols.push(info);
474
- }
475
- /* NOT FOR BROWSER ONLY END */
482
+ levels[i] = undefined;
476
483
  }
484
+ levels[level - 1] = top + height - 1; // 从标题的最后一行开始折叠
477
485
  }
478
- else if (fold && height > 2) {
486
+ else if (height > 2) {
479
487
  ranges.push({
480
488
  startLine: top, // 从表格或模板的第一行开始折叠
481
489
  endLine: top + height - 2,
@@ -483,32 +491,16 @@ class LanguageService {
483
491
  });
484
492
  }
485
493
  }
486
- if (fold) {
487
- for (const startLine of levels) {
488
- if (startLine !== undefined && startLine < length - 1) {
489
- ranges.push({
490
- startLine,
491
- endLine: length - 1,
492
- kind: 'region',
493
- });
494
- }
495
- }
496
- /* NOT FOR BROWSER ONLY */
497
- }
498
- else {
499
- for (const section of sections) {
500
- getSectionEnd(section, lines, length - 1);
494
+ for (const startLine of levels) {
495
+ if (startLine !== undefined && startLine < length - 1) {
496
+ ranges.push({
497
+ startLine,
498
+ endLine: length - 1,
499
+ kind: 'region',
500
+ });
501
501
  }
502
- /* NOT FOR BROWSER ONLY END */
503
502
  }
504
- return fold ? ranges : symbols;
505
- }
506
- /**
507
- * 提供折叠范围
508
- * @param text 源代码
509
- */
510
- async provideFoldingRanges(text) {
511
- return this.#provideFoldingRangesOrDocumentSymbols(text);
503
+ return ranges;
512
504
  }
513
505
  /**
514
506
  * 提供链接
@@ -516,109 +508,79 @@ class LanguageService {
516
508
  */
517
509
  async provideLinks(text) {
518
510
  this.#checkSignature();
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,'
520
- + 'image-parameter#link';
521
- return (await this.#queue(text)).querySelectorAll(selector).flatMap((token) => {
511
+ const { articlePath, protocol } = index_1.default.getConfig(), absolute = articlePath?.includes('//'), protocolRegex = new RegExp(`^(?:${protocol}|//)`, 'iu');
512
+ return (await this.#queue(text))
513
+ .querySelectorAll(`magic-link,ext-link-url,free-ext-link,attr-value,image-parameter#link${absolute ? ',link-target,template-name,invoke-module' : ''}`).reverse()
514
+ .map((token) => {
522
515
  const { type, parentNode, firstChild, lastChild, childNodes } = token, { name, tag } = parentNode;
523
516
  if (!(type !== 'attr-value'
524
517
  || name === 'src' && ['templatestyles', 'img'].includes(tag)
525
518
  || name === 'cite' && ['blockquote', 'del', 'ins', 'q'].includes(tag))
526
519
  || !isPlain(childNodes)) {
527
- return [];
520
+ return false;
521
+ }
522
+ let target = childNodes.filter((node) => node.type === 'text')
523
+ .map(({ data }) => data).join('').trim();
524
+ if (!target) {
525
+ return false;
528
526
  }
529
- let target = type === 'image-parameter'
530
- ? element_1.AstElement.prototype.toString.call(token, true).trim()
531
- : token.toString(true).trim();
532
527
  try {
533
- if (type === 'magic-link') {
534
- if (target.startsWith('ISBN')) {
535
- target = getUrl(`Special:Booksources/${target.slice(4).replace(/[\p{Zs}\t-]/gu, '')}`);
536
- }
537
- else {
538
- target = target.startsWith('RFC')
539
- ? `https://tools.ietf.org/html/rfc${target.slice(3).trim()}`
540
- : `https://pubmed.ncbi.nlm.nih.gov/${target.slice(4).trim()}`;
528
+ if (token.is('magic-link')
529
+ || token.is('ext-link-url')
530
+ || token.is('free-ext-link')) {
531
+ target = token.getUrl(articlePath);
532
+ }
533
+ else if (type === 'link-target' && (parentNode.is('link')
534
+ || parentNode.is('redirect-target')
535
+ || parentNode.is('category'))) {
536
+ if (target.startsWith('/')) {
537
+ return false;
541
538
  }
539
+ target = parentNode.link.getUrl(articlePath);
542
540
  }
543
- else if (['link-target', 'template-name', 'invoke-module'].includes(type)
541
+ else if (type === 'template-name') {
542
+ target = parentNode.getAttribute('title').getUrl(articlePath);
543
+ }
544
+ else if (['link-target', 'invoke-module'].includes(type)
544
545
  || type === 'attr-value' && name === 'src' && tag === 'templatestyles'
545
546
  || type === 'image-parameter' && !protocolRegex.test(target)) {
546
- if (target.startsWith('/')) {
547
- return [];
547
+ if (!absolute || target.startsWith('/')) {
548
+ return false;
548
549
  }
549
550
  let ns = 0;
550
- if (type === 'template-name' || type === 'attr-value') {
551
+ if (type === 'attr-value') {
551
552
  ns = 10;
552
553
  }
553
554
  else if (type === 'invoke-module') {
554
555
  ns = 828;
555
556
  }
556
- target = getUrl(target, ns);
557
+ const title = index_1.default.normalizeTitle(target, ns);
558
+ /* istanbul ignore if */
559
+ if (!title.valid) {
560
+ return false;
561
+ }
562
+ target = title.getUrl();
557
563
  }
558
- if (target.startsWith('//')) {
564
+ if (typeof target === 'string' && target.startsWith('//')) {
559
565
  target = `https:${target}`;
560
566
  }
561
567
  target = new URL(target).href;
562
568
  if (type === 'image-parameter') {
563
- const rect = firstChild.getBoundingClientRect(), { top, left, height, width } = lastChild.getBoundingClientRect();
564
- return [
565
- {
566
- range: {
567
- start: { line: rect.top, character: rect.left },
568
- end: (0, lint_1.getEndPos)(top, left, height, width),
569
- },
570
- target,
569
+ const { top, left, height, width } = lastChild.getBoundingClientRect(), rect = firstChild.getBoundingClientRect();
570
+ return {
571
+ range: {
572
+ start: { line: rect.top, character: rect.left },
573
+ end: (0, lint_1.getEndPos)(top, left, height, width),
571
574
  },
572
- ];
575
+ target,
576
+ };
573
577
  }
574
- return [{ range: createNodeRange(token), target }];
578
+ return { range: createNodeRange(token), target };
575
579
  }
576
580
  catch {
577
- return [];
578
- }
579
- });
580
- }
581
- async #provideReferencesOrDefinition(text, position, usage) {
582
- this.#checkSignature();
583
- const renameTypes = [
584
- 'arg-name',
585
- 'template-name',
586
- 'magic-word-name',
587
- 'link-target',
588
- 'parameter-key',
589
- ], types = [
590
- 'ext',
591
- 'html',
592
- 'attr-key',
593
- 'image-parameter',
594
- 'heading-title',
595
- 'heading',
596
- ...renameTypes,
597
- ], root = await this.#queue(text), node = elementFromWord(root, position), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
598
- if (usage === 2 && type === 'parameter-key' && /^[1-9]\d*$/u.test(node.parentNode.name)
599
- || !refName && (usage === 1
600
- || !refGroup && (usage === 0
601
- ? !types.includes(type)
602
- : !renameTypes.includes(type)
603
- || type === 'link-target' && !['link', 'redirect-target'].includes(node.parentNode.type)))) {
604
- return undefined;
605
- }
606
- else if (usage === 2) {
607
- return createNodeRange(node);
608
- }
609
- const name = getName(node), refs = root.querySelectorAll(type === 'heading-title' ? 'heading' : type).filter(token => {
610
- const { name: n, parentNode } = token.parentNode;
611
- if (usage === 1) {
612
- return getRefName(token) === refName
613
- && n === 'name' && parentNode.parentNode.innerText;
581
+ return false;
614
582
  }
615
- return type === 'attr-value'
616
- ? getRefName(token) === refName || getRefGroup(token) === refGroup
617
- : getName(token) === name;
618
- }).map((token) => ({
619
- range: createNodeRange(token.type === 'parameter-key' ? token.parentNode : token),
620
- }));
621
- return refs.length === 0 ? undefined : refs;
583
+ }).filter(Boolean);
622
584
  }
623
585
  /**
624
586
  * 提供引用
@@ -626,53 +588,87 @@ class LanguageService {
626
588
  * @param position 位置
627
589
  */
628
590
  async provideReferences(text, position) {
629
- return this.#provideReferencesOrDefinition(text, position, 0);
591
+ this.#checkSignature();
592
+ const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, position), element = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, node = offset === 0 && (element.type === 'ext-attr-dirty' || element.type === 'html-attr-dirty')
593
+ ? element.parentNode.parentNode
594
+ : element, { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
595
+ if (!refName && !refGroup && !referenceTypes.has(type)) {
596
+ return undefined;
597
+ }
598
+ const name = getName(node), refs = root.querySelectorAll(type === 'heading-title' ? 'heading' : type).filter(token => type === 'attr-value'
599
+ ? getRefName(token) === refName || getRefGroup(token) === refGroup
600
+ : getName(token) === name).map((token) => ({
601
+ range: createNodeRange(token.type === 'parameter-key' ? token.parentNode : token),
602
+ }));
603
+ return refs.length === 0 ? undefined : refs;
630
604
  }
631
605
  /**
632
606
  * 提供定义
633
607
  * @param text 源代码
634
- * @param position 位置
608
+ * @param pos 位置
609
+ * @param pos.line 行号
610
+ * @param pos.character 列号
635
611
  */
636
- async provideDefinition(text, position) {
637
- return this.#provideReferencesOrDefinition(text, position, 1);
612
+ async provideDefinition(text, { line, character }) {
613
+ this.#checkSignature();
614
+ const root = await this.#queue(text), node = root.elementFromPoint(character, line), ext = node.is('ext') && node.name === 'ref' ? node : node.closest('ext#ref'), refName = getRefTagAttr(ext, 'name');
615
+ if (!refName) {
616
+ return undefined;
617
+ }
618
+ const refGroup = getRefTagAttr(ext, 'group'), refs = root.querySelectorAll('ext#ref').filter(token => token.innerText
619
+ && getRefTagAttr(token, 'name') === refName
620
+ && getRefTagAttr(token, 'group') === refGroup).map(({ lastChild }) => ({
621
+ range: createNodeRange(lastChild),
622
+ }));
623
+ return refs.length === 0 ? undefined : refs;
638
624
  }
639
625
  /**
640
626
  * 提供变量更名准备
641
627
  * @param text 源代码
642
- * @param position 位置
628
+ * @param pos 位置
629
+ * @param pos.line 行号
630
+ * @param pos.character 列号
643
631
  */
644
- async resolveRenameLocation(text, position) {
645
- return this.#provideReferencesOrDefinition(text, position, 2);
632
+ async resolveRenameLocation(text, { line, character }) {
633
+ this.#checkSignature();
634
+ const root = await this.#queue(text), node = root.elementFromPoint(character, line), { type } = node, refName = getRefName(node), refGroup = getRefGroup(node);
635
+ return !refName && !refGroup && (!renameTypes.has(type)
636
+ || type === 'parameter-key' && /^[1-9]\d*$/u.test(node.parentNode.name)
637
+ || type === 'link-target' && !['link', 'redirect-target'].includes(node.parentNode.type))
638
+ ? undefined
639
+ : createNodeRange(node);
646
640
  }
647
641
  /**
648
642
  * 变量更名
649
643
  * @param text 源代码
650
- * @param position 位置
644
+ * @param pos 位置
645
+ * @param pos.line 行号
646
+ * @param pos.character 列号
651
647
  * @param newName 新名称
652
648
  */
653
- async provideRenameEdits(text, position, newName) {
649
+ async provideRenameEdits(text, { line, character }, newName) {
654
650
  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 => {
651
+ const root = await this.#queue(text), node = root.elementFromPoint(character, line), { type } = node, refName = getRefName(node), refNameGroup = refName && getRefTagAttr(node.parentNode.parentNode, 'group'), refGroup = getRefGroup(node), name = getName(node), refs = root.querySelectorAll(type).filter(token => {
657
652
  const { type: t } = token.parentNode;
658
653
  if (type === 'link-target' && t !== 'link' && t !== 'redirect-target') {
659
654
  return false;
660
655
  }
661
656
  return type === 'attr-value'
662
- ? getRefName(token) === refName || getRefGroup(token) === refGroup
657
+ ? getRefGroup(token) === refGroup
658
+ || getRefName(token) === refName
659
+ && getRefTagAttr(token.parentNode.parentNode, 'group') === refNameGroup
663
660
  : getName(token) === name;
664
661
  });
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
- };
662
+ return refs.length === 0
663
+ ? undefined
664
+ : {
665
+ changes: {
666
+ '': refs.map((ref) => ({
667
+ range: createNodeRange(ref),
668
+ newText: newName,
669
+ })),
670
+ },
671
+ };
676
672
  }
677
673
  /**
678
674
  * 检索解析器函数
@@ -692,14 +688,33 @@ class LanguageService {
692
688
  return undefined;
693
689
  }
694
690
  this.#checkSignature();
695
- const token = elementFromWord(await this.#queue(text), position);
696
- let info, f;
691
+ const root = await this.#queue(text), { offsetNode, offset } = caretPositionFromWord(root, position), token = offsetNode.type === 'text' ? offsetNode.parentNode : offsetNode, { type, parentNode, length, name } = token;
692
+ let info, f, range;
697
693
  if (token.is('double-underscore')) {
694
+ if (offset === 0 && token.getBoundingClientRect().left > position.character) {
695
+ return undefined;
696
+ }
698
697
  info = this.data.behaviorSwitches.find(({ aliases }) => aliases.includes(token.innerText.toLowerCase()));
699
698
  }
700
- else if (token.type === 'magic-word-name') {
701
- info = this.#getParserFunction(token.parentNode.name);
702
- f = token.text().trim();
699
+ else if (type === 'magic-word-name') {
700
+ info = this.#getParserFunction(parentNode.name);
701
+ f = token.toString(true).trim();
702
+ }
703
+ else if (token.is('magic-word') && length === 1 && !token.modifier) {
704
+ info = this.#getParserFunction(name);
705
+ f = token.firstChild.toString(true).trim();
706
+ }
707
+ else if ((token.is('magic-word') || token.is('template'))
708
+ && token.modifier && offset >= 2 && token.getRelativeIndex(0) > offset) {
709
+ f = token.modifier.trim().slice(0, -1);
710
+ info = this.#getParserFunction(f.toLowerCase());
711
+ if (info) {
712
+ const aIndex = token.getAbsoluteIndex();
713
+ range = {
714
+ start: positionAt(root, aIndex + 2),
715
+ end: positionAt(root, aIndex + token.modifier.trimEnd().length + 1),
716
+ };
717
+ }
703
718
  }
704
719
  return info && {
705
720
  contents: {
@@ -709,7 +724,7 @@ class LanguageService {
709
724
  : '')
710
725
  + info.description,
711
726
  },
712
- range: createNodeRange(token),
727
+ range: range ?? createNodeRange(token),
713
728
  };
714
729
  }
715
730
  /**
@@ -727,32 +742,41 @@ class LanguageService {
727
742
  throw new Error('This is a regular language server!');
728
743
  }
729
744
  this.#signature = true;
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;
731
- if (type !== 'magic-word') {
745
+ 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)}}}`);
746
+ if (!lastChild.is('magic-word') || lastChild.length === 1) {
732
747
  return undefined;
733
748
  }
734
- const info = this.#getParserFunction(name);
749
+ const { name, childNodes, firstChild } = lastChild, info = this.#getParserFunction(name);
735
750
  if (!info?.signatures) {
736
751
  return undefined;
737
752
  }
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)
753
+ const n = childNodes.length - 1, candidates = info.signatures.filter(params => (params.length >= n || params[params.length - 1]?.rest)
744
754
  && params.every(({ label, const: c }, i) => {
745
- const p = c && i < n && childNodes[i + 1]?.text().trim();
746
- return !p || label.startsWith(p) || label.startsWith(p.toLowerCase());
747
- })).map((params) => ({
748
- label: `{{${f}${params.length === 0 ? '' : ':'}${params.map(({ label }) => label).join('|')}}}`,
749
- parameters: params.map(({ label, const: c }) => ({
750
- label,
751
- ...c ? { documentation: 'Predefined parameter' } : undefined,
755
+ const p = c && i < n && childNodes[i + 1]?.toString(true).trim();
756
+ return !p || label.toLowerCase().includes(p.toLowerCase());
757
+ }));
758
+ if (candidates.length === 0) {
759
+ return undefined;
760
+ }
761
+ let j = -1;
762
+ for (let cur = lastChild.getAbsoluteIndex() + lastChild.getAttribute('padding'); j < n; j++) {
763
+ cur += childNodes[j + 1].toString().length + 1;
764
+ if (cur > character) {
765
+ break;
766
+ }
767
+ }
768
+ const f = firstChild.toString(true).trim();
769
+ return {
770
+ signatures: candidates.map((params) => ({
771
+ label: `{{${f}${params.length === 0 ? '' : ':'}${params.map(({ label }) => label).join('|')}}}`,
772
+ parameters: params.map(({ label, const: c }) => ({
773
+ label,
774
+ ...c ? { documentation: 'Predefined parameter' } : undefined,
775
+ })),
776
+ ...params.length < n ? { activeParameter: Math.min(j, params.length - 1) } : undefined,
752
777
  })),
753
- ...params.length < n ? { activeParameter: Math.min(activeParameter, params.length - 1) } : undefined,
754
- }));
755
- return { signatures, activeParameter };
778
+ activeParameter: j,
779
+ };
756
780
  }
757
781
  /**
758
782
  * 提供 CodeLens
@@ -764,7 +788,7 @@ class LanguageService {
764
788
  for (const template of root.querySelectorAll('template,magic-word#invoke')) {
765
789
  const { type, childNodes } = template;
766
790
  hints.push(...childNodes.slice(type === 'template' ? 1 : 3).filter(({ anon }) => anon)
767
- .map((parameter) => ({
791
+ .reverse().map((parameter) => ({
768
792
  position: positionAt(root, parameter.getAbsoluteIndex()),
769
793
  label: `${parameter.name}=`,
770
794
  kind: 2,
@@ -791,7 +815,46 @@ class LanguageService {
791
815
  * @param text 源代码
792
816
  */
793
817
  async provideDocumentSymbols(text) {
794
- return this.#provideFoldingRangesOrDocumentSymbols(text, false);
818
+ this.#checkSignature();
819
+ const root = await this.#queue(text), lines = root.getLines(), { length } = lines, symbols = [], names = new Set(), sections = new Array(6), tokens = root.querySelectorAll('heading-title');
820
+ for (const token of [...tokens].reverse()) { // 提高 getBoundingClientRect 的性能
821
+ token.getRelativeIndex();
822
+ }
823
+ for (const token of tokens) {
824
+ const { top, height, left, width } = token.getBoundingClientRect();
825
+ if (token.type === 'heading-title') {
826
+ const { level } = token.parentNode;
827
+ for (let i = level - 1; i < 6; i++) {
828
+ getSectionEnd(sections[i], lines, top - 1);
829
+ sections[i] = undefined;
830
+ }
831
+ const section = token.text().trim() || ' ', name = names.has(section)
832
+ ? new Array(names.size).fill('').map((_, i) => `${section.trim()}_${i + 2}`)
833
+ .find(s => !names.has(s))
834
+ : section, container = sections.slice(0, level - 1).reverse().find(Boolean), selectionRange = {
835
+ start: { line: top, character: left - level },
836
+ end: (0, lint_1.getEndPos)(top, left, height, width + level),
837
+ }, info = {
838
+ name,
839
+ kind: 15,
840
+ range: { start: selectionRange.start },
841
+ selectionRange,
842
+ };
843
+ names.add(name);
844
+ sections[level - 1] = info;
845
+ if (container) {
846
+ container.children ??= [];
847
+ container.children.push(info);
848
+ }
849
+ else {
850
+ symbols.push(info);
851
+ }
852
+ }
853
+ }
854
+ for (const section of sections) {
855
+ getSectionEnd(section, lines, length - 1);
856
+ }
857
+ return symbols;
795
858
  }
796
859
  }
797
860
  exports.LanguageService = LanguageService;