I’ve come up with a nice pattern for formatting code using an external CLI tool. Many languages already have a tool to format code, but they often assume a developer will run them from a command line.
This advice assumes that:
- The formatter is an existing CLI tool
- that takes unformatted code as
STDIN
- and outputs formatted code to
STDOUT
Writing to STDIN
First we need the process to read and write:
// This is equivalent to `/usr/local/bin/formatter --no-color -`
const process = new Process("/usr/local/bin/formatter", {
args: ["--no-color", "-"],
stdio: "pipe",
});
stdio: "pipe"
ensures that we can read to and write from the process. Here’s how we write to it:
const writeToStdin = (process, unformattedText) => {
const writer = process.stdin.getWriter();
writer.ready.then(() => {
writer.write(unformattedText);
writer.close();
});
}
Collecting the Formatted Text From STDOUT
The process will send the formatted code to STDOUT
one line at a time. We write a small, single-line function to collect every line:
// The function to collect STDOUT
const collectOutputText = (stdout, buffer) => (buffer.stdout += stdout);
// And how we connect it to our process
let buffer = { stdout: "", stderr: "" };
process.onStdout((stdout) => collectOutputText(stdout, buffer));
We capture STDOUT
inside an object instead of a string variable because JavaScript does not have “pass-by-reference”. If we tried to pass a string variable, we’d only capture the last line because the variable would start from scratch every time it was passed. However passing an object will hold onto that value.
We can do the exact same thing for STDERR
:
const collectErrorText = (stderr, buffer) => (buffer.stderr += stderr);
// Use the same `buffer` object from before
process.onStdout((stdout) => collectOutputText(stdout, buffer));
Making a Promise
We want to be able to treat this like a command line process. We will:
- Pass in the unformatted text
- Return the formatted text
- Or respond to an error
Eventually we’ll be able to use it like this:
const myFormatFunction = require("./myFormatFunction");
myFormatFunction(unformattedText)
.then((formattedText) => {
/* replace the document with formattedText */
})
.catch((error) => {
/* do something with the error */
});
This works by returning a Promise
. Here’s all the (simplified) code:
const myFormatFunction = (unformattedText) => {
const writeToStdin = // from before
const collectOutputText = // from before
const collectErrorText = // from before
return new Promise((resolve, reject) => {
try {
const process = // from before
let buffer = { stdout: "", stderr: "" };
// process.onStdout from before
// process.onStderr from before
process.onDidExit((status) => {
if (status === 0) {
resolve(buffer.stdout);
} else {
reject(buffer.stderr);
}
});
writeToStdin(process, unformattedText); // writeToStdin from before
process.start();
} catch (err) { // just in case this code has a bug
reject(err);
}
});
}
The benefit of returning a Promise
is that it works really well with TextEditor
’s onWillSave
method which is perfect for the common “format on save” functionality. The returned Promise
ensures that Nova waits at least 5 seconds while the formatter runs. Once we resolve
the Promise
, the document saves with the newly formatted text. If we reject
, we can inform the user something went wrong instead.
Bonus: Format on Save
Here’s how I use this function in context:
editor.onWillSave((editor) => {
const documentSpan = new Range(0, editor.document.length);
const unformattedText = editor.document.getTextInRange(documentSpan);
return myFormatFunction(unformattedText)
.then((formattedText) =>
editor.edit((edit) => edit.replace(documentSpan, text))
)
.catch(/* console.error? show a notification? up to you! */);
});
I’ve found this to be a really nice separation of concerns. One function handles the actual formatting of the code and knows nothing about the document or Nova APIs. A separate function deals with the document stuff like collecting the initial text and interacting with Nova.
If you want real-world example of this, you can see one in my Crystal extension here.
I hope you found this useful. Happy to answer any questions.