@tsuzuri-lab/sed-language-server 0.2.0 → 0.3.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/README.md CHANGED
@@ -22,6 +22,7 @@ sed-language-server --stdio
22
22
 
23
23
  - Tree-sitter-based diagnostics
24
24
  - go to definition from `b`, `t`, and GNU `T` label references
25
+ - document formatting for command lists and nested blocks
25
26
 
26
27
  Scripts are analyzed without executing them or accessing referenced files.
27
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsuzuri-lab/sed-language-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Language server for POSIX sed with opt-in GNU syntax support.",
5
5
  "keywords": [
6
6
  "sed",
@@ -0,0 +1,35 @@
1
+ import { rangeForNode, syntaxTreeFor } from "./syntax.js";
2
+
3
+ function labelReferenceAt(rootNode, offset) {
4
+ let node = rootNode.namedDescendantForIndex(offset);
5
+ while (node !== null) {
6
+ if (
7
+ node.type === "label_reference" &&
8
+ node.startIndex <= offset &&
9
+ offset < node.endIndex
10
+ ) {
11
+ return node;
12
+ }
13
+ node = node.parent;
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ export function createDefinitionLocations(document, position, dialect) {
19
+ const rootNode = syntaxTreeFor(document, dialect).rootNode;
20
+ const reference = labelReferenceAt(rootNode, document.offsetAt(position));
21
+
22
+ if (reference === undefined) {
23
+ return null;
24
+ }
25
+
26
+ const locations = rootNode
27
+ .descendantsOfType("label_definition")
28
+ .filter((definition) => definition.text === reference.text)
29
+ .map((definition) => ({
30
+ uri: document.uri,
31
+ range: rangeForNode(document, definition),
32
+ }));
33
+
34
+ return locations.length === 0 ? null : locations;
35
+ }
@@ -0,0 +1,189 @@
1
+ import { DiagnosticSeverity } from "vscode-languageserver/node";
2
+ import {
3
+ collectSyntaxIssueNodes,
4
+ rangeForNode,
5
+ syntaxTreeFor,
6
+ } from "./syntax.js";
7
+
8
+ const diagnosticSource = "sed-language-server";
9
+
10
+ const issueDescriptions = {
11
+ incomplete_escape: {
12
+ code: "incomplete-escape",
13
+ message: "This escape sequence is incomplete.",
14
+ },
15
+ incomplete_regex: {
16
+ code: "unterminated-regular-expression",
17
+ message: "This regular expression is not terminated.",
18
+ },
19
+ incomplete_replacement: {
20
+ code: "unterminated-replacement",
21
+ message: "This replacement is not terminated.",
22
+ },
23
+ incomplete_translate: {
24
+ code: "unterminated-translation",
25
+ message: "This translation is not terminated.",
26
+ },
27
+ invalid_control_escape: {
28
+ code: "invalid-control-escape",
29
+ message: "This GNU sed control escape is invalid.",
30
+ },
31
+ unclosed_bracket: {
32
+ code: "unclosed-bracket-expression",
33
+ message: "This bracket expression is not terminated.",
34
+ },
35
+ invalid_command: (node, dialect) => ({
36
+ code: "invalid-command",
37
+ message: `Unknown ${dialectName(dialect)} sed command: \`${node.text}\`.`,
38
+ }),
39
+ invalid_flag: (node, dialect) => ({
40
+ code: "invalid-substitution-flag",
41
+ message: `Unknown ${dialectName(dialect)} sed substitution flag: \`${node.text}\`.`,
42
+ }),
43
+ invalid_flags: (node, dialect) => ({
44
+ code: "invalid-substitution-flag",
45
+ message: `The ${dialectName(dialect)} sed substitution flags \`${node.text}\` contain invalid characters.`,
46
+ }),
47
+ unexpected_text: (_node, dialect) => ({
48
+ code: "unexpected-text",
49
+ message: `Unexpected text after this ${dialectName(dialect)} sed command.`,
50
+ }),
51
+ };
52
+
53
+ function dialectName(dialect) {
54
+ return dialect === "gnu" ? "GNU" : "POSIX";
55
+ }
56
+
57
+ function descriptionFor(node, dialect) {
58
+ const description = issueDescriptions[node.type];
59
+ if (typeof description === "function") {
60
+ return description(node, dialect);
61
+ }
62
+ if (description !== undefined) {
63
+ return description;
64
+ }
65
+
66
+ if (node.isMissing) {
67
+ return {
68
+ code: "missing-syntax",
69
+ message: `Expected ${node.type.replaceAll("_", " ")}.`,
70
+ };
71
+ }
72
+
73
+ return {
74
+ code: "syntax-error",
75
+ message: `Invalid ${dialectName(dialect)} sed syntax.`,
76
+ };
77
+ }
78
+
79
+ function issueSpecificity(node) {
80
+ if (node.isError) {
81
+ return 0;
82
+ }
83
+ if (node.isMissing) {
84
+ return 1;
85
+ }
86
+ return 2;
87
+ }
88
+
89
+ function compareIssueRanges(left, right) {
90
+ return left.startIndex - right.startIndex || left.endIndex - right.endIndex;
91
+ }
92
+
93
+ function preferSpecificIssues(nodes) {
94
+ const issuesByRange = new Map();
95
+
96
+ for (const node of nodes) {
97
+ const key = `${node.startIndex}:${node.endIndex}`;
98
+ const existing = issuesByRange.get(key);
99
+ if (
100
+ existing === undefined ||
101
+ issueSpecificity(node) > issueSpecificity(existing)
102
+ ) {
103
+ issuesByRange.set(key, node);
104
+ }
105
+ }
106
+
107
+ return [...issuesByRange.values()].sort(compareIssueRanges);
108
+ }
109
+
110
+ function collapseInvalidFlags(nodes) {
111
+ const groupedByParent = new Map();
112
+ const issues = [];
113
+
114
+ for (const node of nodes) {
115
+ const parent = node.parent;
116
+ if (node.type !== "invalid_flag" || parent?.type !== "substitute_flags") {
117
+ issues.push(node);
118
+ continue;
119
+ }
120
+
121
+ const key = `${parent.startIndex}:${parent.endIndex}`;
122
+ const group = groupedByParent.get(key);
123
+ if (group === undefined) {
124
+ groupedByParent.set(key, [node]);
125
+ } else {
126
+ group.push(node);
127
+ }
128
+ }
129
+
130
+ for (const invalidFlags of groupedByParent.values()) {
131
+ if (invalidFlags.length === 1) {
132
+ issues.push(invalidFlags[0]);
133
+ continue;
134
+ }
135
+
136
+ const first = invalidFlags[0];
137
+ const last = invalidFlags.at(-1);
138
+ issues.push({
139
+ type: "invalid_flags",
140
+ startIndex: first.startIndex,
141
+ endIndex: last.endIndex,
142
+ text: first.parent.text,
143
+ });
144
+ }
145
+
146
+ return issues.sort(compareIssueRanges);
147
+ }
148
+
149
+ function mergeAdjacentIssues(nodes) {
150
+ const merged = [];
151
+
152
+ for (const node of nodes) {
153
+ const previous = merged.at(-1);
154
+ if (previous === undefined || node.startIndex > previous.endIndex) {
155
+ merged.push(node);
156
+ continue;
157
+ }
158
+
159
+ merged[merged.length - 1] = {
160
+ type: previous.type,
161
+ text: previous.text,
162
+ isMissing: previous.isMissing,
163
+ startIndex: previous.startIndex,
164
+ endIndex: Math.max(previous.endIndex, node.endIndex),
165
+ };
166
+ }
167
+
168
+ return merged;
169
+ }
170
+
171
+ function normalizeIssues(nodes) {
172
+ return mergeAdjacentIssues(collapseInvalidFlags(preferSpecificIssues(nodes)));
173
+ }
174
+
175
+ export function createDiagnostics(document, dialect) {
176
+ const rootNode = syntaxTreeFor(document, dialect).rootNode;
177
+ const issueNodes = normalizeIssues(collectSyntaxIssueNodes(rootNode));
178
+
179
+ return issueNodes.map((node) => {
180
+ const { code, message } = descriptionFor(node, dialect);
181
+ return {
182
+ severity: DiagnosticSeverity.Error,
183
+ range: rangeForNode(document, node),
184
+ message,
185
+ code,
186
+ source: diagnosticSource,
187
+ };
188
+ });
189
+ }
@@ -0,0 +1,100 @@
1
+ import { collectSyntaxIssueNodes, syntaxTreeFor } from "./syntax.js";
2
+
3
+ function formatCommandList(node, indentation, lineEnding, depth) {
4
+ return node.children
5
+ .filter((child) => child.type === "command")
6
+ .map((command) => formatCommand(command, indentation, lineEnding, depth))
7
+ .join(lineEnding);
8
+ }
9
+
10
+ function formatCommand(command, indentation, lineEnding, depth) {
11
+ const prefix = indentation.repeat(depth);
12
+ const body = command.childForFieldName("body");
13
+ if (body.type !== "block_command") {
14
+ return `${prefix}${command.text}`;
15
+ }
16
+
17
+ const openingBrace = body.childForFieldName("name");
18
+ const opening = command.text.slice(
19
+ 0,
20
+ openingBrace.endIndex - command.startIndex,
21
+ );
22
+ const commandList = body.childForFieldName("argument");
23
+ if (commandList === null) {
24
+ return `${prefix}${opening}}`;
25
+ }
26
+
27
+ const formattedCommands = formatCommandList(
28
+ commandList,
29
+ indentation,
30
+ lineEnding,
31
+ depth + 1,
32
+ );
33
+ const content =
34
+ formattedCommands === "" ? "" : `${formattedCommands}${lineEnding}`;
35
+
36
+ return `${prefix}${opening}${lineEnding}${content}${prefix}}`;
37
+ }
38
+
39
+ function indentationFor(options) {
40
+ return options.insertSpaces ? " ".repeat(options.tabSize) : "\t";
41
+ }
42
+
43
+ function formatScript(rootNode, source, options) {
44
+ const lineEnding = source.match(/\r\n|\n/)?.[0] ?? "\n";
45
+ const indentation = indentationFor(options);
46
+ const firstLine = rootNode.children.find(
47
+ (child) => child.type === "first_line_silent",
48
+ );
49
+ const commandList = rootNode.children.find(
50
+ (child) => child.type === "command_list",
51
+ );
52
+ const sections = [];
53
+
54
+ if (firstLine !== undefined) {
55
+ sections.push(firstLine.text);
56
+ }
57
+ if (commandList !== undefined) {
58
+ const formattedCommands = formatCommandList(
59
+ commandList,
60
+ indentation,
61
+ lineEnding,
62
+ 0,
63
+ );
64
+ if (formattedCommands !== "") {
65
+ sections.push(formattedCommands);
66
+ }
67
+ }
68
+
69
+ let formatted = sections.join(lineEnding);
70
+ if (source.endsWith("\n") && !formatted.endsWith("\n")) {
71
+ formatted += lineEnding;
72
+ }
73
+ return formatted;
74
+ }
75
+
76
+ export function createFormattingEdits(document, dialect, options) {
77
+ const source = document.getText();
78
+ const rootNode = syntaxTreeFor(document, dialect).rootNode;
79
+ if (rootNode.hasError || collectSyntaxIssueNodes(rootNode).length > 0) {
80
+ return [];
81
+ }
82
+
83
+ const formatted = formatScript(rootNode, source, options);
84
+ if (!source.startsWith("#n") && formatted.startsWith("#n")) {
85
+ return [];
86
+ }
87
+ if (formatted === source) {
88
+ return [];
89
+ }
90
+
91
+ return [
92
+ {
93
+ range: {
94
+ start: document.positionAt(0),
95
+ end: document.positionAt(source.length),
96
+ },
97
+ newText: formatted,
98
+ },
99
+ ];
100
+ }
package/src/server.js CHANGED
@@ -3,19 +3,16 @@
3
3
  import {
4
4
  createConnection,
5
5
  ErrorCodes,
6
- MessageType,
7
6
  ProposedFeatures,
8
7
  ResponseError,
9
- ShowMessageNotification,
10
8
  TextDocumentSyncKind,
11
9
  TextDocuments,
12
10
  } from "vscode-languageserver/node";
13
11
  import { TextDocument } from "vscode-languageserver-textdocument";
14
- import {
15
- createDefinitionLocations,
16
- createDiagnostics,
17
- invalidateSyntaxTreeCache,
18
- } from "./analysis.js";
12
+ import { createDefinitionLocations } from "./definition.js";
13
+ import { createDiagnostics } from "./diagnostics.js";
14
+ import { createFormattingEdits } from "./formatting.js";
15
+ import { invalidateSyntaxTreeCache } from "./syntax.js";
19
16
 
20
17
  if (process.argv.length === 2) {
21
18
  process.argv.push("--stdio");
@@ -47,19 +44,6 @@ function resolveDialect(options) {
47
44
  return { dialect };
48
45
  }
49
46
 
50
- function configurationOptions(settings) {
51
- if (
52
- settings !== null &&
53
- typeof settings === "object" &&
54
- !Array.isArray(settings) &&
55
- Object.hasOwn(settings, "sedLanguageServer")
56
- ) {
57
- return settings.sedLanguageServer;
58
- }
59
-
60
- return settings;
61
- }
62
-
63
47
  function publishDiagnostics(document) {
64
48
  return connection.sendDiagnostics({
65
49
  uri: document.uri,
@@ -81,26 +65,11 @@ connection.onInitialize(({ initializationOptions }) => {
81
65
  capabilities: {
82
66
  textDocumentSync: TextDocumentSyncKind.Incremental,
83
67
  definitionProvider: true,
68
+ documentFormattingProvider: true,
84
69
  },
85
70
  };
86
71
  });
87
72
 
88
- connection.onDidChangeConfiguration(async ({ settings }) => {
89
- const result = resolveDialect(configurationOptions(settings));
90
- if (result.error !== undefined) {
91
- connection.sendNotification(ShowMessageNotification.type, {
92
- type: MessageType.Error,
93
- message: result.error,
94
- });
95
- return;
96
- }
97
-
98
- activeDialect = result.dialect;
99
- await Promise.all(
100
- documents.all().map((document) => publishDiagnostics(document)),
101
- );
102
- });
103
-
104
73
  connection.onDefinition(({ textDocument, position }) => {
105
74
  const document = documents.get(textDocument.uri);
106
75
  return document === undefined
@@ -108,6 +77,13 @@ connection.onDefinition(({ textDocument, position }) => {
108
77
  : createDefinitionLocations(document, position, activeDialect);
109
78
  });
110
79
 
80
+ connection.onDocumentFormatting(({ textDocument, options }) => {
81
+ const document = documents.get(textDocument.uri);
82
+ return document === undefined
83
+ ? []
84
+ : createFormattingEdits(document, activeDialect, options);
85
+ });
86
+
111
87
  documents.onDidChangeContent(({ document }) => {
112
88
  publishDiagnostics(document);
113
89
  });
package/src/syntax.js ADDED
@@ -0,0 +1,124 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { Language, Parser } from "web-tree-sitter";
3
+
4
+ await Parser.init();
5
+
6
+ async function createParser(dialect) {
7
+ const path = fileURLToPath(
8
+ new URL(`../vendor/tree-sitter-sed-${dialect}.wasm`, import.meta.url),
9
+ );
10
+ return new Parser().setLanguage(await Language.load(path));
11
+ }
12
+
13
+ const parsers = {
14
+ posix: await createParser("posix"),
15
+ gnu: await createParser("gnu"),
16
+ };
17
+
18
+ const documentTrees = new Map();
19
+
20
+ export function syntaxTreeFor(document, dialect) {
21
+ const source = document.getText();
22
+ let trees = documentTrees.get(document.uri);
23
+
24
+ if (trees === undefined) {
25
+ trees = new Map();
26
+ documentTrees.set(document.uri, trees);
27
+ }
28
+
29
+ const cached = trees.get(dialect);
30
+ if (
31
+ cached !== undefined &&
32
+ cached.version === document.version &&
33
+ cached.source === source
34
+ ) {
35
+ return cached.tree;
36
+ }
37
+
38
+ cached?.tree.delete();
39
+ const tree = parsers[dialect].parse(source);
40
+ if (tree === null) {
41
+ throw new Error(`Failed to parse ${dialect} sed source.`);
42
+ }
43
+
44
+ trees.set(dialect, {
45
+ source,
46
+ tree,
47
+ version: document.version,
48
+ });
49
+ return tree;
50
+ }
51
+
52
+ export function invalidateSyntaxTreeCache(document) {
53
+ const trees = documentTrees.get(document.uri);
54
+ if (trees !== undefined) {
55
+ for (const { tree } of trees.values()) {
56
+ tree.delete();
57
+ }
58
+ documentTrees.delete(document.uri);
59
+ }
60
+ }
61
+
62
+ function isSpecificSyntaxIssue(node) {
63
+ return (
64
+ node.isMissing ||
65
+ node.type.startsWith("incomplete_") ||
66
+ node.type.startsWith("invalid_") ||
67
+ node.type === "unclosed_bracket" ||
68
+ node.type === "unexpected_text"
69
+ );
70
+ }
71
+
72
+ export function collectSyntaxIssueNodes(rootNode) {
73
+ const issues = [];
74
+ const stack = [
75
+ {
76
+ node: rootNode,
77
+ children: rootNode.children,
78
+ childIndex: 0,
79
+ hasDescendantIssue: false,
80
+ issueStartIndex: 0,
81
+ },
82
+ ];
83
+
84
+ while (stack.length > 0) {
85
+ const frame = stack.at(-1);
86
+ if (frame.childIndex < frame.children.length) {
87
+ const child = frame.children[frame.childIndex];
88
+ frame.childIndex += 1;
89
+ stack.push({
90
+ node: child,
91
+ children: child.children,
92
+ childIndex: 0,
93
+ hasDescendantIssue: false,
94
+ issueStartIndex: issues.length,
95
+ });
96
+ continue;
97
+ }
98
+
99
+ const specific = isSpecificSyntaxIssue(frame.node);
100
+ const selected =
101
+ specific || (frame.node.hasError && !frame.hasDescendantIssue);
102
+ if (specific) {
103
+ issues.length = frame.issueStartIndex;
104
+ }
105
+ if (selected) {
106
+ issues.push(frame.node);
107
+ }
108
+
109
+ stack.pop();
110
+ const parent = stack.at(-1);
111
+ if (parent !== undefined) {
112
+ parent.hasDescendantIssue ||= frame.hasDescendantIssue || selected;
113
+ }
114
+ }
115
+
116
+ return issues;
117
+ }
118
+
119
+ export function rangeForNode(document, node) {
120
+ return {
121
+ start: document.positionAt(node.startIndex),
122
+ end: document.positionAt(node.endIndex),
123
+ };
124
+ }
package/src/analysis.js DELETED
@@ -1,275 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import { DiagnosticSeverity } from "vscode-languageserver/node";
3
- import { Language, Parser } from "web-tree-sitter";
4
-
5
- await Parser.init();
6
-
7
- async function createParser(dialect) {
8
- const path = fileURLToPath(
9
- new URL(`../vendor/tree-sitter-sed-${dialect}.wasm`, import.meta.url),
10
- );
11
- return new Parser().setLanguage(await Language.load(path));
12
- }
13
-
14
- const parsers = {
15
- posix: await createParser("posix"),
16
- gnu: await createParser("gnu"),
17
- };
18
-
19
- const documentTrees = new Map();
20
-
21
- function parseDocument(document, dialect) {
22
- const source = document.getText();
23
- let trees = documentTrees.get(document.uri);
24
-
25
- if (trees === undefined) {
26
- trees = new Map();
27
- documentTrees.set(document.uri, trees);
28
- }
29
-
30
- const cached = trees.get(dialect);
31
- if (
32
- cached !== undefined &&
33
- cached.version === document.version &&
34
- cached.source === source
35
- ) {
36
- return cached.tree;
37
- }
38
-
39
- cached?.tree.delete();
40
- const tree = parsers[dialect].parse(source);
41
- if (tree === null) {
42
- throw new Error(`Failed to parse ${dialect} sed source.`);
43
- }
44
-
45
- trees.set(dialect, {
46
- source,
47
- tree,
48
- version: document.version,
49
- });
50
- return tree;
51
- }
52
-
53
- export function invalidateSyntaxTreeCache(document) {
54
- const trees = documentTrees.get(document.uri);
55
- if (trees !== undefined) {
56
- for (const { tree } of trees.values()) {
57
- tree.delete();
58
- }
59
- documentTrees.delete(document.uri);
60
- }
61
- }
62
-
63
- const diagnosticSource = "sed-language-server";
64
-
65
- const issueDescriptions = {
66
- incomplete_escape: {
67
- code: "incomplete-escape",
68
- message: "This escape sequence is incomplete.",
69
- },
70
- incomplete_regex: {
71
- code: "unterminated-regular-expression",
72
- message: "This regular expression is not terminated.",
73
- },
74
- incomplete_replacement: {
75
- code: "unterminated-replacement",
76
- message: "This replacement is not terminated.",
77
- },
78
- incomplete_translate: {
79
- code: "unterminated-translation",
80
- message: "This translation is not terminated.",
81
- },
82
- invalid_control_escape: {
83
- code: "invalid-control-escape",
84
- message: "This GNU sed control escape is invalid.",
85
- },
86
- unclosed_bracket: {
87
- code: "unclosed-bracket-expression",
88
- message: "This bracket expression is not terminated.",
89
- },
90
- invalid_command: (node, dialect) => ({
91
- code: "invalid-command",
92
- message: `Unknown ${dialectName(dialect)} sed command: \`${node.text}\`.`,
93
- }),
94
- invalid_flag: (node, dialect) => ({
95
- code: "invalid-substitution-flag",
96
- message: `Unknown ${dialectName(dialect)} sed substitution flag: \`${node.text}\`.`,
97
- }),
98
- unexpected_text: (_node, dialect) => ({
99
- code: "unexpected-text",
100
- message: `Unexpected text after this ${dialectName(dialect)} sed command.`,
101
- }),
102
- };
103
-
104
- function dialectName(dialect) {
105
- return dialect === "gnu" ? "GNU" : "POSIX";
106
- }
107
-
108
- function descriptionFor(node, dialect) {
109
- const description = issueDescriptions[node.type];
110
- if (typeof description === "function") {
111
- return description(node, dialect);
112
- }
113
- if (description !== undefined) {
114
- return description;
115
- }
116
-
117
- const displayName = dialectName(dialect);
118
- if (node.isMissing) {
119
- return {
120
- code: "missing-syntax",
121
- message: `Expected ${node.type.replaceAll("_", " ")}.`,
122
- };
123
- }
124
-
125
- return {
126
- code: "syntax-error",
127
- message: `Invalid ${displayName} sed syntax.`,
128
- };
129
- }
130
-
131
- function isSpecificIssue(node) {
132
- return node.isMissing || Object.hasOwn(issueDescriptions, node.type);
133
- }
134
-
135
- function collectIssueNodes(rootNode) {
136
- const issues = [];
137
- const stack = [
138
- {
139
- node: rootNode,
140
- children: rootNode.children,
141
- childIndex: 0,
142
- hasDescendantIssue: false,
143
- issueStartIndex: 0,
144
- },
145
- ];
146
-
147
- while (stack.length > 0) {
148
- const frame = stack.at(-1);
149
- if (frame.childIndex < frame.children.length) {
150
- const child = frame.children[frame.childIndex];
151
- frame.childIndex += 1;
152
- stack.push({
153
- node: child,
154
- children: child.children,
155
- childIndex: 0,
156
- hasDescendantIssue: false,
157
- issueStartIndex: issues.length,
158
- });
159
- continue;
160
- }
161
-
162
- const specific = isSpecificIssue(frame.node);
163
- const selected =
164
- specific || (frame.node.isError && !frame.hasDescendantIssue);
165
- if (specific) {
166
- issues.length = frame.issueStartIndex;
167
- }
168
- if (selected) {
169
- issues.push(frame.node);
170
- }
171
-
172
- stack.pop();
173
- const parent = stack.at(-1);
174
- if (parent !== undefined) {
175
- parent.hasDescendantIssue ||= frame.hasDescendantIssue || selected;
176
- }
177
- }
178
-
179
- return issues;
180
- }
181
-
182
- function issuePriority(node) {
183
- if (node.isError) {
184
- return 0;
185
- }
186
- if (node.isMissing) {
187
- return 1;
188
- }
189
- return 2;
190
- }
191
-
192
- function preferSpecificIssues(nodes) {
193
- const issuesByRange = new Map();
194
-
195
- for (const node of nodes) {
196
- const key = `${node.startIndex}:${node.endIndex}`;
197
- const existing = issuesByRange.get(key);
198
- if (
199
- existing === undefined ||
200
- issuePriority(node) > issuePriority(existing)
201
- ) {
202
- issuesByRange.set(key, node);
203
- }
204
- }
205
-
206
- return [...issuesByRange.values()].sort(
207
- (left, right) =>
208
- left.startIndex - right.startIndex || left.endIndex - right.endIndex,
209
- );
210
- }
211
-
212
- function rangeForNode(document, node) {
213
- return {
214
- start: document.positionAt(node.startIndex),
215
- end: document.positionAt(node.endIndex),
216
- };
217
- }
218
-
219
- function collectLabels(rootNode, offset) {
220
- const definitions = [];
221
- let reference;
222
- const stack = [rootNode];
223
-
224
- while (stack.length > 0) {
225
- const node = stack.pop();
226
- if (node.type === "label_definition") {
227
- definitions.push(node);
228
- } else if (
229
- node.type === "label_reference" &&
230
- node.startIndex <= offset &&
231
- offset < node.endIndex
232
- ) {
233
- reference = node;
234
- }
235
- stack.push(...node.children.toReversed());
236
- }
237
-
238
- return { definitions, reference };
239
- }
240
-
241
- export function createDiagnostics(document, dialect) {
242
- const tree = parseDocument(document, dialect);
243
-
244
- return preferSpecificIssues(collectIssueNodes(tree.rootNode)).map((node) => {
245
- const { code, message } = descriptionFor(node, dialect);
246
- return {
247
- severity: DiagnosticSeverity.Error,
248
- range: rangeForNode(document, node),
249
- message,
250
- code,
251
- source: diagnosticSource,
252
- };
253
- });
254
- }
255
-
256
- export function createDefinitionLocations(document, position, dialect) {
257
- const tree = parseDocument(document, dialect);
258
- const { definitions, reference } = collectLabels(
259
- tree.rootNode,
260
- document.offsetAt(position),
261
- );
262
-
263
- if (reference === undefined) {
264
- return null;
265
- }
266
-
267
- const locations = definitions
268
- .filter((definition) => definition.text === reference.text)
269
- .map((definition) => ({
270
- uri: document.uri,
271
- range: rangeForNode(document, definition),
272
- }));
273
-
274
- return locations.length === 0 ? null : locations;
275
- }