Skip to content

Commit

Permalink
majjit-lsp
Browse files Browse the repository at this point in the history
  • Loading branch information
matklad committed Dec 13, 2024
1 parent 9fbe13d commit 449c4cc
Showing 1 changed file with 128 additions and 0 deletions.
128 changes: 128 additions & 0 deletions content/posts/2024-12-13-majjit-lsp.dj
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!

0 comments on commit 449c4cc

Please sign in to comment.