Skip to content

Improve the way mix is invoked in concurrent VSCode environment #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
108 changes: 79 additions & 29 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -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<TextEdit[]> {
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<string> {
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<TextEdit[]> {
return document.save().then(() => {
return format(document);
});
languages.registerDocumentFormattingEditProvider("elixir", {
provideDocumentFormattingEdits(
document: TextDocument
): Thenable<TextEdit[]> {
return format(document).then(
formatted => {
return [TextEdit.replace(fullDocumentRange(document), formatted)];
},
error => {
window.showErrorMessage(helpfulMixErrorMessage(error));
throw error;
}
);
}
});
}
}