extension.js raw
1 // Moxie language support.
2 //
3 // Diagnostics come from the compiler itself: on save (and on demand) the
4 // package's directory is built with `moxie build`, and its positioned errors
5 // become problems. Completion, hover and go-to-definition are served from the
6 // same keyword/builtin sets the compiler's own token package defines plus a
7 // workspace index of declarations, so there is no second grammar to drift.
8
9 const vscode = require("vscode");
10 const cp = require("child_process");
11 const fs = require("fs");
12 const os = require("os");
13 const path = require("path");
14
15 const KEYWORDS = {
16 break: "Leave the innermost for, switch or select.",
17 case: "A switch or select arm.",
18 chan: "A channel. Channels are fixed at spawn time; there is no make().",
19 const: "A compile-time constant.",
20 continue: "Continue the innermost for loop.",
21 default: "The default arm of a switch or select.",
22 defer: "Run at function return.",
23 else: "The else branch of an if.",
24 for: "The only loop. There is no while and no do.",
25 goto: "A jump to a label.",
26 if: "A conditional. The init statement is allowed: if x, ok := v.(T); ok { … }.",
27 import: "Import a package. Blank imports are legal, and are how //:linkname is enabled.",
28 interface: "A method set. There is no empty interface and no any.",
29 map: "A hashmap type.",
30 package: "The package clause, first non-comment line of a file.",
31 range: "Ranges over a slice, map, string (bytes, not runes) or channel.",
32 return: "Return from a function. Every return value is named and return is bare.",
33 select: "Wait on several channel operations.",
34 struct: "A struct type.",
35 switch: "A switch on a tag or on true.",
36 type: "A type declaration.",
37 var: "A package- or function-scope variable.",
38 true: "The boolean constant true.",
39 false: "The boolean constant false.",
40 nil: "The zero value for a pointer, slice, map, chan or func.",
41 iota: "The constant counter inside a const block.",
42 };
43
44 const BUILTINS = {
45 push: "Append one element. Bounded: it fails loud past the end of capacity.",
46 pop: "Remove and return the last element.",
47 resize: "Change a slice's length within its capacity.",
48 len: "The length of a slice, map, string or channel.",
49 cap: "The capacity of a slice or channel.",
50 copy: "Copy between slices.",
51 delete: "Delete a map key.",
52 close: "Close a channel; every holder sees the close signal.",
53 clear: "Zero a map or slice.",
54 min: "The smaller of two ordered values.",
55 max: "The larger of two ordered values.",
56 panic: "Abort the domain with a string.",
57 recover: "Resume from a panic inside a deferred call.",
58 print: "Write to stderr without formatting.",
59 println: "Write a line to stderr.",
60 spawn: "Start a new domain. Channels are bound at spawn time; a closure cannot be spawned.",
61 };
62
63 const TYPES = {
64 bool: "Boolean.",
65 byte: "uint8, and byte is an alias for it.",
66 rune: "int32, and rune is an alias for it.",
67 string: "A mutable byte slice: string and []byte are one type.",
68 error: "The error interface: Error() string (and String() so fmt can print it).",
69 uintptr: "The pointer-sized integer for unsafe.Pointer arithmetic.",
70 comparable: "The type-set constraint for == and !=.",
71 int8: "8-bit signed integer.",
72 int16: "16-bit signed integer.",
73 int32: "32-bit signed integer; len() and indexes are this.",
74 int64: "64-bit signed integer.",
75 uint8: "8-bit unsigned integer.",
76 uint16: "16-bit unsigned integer.",
77 uint32: "32-bit unsigned integer.",
78 uint64: "64-bit unsigned integer.",
79 float32: "32-bit float.",
80 float64: "64-bit float.",
81 };
82
83 const REMOVED = {
84 int: "Not predeclared in Moxie: use an explicit width. You may declare your own int.",
85 uint: "Not predeclared in Moxie: use an explicit width.",
86 complex64: "Removed from Moxie.",
87 complex128: "Removed from Moxie.",
88 make: "Removed: presize with []T{:n:cap}.",
89 new: "Removed: use &T{} with an arena.",
90 append: "Removed: use push, with mxutil.Ensure for a spread.",
91 complex: "Removed from Moxie.",
92 real: "Removed from Moxie.",
93 imag: "Removed from Moxie.",
94 reflect: "Removed from Moxie.",
95 any: "The empty interface is not available; constrain a type parameter concretely.",
96 };
97
98 let channel;
99 let diagnostics;
100 let buildTimer;
101 let building = false;
102
103 function config() {
104 return vscode.workspace.getConfiguration("moxie");
105 }
106
107 function compilerRoot(scopeUri) {
108 const configured = (config().get("moxieRoot") || "").trim();
109 if (configured) {
110 return configured;
111 }
112 const folder = scopeUri ? vscode.workspace.getWorkspaceFolder(scopeUri) : undefined;
113 return folder ? folder.uri.fsPath : "";
114 }
115
116 // packageDir returns the directory to build for a document: its own directory
117 // when that holds .mx files, otherwise the workspace folder.
118 function packageDir(doc) {
119 const dir = path.dirname(doc.uri.fsPath);
120 try {
121 if (fs.readdirSync(dir).some((f) => f.endsWith(".mx"))) {
122 return dir;
123 }
124 } catch (_) {
125 /* fall through */
126 }
127 const folder = vscode.workspace.getWorkspaceFolder(doc.uri);
128 return folder ? folder.uri.fsPath : dir;
129 }
130
131 function runCompiler(dir, scopeUri) {
132 return new Promise((resolve) => {
133 const exe = config().get("moxie.path") || "moxie";
134 const out = path.join(os.tmpdir(), "moxie-lang-diagnostics");
135 const extra = config().get("moxie.buildFlags") || [];
136 const args = ["build"].concat(extra).concat(["-o", out, "."]);
137 const env = Object.assign({}, process.env);
138 const root = compilerRoot(scopeUri);
139 if (root) {
140 env.MOXIEROOT = root;
141 }
142 const child = cp.spawn(exe, args, { cwd: dir, env });
143 let text = "";
144 child.stdout.on("data", (d) => (text += d.toString()));
145 child.stderr.on("data", (d) => (text += d.toString()));
146 child.on("error", (err) => resolve({ text: err.message, dir }));
147 child.on("close", () => resolve({ text, dir }));
148 });
149 }
150
151 function parseErrors(text) {
152 const found = [];
153 for (const raw of text.split(/\r?\n/)) {
154 const line = raw.trim();
155 if (!line || line.startsWith(";") || line.startsWith(">>>") || line.startsWith("[mem]")) {
156 continue;
157 }
158 let m = /^(.*?):(\d+):(\d+):\s*(.*)$/.exec(line);
159 if (m) {
160 found.push({ file: m[1], line: Number(m[2]), col: Number(m[3]), msg: m[4] });
161 continue;
162 }
163 m = /^(.*?):(\d+):\s*(.*)$/.exec(line);
164 if (m) {
165 found.push({ file: m[1], line: Number(m[2]), col: 1, msg: m[3] });
166 }
167 }
168 return found;
169 }
170
171 function resolveFile(file, dir) {
172 if (path.isAbsolute(file)) {
173 return vscode.Uri.file(file);
174 }
175 const candidate = path.join(dir, file);
176 if (fs.existsSync(candidate)) {
177 return vscode.Uri.file(candidate);
178 }
179 const folder = vscode.workspace.workspaceFolders;
180 if (folder && folder.length) {
181 const other = path.join(folder[0].uri.fsPath, file);
182 if (fs.existsSync(other)) {
183 return vscode.Uri.file(other);
184 }
185 }
186 return vscode.Uri.file(candidate);
187 }
188
189 async function diagnose(doc, quiet) {
190 if (building) {
191 return;
192 }
193 building = true;
194 try {
195 const dir = packageDir(doc);
196 const res = await runCompiler(dir, doc.uri);
197 if (config().get("moxie.trace") !== "off") {
198 channel.appendLine("$ moxie build -o <tmp> . (cwd " + dir + ")");
199 channel.appendLine(res.text.trim());
200 }
201 diagnostics.clear();
202 const byFile = new Map();
203 for (const e of parseErrors(res.text)) {
204 const uri = resolveFile(e.file, res.dir);
205 const key = uri.toString();
206 if (!byFile.has(key)) {
207 byFile.set(key, { uri, items: [] });
208 }
209 const range = new vscode.Range(
210 Math.max(0, e.line - 1),
211 Math.max(0, e.col - 1),
212 Math.max(0, e.line - 1),
213 Math.max(0, e.col)
214 );
215 byFile.get(key).items.push(
216 new vscode.Diagnostic(range, e.msg, vscode.DiagnosticSeverity.Error)
217 );
218 }
219 for (const { uri, items } of byFile.values()) {
220 diagnostics.set(uri, items);
221 }
222 const total = Array.from(byFile.values()).reduce((n, f) => n + f.items.length, 0);
223 if (total === 0 && !quiet) {
224 channel.appendLine("moxie build: no errors");
225 }
226 } finally {
227 building = false;
228 }
229 }
230
231 function declarationIndex() {
232 const index = new Map();
233 return vscode.workspace.findFiles("**/*.mx", "**/{node_modules,.git,tmp}/**").then((files) => {
234 const reads = files.map((uri) =>
235 vscode.workspace.openTextDocument(uri).then((doc) => {
236 const text = doc.getText();
237 const re = /^\s*(?:func\s+(?:\([^)]*\)\s+)?|type\s+|var\s+|const\s+)([A-Za-z_][A-Za-z0-9_]*)/gm;
238 let m;
239 while ((m = re.exec(text)) !== null) {
240 const name = m[1];
241 if (!index.has(name)) {
242 index.set(name, []);
243 }
244 const pos = doc.positionAt(m.index + m[0].length - name.length);
245 index.get(name).push({ uri, pos, line: doc.lineAt(pos.line).text.trim() });
246 }
247 return null;
248 })
249 );
250 return Promise.all(reads).then(() => index);
251 });
252 }
253
254 function activate(context) {
255 channel = vscode.window.createOutputChannel("Moxie");
256 diagnostics = vscode.languages.createDiagnosticCollection("moxie");
257 context.subscriptions.push(channel, diagnostics);
258
259 const selector = { language: "moxie", scheme: "file" };
260
261 context.subscriptions.push(
262 vscode.workspace.onDidSaveTextDocument((doc) => {
263 if (doc.languageId !== "moxie" || !config().get("moxie.buildOnSave")) {
264 return;
265 }
266 if (buildTimer) {
267 clearTimeout(buildTimer);
268 }
269 buildTimer = setTimeout(() => diagnose(doc, true), 250);
270 })
271 );
272 context.subscriptions.push(vscode.workspace.onDidOpenTextDocument((doc) => {
273 if (doc.languageId === "moxie") {
274 diagnose(doc, true);
275 }
276 }));
277
278 context.subscriptions.push(
279 vscode.commands.registerCommand("moxie.build", () => {
280 const doc = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document;
281 if (!doc || doc.languageId !== "moxie") {
282 vscode.window.showInformationMessage("Open a .mx file to build its package.");
283 return;
284 }
285 channel.show(true);
286 diagnose(doc, false);
287 })
288 );
289
290 context.subscriptions.push(
291 vscode.languages.registerCompletionItemProvider(selector, {
292 provideCompletionItems() {
293 const items = [];
294 const add = (label, kind, doc) => {
295 const item = new vscode.CompletionItem(label, kind);
296 item.documentation = new vscode.MarkdownString(doc);
297 items.push(item);
298 };
299 for (const k of Object.keys(KEYWORDS)) {
300 add(k, vscode.CompletionItemKind.Keyword, KEYWORDS[k]);
301 }
302 for (const b of Object.keys(BUILTINS)) {
303 add(b, vscode.CompletionItemKind.Function, BUILTINS[b]);
304 }
305 for (const t of Object.keys(TYPES)) {
306 add(t, vscode.CompletionItemKind.TypeParameter, TYPES[t]);
307 }
308 return items;
309 },
310 })
311 );
312
313 context.subscriptions.push(
314 vscode.languages.registerHoverProvider(selector, {
315 provideHover(doc, position) {
316 const range = doc.getWordRangeAtPosition(position, /[A-Za-z_][A-Za-z0-9_]*/);
317 if (!range) {
318 return null;
319 }
320 const word = doc.getText(range);
321 const help = KEYWORDS[word] || BUILTINS[word] || TYPES[word] || REMOVED[word];
322 if (help) {
323 return new vscode.Hover(new vscode.MarkdownString("**" + word + "** — " + help));
324 }
325 return null;
326 },
327 })
328 );
329
330 const indexPromise = declarationIndex();
331 context.subscriptions.push(
332 vscode.languages.registerDefinitionProvider(selector, {
333 async provideDefinition(doc, position) {
334 const range = doc.getWordRangeAtPosition(position, /[A-Za-z_][A-Za-z0-9_]*/);
335 if (!range) {
336 return null;
337 }
338 const word = doc.getText(range);
339 const index = await indexPromise;
340 const hits = index.get(word) || [];
341 return hits.map((h) => new vscode.Location(h.uri, h.pos));
342 },
343 })
344 );
345 }
346
347 function deactivate() {
348 if (diagnostics) {
349 diagnostics.dispose();
350 }
351 }
352
353 module.exports = { activate, deactivate };
354