Running ‘go fmt’ on Save

Artem Khvastunov

Even though GoLand supports running ‘go fmt’ on save, users regularly request making this option more discoverable or even enabling it by default. In this article, I’d like to describe some of the obstacles that make doing this difficult, analyze some solutions to the same problem from other IDEs, and reflect on the paths that GoLand can take to address this request. This blog post’s primary purpose is to collect as much user feedback as possible before implementing anything. If you believe that what GoLand provides now is not enough or could work better, please read the post and share your thoughts with us.

Note: For the sake of brevity, I’m assuming you know what ‘go fmt’ is. If you’re new to the language, you might want to familiarize yourself with the topic before continuing reading.

The current state

Currently, GoLand offers three ways to interact with ‘go fmt’: dedicated actions, before commit tools, and file watchers.

Under the Tools | Go Tools menu, you can find two actions related to code formatting: Go Fmt File and Go Fmt Project. Each has a shortcut assigned to it: Alt + Shift + Control + F (Alt + Shift + Cmd + F on macOS) and Alt + Shift + Control + P (Alt + Shift + Cmd + P), respectively. The sole purpose of these actions is to run ‘go fmt’, providing it with either file paths or a path to the project directory, and they only work when you invoke them manually.

There are several hooks that can be run before a commit. They can be configured via Settings (Preferences on macOS) | Version Control | Commit | Before Commit. There you can find Go fmt, which is enabled by default.

The third way is to configure a file watcher. Under Settings (Preferences), you can find Tools | File Watchers. It’s easy to add a new watcher by using the predefined ‘go fmt’ template. Whenever a Go file is changed, the command is executed on it. The key word in this case is “whenever,” as users don’t have full control over the precise moment a file gets modified. I’ll describe this problem in more detail in the next session.

Saving private files

There are many reasons why file contents can be changed, but we can roughly divide them into two groups: external (with respect to the IDE) and internal.

By an external reason, I mean a tool modifies file content on disk. It could be Git pulling updates, another editor modifying a file, and so on. A file watcher can be configured to run ‘go fmt’ on external changes, but this option is disabled by default. The main concern here is the user experience. Let’s imagine the following situation. You have no changes. Then you update the project using, say, ‘git pull’. Some files are changed on disk, so the IDE runs ‘go fmt’ on them. If they were committed with non-canonical formatting, they are updated. Thus you now have modified files even though your only intention was to update the project. While it might seem like a good idea to just go ahead and commit them, that’s probably not what you opened the IDE to do.

If you’ve never used GoLand before, you may be surprised that there is more than one internal reason to modify file content. Indeed, conventional text editors typically have a straightforward workflow. You open a file and modify it, and then you either explicitly save it or close the editor, losing your changes. IntelliJ-based IDEs, such as GoLand, work differently. They can save a file for a lot of different reasons. You can control some of these reasons, such as the option to save your file upon IDE frame deactivation, via Settings (Preferences) | Appearance & Behavior | System Settings | Autosave. Others are beyond the user’s control. Building/running, performing VCS operations, and closing a file are among such reasons. File | Local History is available to help you restore a previous file snapshot if necessary.

The conclusion I’d like to emphasize from the above description of the IDE’s behavior is that the meaning of “on-save” in GoLand is not quite the same as it is for other editors. Even if ‘go fmt’ is enabled by default in GoLand, the resulting experience will be different from what users expect. At this point, we should probably step back and think about why one might want to reformat code automatically and regularly. But that is a large topic that probably deserves a separate post. Also, I’m afraid that even if we find a satisfying answer for this question, there would still be yet another hurdle that is extremely hard to overcome – habit. Recently, more and more developers have been switching to GoLand from conventional editors. And though the IDE can’t support all of their workflows, we should still try to make them feel as comfortable as possible in the new environment.

Problems to solve

There are a few problems that appear when using an external tool to reformat code.

First of all, GoLand already has an embedded code formatter. Does it make sense to continue supporting it, or can it be replaced with ‘go fmt’ entirely? There are at least two reasons to keep it. First, the IDE sometimes needs to reformat blocks of code that don’t yet belong to project files. For instance, refactorings often create code snippets that have to be properly formatted before being inserted into a file. Using an external tool every time a case like this arises would be inefficient. Second, GoLand’s formatter has a couple of features that are missing from ‘go fmt’. For instance, the GoLand formatter works with syntactically incorrect code and can be invoked on an arbitrary block, automatically inserts semicolons, and wraps parameters and arguments. These factors suggest a significant, and we think unacceptable, downside to switching to ‘go fmt’ completely.

If we decide to keep both formatters, however, how should we make them work together? Several different workflows would need to be supported. For instance, there are existing users of JetBrains IDEs who are used to invoking the formatter manually. They are satisfied with how things are now, but they can get frustrated when automatic formatting intervenes at seemingly random moments. Then there are people who would like to have ‘go fmt’ on save but still want to benefit from GoLand’s additional formatter features. Other users might want to configure a combination of the two formatters, along with separate ways to run them. These all should probably be addressed by a potential configuration.

Finally, running ‘go fmt’ on a file requires first saving it on disk. This doesn’t seem to be a problem in terms of performance, since almost everyone uses SSD nowadays. But it doesn’t always work well in terms of UX. GoLand does its best to preserve caret position and selection after an external change, but this is not always possible. Keeping content modification on the IDE side could solve this problem, and this is how it’s done in the Prettier and Dart plugins. What I particularly like about ‘dartfmt’ is that it accepts a caret offset and selection, and returns them modified, so there’s no job left to do for the editor.

Possible action points

The first action point could be to implement the ability to run GoLand’s formatter when the IDE saves files, either automatically or when the user saves manually with Ctrl + S (Cmd + S). The main benefit is that this would allow the user to have an on-save formatter without losing the enhanced IDE formatting features. Excluding external changes makes it possible to preserve a consistent UX for the feature.

The second action point would be to add an option to enable a ‘go fmt’ pass after running GoLand’s formatter. This would allow the user to invoke both formatters using the same key combination, Ctrl + Alt + L (Cmd + Alt + L) by default. This has the advantage of offering the features of GoLand and the consistency of the language tooling. When combined with the first action point, it solves the original problem of running ‘go fmt’ on save.

The third possible action point is to migrate to ‘go fmt’, which is effectively ‘gofmt -l -w’, to ‘gofmt’ without options. This would allow the IDE to have better control over content modification. Additionally, it would be great to investigate whether it’s possible to delegate offset computation to ‘gofmt’ itself. This would make it possible to replace the IDE’s heuristics with exact numbers, as ‘gofmt’ could potentially track offsets using AST.

Conclusion

We have not yet started working on this feature, so now is the best time to provide your feedback on our thoughts and plans. What’s missing? Are there any workflows that are not supported? How could it work better? Every opinion is valuable. Please feel free to comment under this post, post your thoughts in the issue, or contact us in whatever way is the most comfortable for you. Thanks in advance, and thank you for reading.

Subscribe

Subscribe for updates