@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 +1 -0
- package/package.json +1 -1
- package/src/definition.js +35 -0
- package/src/diagnostics.js +189 -0
- package/src/formatting.js +100 -0
- package/src/server.js +12 -36
- 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,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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
}
|