diff --git a/README.md b/README.md index 9b51cf6..396107b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ options are available for the user to configure, in `settings.json`: { elixir.formatter: { mixFormatArgs: "--dry-run", + mixCommandPath: "/etc/custom/elixir/path/bin/mix", formatterCwd: "../some/dir/to/run/mix/format/from" } } diff --git a/src/extension.ts b/src/extension.ts index bd04cd9..468bdeb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,49 +1,99 @@ -import { languages, ExtensionContext } from "vscode"; -import { workspace, Range, TextDocument, TextEdit, window } from "vscode"; -import cp = require("child_process"); -var path = require('path') +import * as path from "path"; + +import { ExtensionContext, languages, window } from "vscode"; +import { Range, TextDocument, TextEdit, workspace } from "vscode"; + +import { spawn } from "child_process"; function fullDocumentRange(document: TextDocument): Range { const lastLineId = document.lineCount - 1; return new Range(0, 0, lastLineId, document.lineAt(lastLineId).text.length); } -function format(document: TextDocument): Promise { +function helpfulMixErrorMessage(error: any): string { + if (error.code === "ENOENT") { + return ( + (error.path || "mix") + + " command not found. It was expected to be in $PATH: " + + process.env.PATH + + ". Note that VSCode is running in an environment different from your terminal, " + + "and it doesn't read your shell rc files. You can set elixir.formatter.mixCommandPath " + + "to a custom location if it's not in your $PATH." + ); + } + // Get rid of standard header to leave space in the error popup + // for the actual line number + return error.message.replace("mix format failed for stdin\n", ""); +} + +function format(document: TextDocument): Promise { return new Promise((resolve, reject) => { // Create mix command - const mixFormatArgs: string = workspace.getConfiguration("elixir.formatter").get("mixFormatArgs") || ""; - const cmd = `mix format ${mixFormatArgs} ${document.fileName}`; + const mixCommandPath: string = + workspace.getConfiguration("elixir.formatter").get("mixCommandPath") || + "/bin/mix"; + const mixFormatArgsSetting: string = workspace + .getConfiguration("elixir.formatter") + .get("mixFormatArgs"); + const mixFormatArgs = + typeof mixFormatArgsSetting === "string" && mixFormatArgsSetting !== "" + ? mixFormatArgsSetting.split(" ") + : []; // Figure out the working directory to run mix format in const workspaceRootPath = workspace.rootPath ? workspace.rootPath : ""; - const relativePath: string = workspace.getConfiguration("elixir.formatter").get("formatterCwd") || ""; + const relativePath: string = + workspace.getConfiguration("elixir.formatter").get("formatterCwd") || ""; const cwd = path.resolve(workspaceRootPath, relativePath); - // Run the command - cp.exec( - cmd, - { - cwd - }, - function(error, stdout, stderr) { - if (error !== null) { - const message = `Cannot format due to syntax errors.: ${stderr}`; - window.showErrorMessage(message); - return reject(message); - } else { - return [TextEdit.replace(fullDocumentRange(document), stdout)]; - } + const proc = spawn(mixCommandPath, ["format", ...mixFormatArgs, "-"], { + cwd + }); + proc.on("error", reject); + + // If process fails to start, write syscall will fail synchronously and + // will mask the original error message. Let's postpone writing until + // all event handlers are setup and NodeJS had a chance to call the + // on("error") callback. + process.nextTick(() => { + proc.stdin.write(document.getText(), "utf8", error => + error ? "reject(error)" : proc.stdin.end() + ); + }); + + const stdout = []; + const stderr = []; + proc.stdout.setEncoding("utf8"); + proc.stderr.setEncoding("utf8"); + proc.stdout.on("data", data => stdout.push(data)); + proc.stderr.on("data", data => stderr.push(data)); + + proc.on("exit", code => { + if (code === 0) { + resolve(stdout.join("")); + } else { + const error: any = new Error(stderr.join("")); + error.code = code; + reject(error); } - ); + }); }); } export function activate(context: ExtensionContext) { - languages.registerDocumentFormattingEditProvider('elixir', { - provideDocumentFormattingEdits(document: TextDocument): Thenable { - return document.save().then(() => { - return format(document); - }); + languages.registerDocumentFormattingEditProvider("elixir", { + provideDocumentFormattingEdits( + document: TextDocument + ): Thenable { + return format(document).then( + formatted => { + return [TextEdit.replace(fullDocumentRange(document), formatted)]; + }, + error => { + window.showErrorMessage(helpfulMixErrorMessage(error)); + throw error; + } + ); } }); -} \ No newline at end of file +}