@tsuzuri-lab/sed-language-server 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/definition.js +44 -0
- package/src/diagnostics.js +119 -0
- package/src/formatting.js +97 -0
- package/src/server.js +12 -5
- package/src/syntax.js +124 -0
- package/src/analysis.js +0 -275
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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { rangeForNode, syntaxTreeFor } from "./syntax.js";
|
|
2
|
+
|
|
3
|
+
function collectLabels(rootNode, offset) {
|
|
4
|
+
const definitions = [];
|
|
5
|
+
let reference;
|
|
6
|
+
const stack = [rootNode];
|
|
7
|
+
|
|
8
|
+
while (stack.length > 0) {
|
|
9
|
+
const node = stack.pop();
|
|
10
|
+
if (node.type === "label_definition") {
|
|
11
|
+
definitions.push(node);
|
|
12
|
+
} else if (
|
|
13
|
+
node.type === "label_reference" &&
|
|
14
|
+
node.startIndex <= offset &&
|
|
15
|
+
offset < node.endIndex
|
|
16
|
+
) {
|
|
17
|
+
reference = node;
|
|
18
|
+
}
|
|
19
|
+
stack.push(...node.children.toReversed());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { definitions, reference };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createDefinitionLocations(document, position, dialect) {
|
|
26
|
+
const tree = syntaxTreeFor(document, dialect);
|
|
27
|
+
const { definitions, reference } = collectLabels(
|
|
28
|
+
tree.rootNode,
|
|
29
|
+
document.offsetAt(position),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (reference === undefined) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const locations = definitions
|
|
37
|
+
.filter((definition) => definition.text === reference.text)
|
|
38
|
+
.map((definition) => ({
|
|
39
|
+
uri: document.uri,
|
|
40
|
+
range: rangeForNode(document, definition),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return locations.length === 0 ? null : locations;
|
|
44
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
unexpected_text: (_node, dialect) => ({
|
|
44
|
+
code: "unexpected-text",
|
|
45
|
+
message: `Unexpected text after this ${dialectName(dialect)} sed command.`,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function dialectName(dialect) {
|
|
50
|
+
return dialect === "gnu" ? "GNU" : "POSIX";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function descriptionFor(node, dialect) {
|
|
54
|
+
const description = issueDescriptions[node.type];
|
|
55
|
+
if (typeof description === "function") {
|
|
56
|
+
return description(node, dialect);
|
|
57
|
+
}
|
|
58
|
+
if (description !== undefined) {
|
|
59
|
+
return description;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node.isMissing) {
|
|
63
|
+
return {
|
|
64
|
+
code: "missing-syntax",
|
|
65
|
+
message: `Expected ${node.type.replaceAll("_", " ")}.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
code: "syntax-error",
|
|
71
|
+
message: `Invalid ${dialectName(dialect)} sed syntax.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function issuePriority(node) {
|
|
75
|
+
if (node.isError) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
if (node.isMissing) {
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function preferSpecificIssues(nodes) {
|
|
85
|
+
const issuesByRange = new Map();
|
|
86
|
+
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
const key = `${node.startIndex}:${node.endIndex}`;
|
|
89
|
+
const existing = issuesByRange.get(key);
|
|
90
|
+
if (
|
|
91
|
+
existing === undefined ||
|
|
92
|
+
issuePriority(node) > issuePriority(existing)
|
|
93
|
+
) {
|
|
94
|
+
issuesByRange.set(key, node);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return [...issuesByRange.values()].sort(
|
|
99
|
+
(left, right) =>
|
|
100
|
+
left.startIndex - right.startIndex || left.endIndex - right.endIndex,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createDiagnostics(document, dialect) {
|
|
105
|
+
const tree = syntaxTreeFor(document, dialect);
|
|
106
|
+
|
|
107
|
+
return preferSpecificIssues(collectSyntaxIssueNodes(tree.rootNode)).map(
|
|
108
|
+
(node) => {
|
|
109
|
+
const { code, message } = descriptionFor(node, dialect);
|
|
110
|
+
return {
|
|
111
|
+
severity: DiagnosticSeverity.Error,
|
|
112
|
+
range: rangeForNode(document, node),
|
|
113
|
+
message,
|
|
114
|
+
code,
|
|
115
|
+
source: diagnosticSource,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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 tree = syntaxTreeFor(document, dialect);
|
|
79
|
+
if (collectSyntaxIssueNodes(tree.rootNode).length > 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const formatted = formatScript(tree.rootNode, source, options);
|
|
84
|
+
if (formatted === source) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
range: {
|
|
91
|
+
start: document.positionAt(0),
|
|
92
|
+
end: document.positionAt(source.length),
|
|
93
|
+
},
|
|
94
|
+
newText: formatted,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
}
|
package/src/server.js
CHANGED
|
@@ -11,11 +11,10 @@ import {
|
|
|
11
11
|
TextDocuments,
|
|
12
12
|
} from "vscode-languageserver/node";
|
|
13
13
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "./analysis.js";
|
|
14
|
+
import { createDefinitionLocations } from "./definition.js";
|
|
15
|
+
import { createDiagnostics } from "./diagnostics.js";
|
|
16
|
+
import { createFormattingEdits } from "./formatting.js";
|
|
17
|
+
import { invalidateSyntaxTreeCache } from "./syntax.js";
|
|
19
18
|
|
|
20
19
|
if (process.argv.length === 2) {
|
|
21
20
|
process.argv.push("--stdio");
|
|
@@ -81,6 +80,7 @@ connection.onInitialize(({ initializationOptions }) => {
|
|
|
81
80
|
capabilities: {
|
|
82
81
|
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
83
82
|
definitionProvider: true,
|
|
83
|
+
documentFormattingProvider: true,
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
});
|
|
@@ -108,6 +108,13 @@ connection.onDefinition(({ textDocument, position }) => {
|
|
|
108
108
|
: createDefinitionLocations(document, position, activeDialect);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
connection.onDocumentFormatting(({ textDocument, options }) => {
|
|
112
|
+
const document = documents.get(textDocument.uri);
|
|
113
|
+
return document === undefined
|
|
114
|
+
? []
|
|
115
|
+
: createFormattingEdits(document, activeDialect, options);
|
|
116
|
+
});
|
|
117
|
+
|
|
111
118
|
documents.onDidChangeContent(({ document }) => {
|
|
112
119
|
publishDiagnostics(document);
|
|
113
120
|
});
|
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.isError && !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
|
-
}
|