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