-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
128 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# Majjit LSP | ||
|
||
An out-there suggestion for the nascent [jj](https://github.com/martinvonz/jj) ecosystem! | ||
|
||
I was skimming [Martin's post](https://neugierig.org/software/blog/2024/12/jujutsu.html), and the VS | ||
Code section of it kicked off this particular train of thought. Summarizing, VSCode has some | ||
built-in integrations with git, and they don't necessary directly applicable to jj. Direct solution | ||
is, of course, to add first class support of jj to VSCode. | ||
|
||
But I want better. Specifically, my problem is that I don't actually like VS Code VCS UI. I use | ||
[@kahole](https://github.com/kahole)'s awesome implementation of [Magit](https://magit.vc) for | ||
VSCode, [edamagit](https://github.com/kahole/edamagit). Magit is objectively correct way to | ||
implement a VCS user interface (more on this in just a moment). | ||
|
||
So I need to wait for someone for coding Magit/jj/VSCode triple. What's worse, I actually want to | ||
switch to Zed at some point, and absence of Magit is one of the bigger things that prevent this. But | ||
Zed doesn't at the moment have a rich-enough plugin API to express that. | ||
|
||
At the same time, although two separate code-bases, Magit in VSCode and Magit in Emacs are basically | ||
the same thing from the perspective of the user. We are _clearly_ missing a [narrow | ||
waist](https://www.oilshell.org/blog/2022/02/diagrams.html) here which would allow us to implement | ||
Magit only once. | ||
|
||
Or rather, there's a "barely adequate enough" narrow waist --- shell/terminal combo. You _could_ | ||
implement magit-tui which would be re-usable between, say, Vim and | ||
[Helix](https://helix-editor.com). But this particular thin waist sucks. It suffers from Byzantine | ||
complexity (why the hell the kernel needs to be directly involved), unfixable design bugs (terminfo | ||
database, necessity to set both your terminal emulator's and your shell color themes separately), | ||
and missing features (window management, for example). I _think_ an absolutely massive improvement | ||
is possible here. I have [a separate set of notes](https://github.com/matklad/abont) about that, but | ||
that's a huge reinvent the wheel by starting from atoms kind of thing. | ||
|
||
Luckily, for the present problem of getting first-class UI for jj into any editor, I think a | ||
shortcut exists! We can implement "magit for jj" once, and have it working for almost free in any | ||
editor, GUI or terminal. To not spoil the answer right away, let me describe, first, | ||
|
||
## What is Magit | ||
|
||
The core idea is **text file is the user interface**. That' it! This the paradigm of Emacs (and | ||
Acme), with Magit being the most successful application. Text being the interface means two things: | ||
|
||
- When we need to present information to the user, we present it in textual format. | ||
- When we need _input_ from the user, it is also text driven. In the simplest form, the user can | ||
"click" on a particular word in the text. | ||
|
||
Let's apply this to various UI tasks of a VCS. The basic one is of course informing the user about | ||
changes. Here, we can generate a textual diff --- a file witch each line marked at `+` or `-`. On | ||
top of this, there's a number of progressive enhancements: | ||
|
||
- Of course `+` lines are colored green and `-` red. | ||
- Consecutive changes are grouped into hunks, and each hung is individually foldable. | ||
- A diff can span multiple files, and files form another level in the folding hierarchy. By folding | ||
the entire diff recursively, you get a list of changed files, from which you can drill down an | ||
individual file. | ||
|
||
This is the _presentation_ side of the interface. And now, interaction: | ||
|
||
Clicking on any line in the diff opens the corresponding file in the working copy. This composes | ||
well with split view (a feature missing from the terminal's narrow waist): on the right you can have | ||
your hierarchical diff, on the left, your code. Moving in the diff file automatically moves the | ||
working copy view. | ||
|
||
This is a very productive interface to make sense of larger changes. You open the change as this | ||
hierarchical diff. First, everything is folded, so you only see the list of changed files, which | ||
gives you the context about a change. You then unfold the most interesting changed file and start | ||
perusing the diff. If, at some point, you realize that you need more context to understand a | ||
particular hunk, you click on it, and view, at the other half of the screen, the entire function | ||
surrounding the changed line. | ||
|
||
The rest of VCS builds naturally on top. Above hunks and files you add commits as another level of | ||
hierarchy. So, "everything folded" is now a list of commits. The list of commits view suggest a | ||
natural way to model history manipulation. If you want to reorder two commits, you can reorder two | ||
lines in the "list of commits" text document. | ||
|
||
If you want to split an individual commit, you can open a text document which contains _two_ diffs. | ||
One is the commit, the other is initially empty. Clicking on a hunk moves is from one diff to | ||
another. | ||
|
||
And of course, you can have a "dashboard" text document, which shows the diff for the latest commit | ||
(jj) or staging area (git), a list of recent commits, a list of "branches", and has "hyper-links" | ||
for all VCS operations. | ||
|
||
## Magit For All Editors | ||
|
||
Finally, the idea I wrote this article for: | ||
|
||
**You can materialize all the above files on disk** | ||
|
||
That is, for the dashboard, we don't write a custom editor extension that materializes a virtual | ||
text buffer, but rather directly create an `.jj/status.jj` file on disk and open it in the editor. | ||
The editor watches the file for changes, so we can update the on-disk buffer to update the user. | ||
|
||
And I think _most_ of the interactions you need to implement Magit-like experience are expressible | ||
in LSP: | ||
|
||
- [Semantic Tokens](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens) | ||
give syntax highlighting for diffs&dashboard. | ||
- [Folding Ranges](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_foldingRange) | ||
give folding for diffs. | ||
- [Goto Definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) | ||
give "click hunk to jump to working copy" behavior for diffs. It can also be used by dashboard for | ||
materializing additional files. The dashboard probably _shouldn't_ show diffs for any commits but | ||
last. Rather, "goto to definition" on a commit `zxf` should materialize `.jj/zxf.diff` file and | ||
than jump there. | ||
- And of course, [Code | ||
Actions](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction) | ||
can provide interface for arbitrary commands. For example, if you want to reword a commit, you can | ||
place your cursor on it in the dashboard, and pick "reword" from code-actions context menu. | ||
|
||
|
||
What you wouldn't get out of LSP is Magit's sleek one-letter mnemonics for action. In Magit, the | ||
status buffer is read-only, so rewording a commit is a simple as placing a cursor on it and typing | ||
`r`. _This_ level of integration I think will require some tighter interaction with the editor, but | ||
this should be a relatively thin layer. | ||
|
||
## Bigger Picture | ||
|
||
It's useful to take a bird's eye view here. You can think of a VCS as a system for storing your | ||
source code files. But that is the illusion, the reality is the opposite. What VCS actually does is | ||
it stores is a complex graph of objects linked through content hashes. You use VCS to mutate this | ||
graph. Now, mutating the graph manually is a pain, so VCS resorts to using your working copy as a | ||
convenient way to author graph changes! To view a node in the graph, it is "checked out" into the | ||
working copy. To insert a new node, you save working copy as this node. See how bidirectional | ||
human-computer interaction is mediated through mutable file system! | ||
|
||
In other words, Git, and jj, already use "file system as the user interface" paradigm at the macro | ||
level (authoring new commits) and at the micro level (authoring commit messages). The middle-end is | ||
missing though! `git status` is a CLI utility, but it could be just a file on disk! |