@tsuzuri-lab/sed-language-server 0.1.2 → 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.
@@ -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,17 +11,10 @@ import {
11
11
  TextDocuments,
12
12
  } from "vscode-languageserver/node";
13
13
  import { TextDocument } from "vscode-languageserver-textdocument";
14
- import {
15
- completionProviderOptions,
16
- createCompletionHandler,
17
- } from "./completion.js";
18
- import { createDefinitionHandler, definitionProvider } from "./definition.js";
14
+ import { createDefinitionLocations } from "./definition.js";
19
15
  import { createDiagnostics } from "./diagnostics.js";
20
- import { invalidateDocumentStructureCache } from "./document-structure.js";
21
- import {
22
- defaultSyntaxProfile,
23
- resolveSyntaxProfile,
24
- } from "./syntax-profile.js";
16
+ import { createFormattingEdits } from "./formatting.js";
17
+ import { invalidateSyntaxTreeCache } from "./syntax.js";
25
18
 
26
19
  if (process.argv.length === 2) {
27
20
  process.argv.push("--stdio");
@@ -29,19 +22,31 @@ if (process.argv.length === 2) {
29
22
 
30
23
  const connection = createConnection(ProposedFeatures.all);
31
24
  const documents = new TextDocuments(TextDocument);
32
- let activeSyntaxProfile = defaultSyntaxProfile;
25
+ const defaultDialect = "posix";
26
+ let activeDialect = defaultDialect;
27
+
28
+ function resolveDialect(options) {
29
+ if (options === undefined || options === null) {
30
+ return { dialect: defaultDialect };
31
+ }
32
+ if (typeof options !== "object" || Array.isArray(options)) {
33
+ return {
34
+ error: "Syntax options must be provided as an object or null.",
35
+ };
36
+ }
33
37
 
34
- const serverCapabilities = {
35
- textDocumentSync: TextDocumentSyncKind.Incremental,
36
- completionProvider: completionProviderOptions,
37
- definitionProvider,
38
- };
38
+ const dialect =
39
+ options.dialect === undefined ? defaultDialect : options.dialect;
40
+ if (dialect !== "posix" && dialect !== "gnu") {
41
+ return {
42
+ error: 'The syntax dialect must be either "posix" or "gnu".',
43
+ };
44
+ }
39
45
 
40
- function syntaxProfileErrorMessage(errors) {
41
- return errors.map(({ message }) => message).join(" ");
46
+ return { dialect };
42
47
  }
43
48
 
44
- function configurationProfileOptions(settings) {
49
+ function configurationOptions(settings) {
45
50
  if (
46
51
  settings !== null &&
47
52
  typeof settings === "object" &&
@@ -54,63 +59,68 @@ function configurationProfileOptions(settings) {
54
59
  return settings;
55
60
  }
56
61
 
57
- function publishDiagnostics(document, syntaxProfile = activeSyntaxProfile) {
62
+ function publishDiagnostics(document) {
58
63
  return connection.sendDiagnostics({
59
64
  uri: document.uri,
60
65
  version: document.version,
61
- diagnostics: createDiagnostics(document, syntaxProfile),
66
+ diagnostics: createDiagnostics(document, activeDialect),
62
67
  });
63
68
  }
64
69
 
65
70
  connection.onInitialize(({ initializationOptions }) => {
66
- const result = resolveSyntaxProfile(initializationOptions);
67
- if (!result.ok) {
68
- return new ResponseError(
69
- ErrorCodes.InvalidParams,
70
- syntaxProfileErrorMessage(result.errors),
71
- { retry: false },
72
- );
71
+ const result = resolveDialect(initializationOptions);
72
+ if (result.error !== undefined) {
73
+ return new ResponseError(ErrorCodes.InvalidParams, result.error, {
74
+ retry: false,
75
+ });
73
76
  }
74
77
 
75
- activeSyntaxProfile = result.profile;
78
+ activeDialect = result.dialect;
76
79
  return {
77
- capabilities: serverCapabilities,
80
+ capabilities: {
81
+ textDocumentSync: TextDocumentSyncKind.Incremental,
82
+ definitionProvider: true,
83
+ documentFormattingProvider: true,
84
+ },
78
85
  };
79
86
  });
80
87
 
81
88
  connection.onDidChangeConfiguration(async ({ settings }) => {
82
- const result = resolveSyntaxProfile(configurationProfileOptions(settings));
83
- if (!result.ok) {
89
+ const result = resolveDialect(configurationOptions(settings));
90
+ if (result.error !== undefined) {
84
91
  connection.sendNotification(ShowMessageNotification.type, {
85
92
  type: MessageType.Error,
86
- message: syntaxProfileErrorMessage(result.errors),
93
+ message: result.error,
87
94
  });
88
95
  return;
89
96
  }
90
97
 
91
- const nextSyntaxProfile = result.profile;
92
- activeSyntaxProfile = nextSyntaxProfile;
93
- invalidateDocumentStructureCache();
98
+ activeDialect = result.dialect;
94
99
  await Promise.all(
95
- documents
96
- .all()
97
- .map((document) => publishDiagnostics(document, nextSyntaxProfile)),
100
+ documents.all().map((document) => publishDiagnostics(document)),
98
101
  );
99
102
  });
100
103
 
101
- connection.onCompletion(
102
- createCompletionHandler(documents, () => activeSyntaxProfile),
103
- );
104
- connection.onDefinition(
105
- createDefinitionHandler(documents, () => activeSyntaxProfile),
106
- );
104
+ connection.onDefinition(({ textDocument, position }) => {
105
+ const document = documents.get(textDocument.uri);
106
+ return document === undefined
107
+ ? null
108
+ : createDefinitionLocations(document, position, activeDialect);
109
+ });
110
+
111
+ connection.onDocumentFormatting(({ textDocument, options }) => {
112
+ const document = documents.get(textDocument.uri);
113
+ return document === undefined
114
+ ? []
115
+ : createFormattingEdits(document, activeDialect, options);
116
+ });
107
117
 
108
118
  documents.onDidChangeContent(({ document }) => {
109
119
  publishDiagnostics(document);
110
120
  });
111
121
 
112
122
  documents.onDidClose(({ document }) => {
113
- invalidateDocumentStructureCache(document);
123
+ invalidateSyntaxTreeCache(document);
114
124
  connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
115
125
  });
116
126
 
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
+ }
Binary file
package/src/completion.js DELETED
@@ -1,148 +0,0 @@
1
- import {
2
- CompletionItemKind,
3
- InsertTextFormat,
4
- } from "vscode-languageserver/node";
5
- import { getDocumentStructure } from "./document-structure.js";
6
- import {
7
- commandSpecificationsFor,
8
- substituteFlagSpecificationsFor,
9
- } from "./sed-syntax.js";
10
- import {
11
- defaultSyntaxProfile,
12
- requireSyntaxProfile,
13
- } from "./syntax-profile.js";
14
-
15
- function plainCompletion(label, kind, documentation) {
16
- return {
17
- label,
18
- kind,
19
- insertText: label,
20
- insertTextFormat: InsertTextFormat.PlainText,
21
- documentation,
22
- };
23
- }
24
-
25
- const commandCompletionsByProfile = new Map();
26
- const substituteFlagCompletionsByProfile = new Map();
27
-
28
- function commandCompletionsFor(syntaxProfile) {
29
- let completions = commandCompletionsByProfile.get(syntaxProfile);
30
- if (completions !== undefined) {
31
- return completions;
32
- }
33
-
34
- completions = [
35
- plainCompletion(
36
- ";",
37
- CompletionItemKind.Keyword,
38
- "Insert an empty command and begin the next command.",
39
- ),
40
- ...commandSpecificationsFor(syntaxProfile).map(
41
- ({ command, documentation }) =>
42
- plainCompletion(command, CompletionItemKind.Keyword, documentation),
43
- ),
44
- ];
45
- commandCompletionsByProfile.set(syntaxProfile, completions);
46
- return completions;
47
- }
48
-
49
- function substituteFlagCompletionsFor(syntaxProfile) {
50
- let completions = substituteFlagCompletionsByProfile.get(syntaxProfile);
51
- if (completions !== undefined) {
52
- return completions;
53
- }
54
-
55
- completions = [
56
- ...Array.from({ length: 9 }, (_, index) => {
57
- const occurrence = String(index + 1);
58
- return plainCompletion(
59
- occurrence,
60
- CompletionItemKind.Value,
61
- `Replace only occurrence number ${occurrence}.`,
62
- );
63
- }),
64
- ...substituteFlagSpecificationsFor(syntaxProfile).map(
65
- ({ flag, documentation }) =>
66
- plainCompletion(flag, CompletionItemKind.Keyword, documentation),
67
- ),
68
- ];
69
- substituteFlagCompletionsByProfile.set(syntaxProfile, completions);
70
- return completions;
71
- }
72
-
73
- export const completionProviderOptions = Object.freeze({});
74
-
75
- function branchLabelEditRange(document, context, offset) {
76
- const replacementRange = context.replacementRange ?? {
77
- startOffset: offset,
78
- endOffset: offset,
79
- };
80
- const startOffset = Math.min(replacementRange.startOffset, offset);
81
- const endOffset = Math.max(replacementRange.endOffset, offset);
82
-
83
- return {
84
- start: document.positionAt(startOffset),
85
- end: document.positionAt(endOffset),
86
- };
87
- }
88
-
89
- export function createCompletions(
90
- document,
91
- position,
92
- options = defaultSyntaxProfile,
93
- ) {
94
- const syntaxProfile = requireSyntaxProfile(options);
95
- const structure = getDocumentStructure(document, syntaxProfile);
96
- const offset = document.offsetAt(position);
97
- const context = structure.contextDetailsAt(offset);
98
-
99
- if (context?.kind === "command") {
100
- return commandCompletionsFor(syntaxProfile).map((completion) => ({
101
- ...completion,
102
- }));
103
- }
104
-
105
- if (context?.kind === "substitute-flag") {
106
- return substituteFlagCompletionsFor(syntaxProfile).map((completion) => ({
107
- ...completion,
108
- }));
109
- }
110
-
111
- if (context?.kind !== "branch-label") {
112
- return [];
113
- }
114
-
115
- const names = new Set();
116
- const completions = [];
117
- const editRange = branchLabelEditRange(document, context, offset);
118
- for (const definition of structure.labelDefinitions) {
119
- if (names.has(definition.name)) {
120
- continue;
121
- }
122
-
123
- names.add(definition.name);
124
- completions.push({
125
- label: definition.name,
126
- kind: CompletionItemKind.Reference,
127
- insertTextFormat: InsertTextFormat.PlainText,
128
- documentation: "Branch label defined in this document.",
129
- textEdit: {
130
- range: editRange,
131
- newText: definition.name,
132
- },
133
- });
134
- }
135
- return completions;
136
- }
137
-
138
- export function createCompletionHandler(
139
- documents,
140
- getSyntaxProfile = () => defaultSyntaxProfile,
141
- ) {
142
- return ({ textDocument, position }) => {
143
- const document = documents.get(textDocument.uri);
144
- return document === undefined
145
- ? null
146
- : createCompletions(document, position, getSyntaxProfile());
147
- };
148
- }