Keep cursor position after altering a file

This is similar to this thread but I believe different enough to warrant its own.

The Crystal language has a formatter that replaces files in-place. You call it like crystal tool format path/to/file.cr. Here’s the extension command I’ve created:

nova.commands.register("crystal.format", (editor) => {
  try {
    const crystalPath = nova.path.expanduser("~/path/to/crystal");
    const filePath = editor.document.path;
    const previousSelectedRanges = editor.selectedRanges;
    const process = new Process(
      crystalPath,
      { args: ["tool", "format", filePath] }
    );
    process.start();
    editor.selectedRanges = previousSelectedRanges;
  } catch (err) {
    console.error("Could not call crystal tool format: " + err);
  }
});

When I run this command, it does successfully format the code, but the cursor is placed all the way at the end of the document. You can see I’m trying to grab the selected ranges and restore them after the process runs. This doesn’t work. I’ve also tried Process.onDidExit and wrapping the whole thing in a TextEditor.edit function (both inside the edit callback, and inside the then function as edit returns a promise).

The only thing that kind of worked was trying to change the selection in TextEditor.onDidChangeSelection. While it does work, I’m changing the selection in the callback which recursively triggers the callback. Thankfully, Nova kills this recursion eventually.

The core problem seems to be that Nova sets the selection/cursor some time after a file is changed externally. That delay is far past any of the callbacks mentioned above. So I can set the selection all I want in those callbacks, but then ~500ms later Nova moves the cursor to the end of the document regardless.

Am I doing something wrong here? Is there anything else I can try? Is this a known limitation? A bug?

In your code, it appears that the subprocess itself is the one altering the document (and not, for example, returning an updated text content for the extension to do). When you invoke .start() on the process, Nova runs it asynchronously in the background, so your attempt to restore the ranges is likely occurring before the subprocess is even fully started by the system.

It’d probably be better, as you said, to try and restore your ranges in the onDidExit() callback of the process, as that is guaranteed to be invoked after the process has fully quit.

However, since this is a subprocess that is altering the file and not Nova itself (or your extension), there will be a delay as macOS does not inform us the file has changed for up to a half second after this occurs. So, Nova can’t really know that anything has changed until after the filesystem notification has come in from the system.

If you want to guarantee that the file has fully been edited, you’ll need to either have your extension JavaScript code be what changes the file contents (by capturing the output of the subprocess) or waiting for a certain amount of time after the subprocess exits.

Thanks for the deep dive @logan! :smiley: That all makes sense. I’ll see what I can come up with.