Skip to content

Commit

Permalink
,
Browse files Browse the repository at this point in the history
  • Loading branch information
matklad committed Oct 14, 2024
1 parent fc6b4e7 commit a51b369
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 5 deletions.
7 changes: 5 additions & 2 deletions content/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,13 @@ a { text-decoration-color: #2156a5; color: black; }
a:hover, a:focus { color: #2156a5; fill: #2156a5; }
a.url { word-break: break-all; }

kbd {
font-family: "JetBrains Mono", monospace; font-variant-ligatures: none; font-size: .65rem;
line-height: 1.45;
}

kbd > kbd {
display: inline-block;
font-family: "JetBrains Mono", monospace; font-variant-ligatures: none; font-size: .65em;
line-height: 1.45;
color: rgba(0, 0, 0, .8); background: #f7f7f7; border: 1px solid #ccc; border-radius: 3px; box-shadow: 0 1px 0 rgb(0 0 0 / 20%), 0 0 0 0.1em #fff inset;
margin: 0 0.15em; padding: 0.2em 0.5em; top: -0.1em;
vertical-align: middle; position: relative; white-space: nowrap;
Expand Down
108 changes: 108 additions & 0 deletions content/posts/2024-10-08-two-tips.dj
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Two Workflow Tips

An article about a couple of relatively recent additions to my workflow which I wish I knew about
years ago.

## Split And Go To Definition

Go to definition is super useful (in general, navigation is much more important than code
completion). But often, when I use "goto def" I don't actually mean to permanently _go_ there.
Rather, I want to stay where I am, but I need a bit more context about a particular thing at point.

What I've found works really great in this context is to _split_ the screen in two, and issue "go to
def" in the split. So that you see both the original context, and the definition at the same time,
and can choose just how far you would like to go. Here's an example, where I want to understand how
`apply` function works, and, to understand _that_, I want to quickly look up the definition of
`FuzzOp`:

![](https://github.com/user-attachments/assets/0adc0f3c-ec7e-49a6-9e7e-f93f30704fdd){.video}

VS Code actually has a first-class UI for something like this, called "Peek Definition" but it is
just not good --- it opens some kind of separate pop-up, with a completely custom UX. It's much more
fruitful to compose two existing basic features --- splitting the screen and going to definition.

Note that in the example above I do move focus to the split. I also tried a version that keeps focus
in the original split, but focusing new one turned out to be much better. You actually don't always
know up front which split would become the "main" one, and moving the focus gives you flexibility of
moving around, closing the split, or closing the other split.

I highly recommend adding a shortcut for this action. It's a good idea to make it a "complementary"
shortcut for the usual goto definition. I use [, .]{.kbd} for goto definition, and hence [, >]{.kbd}
is the splitting version:

```json
{ "key": ", .", "command": "editor.action.revealDefinition" },
{ "key": ", shift+.", "command": "editor.action.revealDefinitionAside" },
```

## , .

Yes, you are reading this right. [, .]{.kbd}, that is a comma followed by a full stop, is my goto
definition shortcut. This is not some kind of evil vim mode. I use pedestrian non-modal editing,
where I copy with [ctrl + c]{.kbd}, move to the beginning of line with [Home]{.kbd} and kill a word
with [ctrl + Backspace]{.kbd} (though keys like [Home]{.kbd}, [Backspace]{.kbd}, or arrows are on my
home row thanks to [`kanata`](https://github.com/jtroo/kanata)).

And yet, I use `,` as a first keypress in a sequence for multiple shortcuts. That is, [, .]{.kbd} is
not [, + .]{.kbd} pressed together, but rather a [,]{.kbd} followed by a separate [.]{.kbd}. So,
when I press [,]{.kbd} my editor doesn't actually type a comma, but rather waits for me to complete
the shortcut. I have many of them, with just a few being:

* [, .]{.kbd} goes to definition,
* [, >]{.kbd} goes to definition in a split,
* [, r]{.kbd} runs a task,
* [, e s]{.kbd} *e*dits selection by sorting it, [, e C]{.kbd} converts to camel*C*ase,
* [, o g]{.kbd} *o*pens [magit for VS Code](https://github.com/kahole/edamagit), [, o k]{.kbd} opens
keybindings.
* [, w]{.kbd} re-*w*raps selection at 80 (something I just did to format the previous bullet point),
[, p]{.kbd} *p*retty-prints the whole file.

I've used many different shortcut schemes, but this is by far the most convenient one for me. How do
I type an comma? I bind [, Space]{.kbd} and [, Enter]{.kbd} to insert comma and a space/newline
respectively, which handles most of the cases. And there's [, ,]{.kbd} which types just a lone comma.

To remember longer sequences, I pair the comma with
[whichkey](https://github.com/VSpaceCode/vscode-which-key), such that, when I type [, e]{.kbd}, what
I see is actually a menu of editing operations:

![](https://github.com/user-attachments/assets/57f50d23-8c4d-4ec3-a1f0-51fce46bf4d0)

This horrible idea was born in the mind of Susam Pal, and is officially (and aptly I should say)
named [[Devil Mode](https://susam.github.io/devil/).]{.display}

I highly recommend trying it out! It is the perfect interface for actions that you do once in a
while. Where it doesn't work is for actions you want to repeat. For example, if you want to cycle
through compilation errors, binding [, e]{.kbd} to the "next error" would probably be a bad
idea, as typing `, e , e , e` to cycle three times is quite tiring.

This is actually a common theme, there are _many_ things you might to cycle back and forward
through:

* completion suggestions
* compiler errors
* textual search results
* reference search results
* merge conflicts
* working tree changes

It is mighty annoying to have to remember different shortcuts for all of them, isn't it? If only
there was some way to have a universal pair of shortcuts for the next/prev generalized motion...

The insight here is that you'd rarely need to cycle through several different categories of things
at the same time. So I bind the venerable [ctrl+n]{.kbd} and [ctrl+p]{.kbd} to _repeating_ the last
next/prev motion. So, if the last next thing was a worktree change, then [ctrl+n]{.kbd} moves me to
the next worktree change. But if I then query the next compilation error, the subsequent
[ctrl+n]{.kbd} would continue cycling through compilation errors. To kick-start the cycle, I have a
[, n]{.kbd} hydra:

* [, n e]{.kbd} next error
* [, n c]{.kbd} next change
* [, n C]{.kbd} next merge Conflict
* [, n r]{.kbd} next reference
* [, n f]{.kbd} next find
* [, n .]{.kbd} previous edit

I don't know if there's some existing VS Code extension to do this, I implement this in
[my personal extension](https://github.com/matklad/config/blob/0f690f89c80b0e246909b54a0e97c67d5ce6ab0c/my-code/src/main.ts#L63-L96).

Hope this is useful! Now go and make a deal with the devil yourself!
130 changes: 130 additions & 0 deletions content/posts/2024-10-14-missing-ide-feature.dj
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# A Missing IDE Feature

Slightly unusual genre --- with this article, I want to try to enact a change in the world. I
believe that there is a "missing" IDE feature which is:

* very easy to implement (these days),
* is a large force multiplier for experienced users,
* is conspicuously missing from almost every editor.

The target audience here is anyone who can land a PR in Zed, VS Code, Helix, Neovim, Emacs, Kakoune,
or any other editor or any language server. The blog post would be a success if one of you feels
sufficiently inspired to do the thing!

## The Feature

Suppose you are casually reading the source code of rust-analyzer, and are curious about handling of
method bodies. There's a `Body` struct in the code base, and you want to understand how it is used.

Would you rather look at this?

![](https://github.com/user-attachments/assets/4c794726-1b56-4c32-b51e-e0b671b73bab)

Or this?

![](https://github.com/user-attachments/assets/e0d9adf5-15a1-48e1-a1b8-3762104a031e)

(The screenshots are from IntelliJ/RustRover, because of course it gets this right)

The second option is clearly superior --- it conveys significantly more useful information in the
same amount of pixels. Function names, argument lists and return types are so much more valuable
than a body of any particular function. Especially if the function is a page-full of boilerplate
code!

And this is the feature I am asking for --- make the code look like the second image. Or,
specifically, [*Fold Method Bodies by Default*.]{.display}

There are two components here. _First_, only method bodies are folded. This is a syntactic check ---
we are _not_ folding the second level. For code like

```rust
fn f() { ... }

impl S {
fn g(&self) { ... }
}
```

Both `f` and `g` are folded, but `impl S` is not. Similarly, function parameters and function body
are actually on the same level of folding hierarchy, but it is imperative that parameters are _not_
folded. This is the part that was hard ten years ago but is easy today. "what is function body" is a
non-trivial question, which requires proper parsing of the code. These days, either an LSP server or
Tree-sitter can answer this question quickly and reliably.

_The second_ component of the feature is that folded is a default state. It is not a "fold method
bodies" _action_. It is a setting that ensures that, whenever you visit a new file, bodies are
folded by default. To make this work, the editor should be smart to seamlessly unfold specific
function when appropriate. For example, if you "go to definition" to a function, that function
should get unfolded, while the surrounding code should remail folded.

Now that I have explained how the feature works, I will _not_ try to motivate it. I think it is
pretty obvious how awesome this actually is. Code is read more often than written, and this is one
of the best multipliers for readability. _Most_ of the code is in method bodies, but most important
code is in function signatures. Folding bodies auto-magically hide the 80% of boring code, leaving
the most important 20%. It was in 2018 when I last the IDE (IntelliJ) which has this implemented
properly, and I've been missing this function ever since!

You might also be wondering whether it is the same feature as the Outline, that special UI which
shows a graphical, hierarchical table of contents of the file. It is true that outline and
fold-bodies-by-default attack the same issue. But I'd argue that folding solves it better. This is
an instance of a common pattern. In a smart editor, it is often possible to implement any given
feature either by "lowering" it to plain text, or by creating a dedicated GUI. And the lowering
approach almost always wins, because it gets to re-use all existing functionality for free. For
example, the folding approach trivially gives you an ability to move a bunch of functions from one
impl block to the other by selecting them with [Shift + Down]{.kbd}, cutting with [Ctrl + X]{.kbd}
and pasting with [Ctrl + V]{.kbd}.

## Call to Action

So, if you are a committer to one of the editors, please consider adding a "fold function bodies by
default" mode. It probably should be off by default, as it can easily scare new users away, but it
should be there for power users to enable, and it should be prominently documented, so that people
can learn that they want it. After the checkbox is in place, see if you can implement the actual
logic! If your editor uses Tree-sitter, this should be relatively easy --- its syntax tree contains
all the information you need. Just make sure that:

* bodies are folded when the new file is opened,
* the editor unfolds them when appropriate (generally, when navigated to a function from elsewhere).

If your editor is _not_ based on Tree-sitter, you'll have a harder time. In theory, the information
should be readily available from the language server, but LSP currently doesn't expose it. Here's
the problem:

<https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#foldingRangeKind>

There's no `body` kind there! Adding it should be trivially technically, but it always is a pain to
get something into the protocol if you are not VS Code.

## My Role

What is my job here, besides sitting there and writing blog posts? I actually think that writing
this down is quite valuable!

I suppose the feature is still commonly missing due to a two-sided market failure --- the feature
doesn't exist, so prospective users don't realize that it is possible, and don't ask editor's
authors to implement it. Without users asking, editor authors themselves don't realize this feature
could exist, and don't rush implementing it. This is exacerbated by the fact that it _was_ a hard
feature to implement ten years ago, when we didn't have Tree-sitter/LSP, so there are poor
workarounds in place --- actions to fold a certain _level_. These workarounds the prevent the proper
feature from gaining momentum.

So here I hope to maybe tip the equilibrium's scale a bit, and start a feedback loop where more
people realize that they _want_ this feature, such that it is implemented in some of the more
experimental editors, which hopefully would expose the feature to more users, popularizing it until
it gets implemented everywhere!

Still, just talking _isn't_ everything I did here! Six years ago, I implemented the language-server
side of this in rust-analyzer:

<https://github.com/rust-lang/rust-analyzer/commit/23b040962ff299feeef1f967bc2d5ba92b01c2bc>

This currently _isn't_ exposed to LSP, because it doesn't allow flagging a folding range as method
body. To fix _that_ I opened this VS Code issue (LSP generally avoids doing something before VS
Code):

<https://github.com/microsoft/vscode/issues/128912>

And since then I have been quietly waiting for some editor (not necessary VS Code) to pick this up.
This hasn't happened yet, hence this article!

Thanks for reading!
3 changes: 2 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"tasks": {
"build": " deno run --lock --allow-write=./out,./build --allow-read=./out,./build,./content --allow-net ./src/main.ts build --profile",
"watch": "rm -rf ./out && deno run --lock --allow-write=./out,./build --allow-read=./out,./build,./content --allow-net --watch ./src/main.ts watch",
"serve": "deno task watch & live-server --host 127.0.0.1 --port 8080 ./out/res"
"serve": "deno task watch & live-server --host 127.0.0.1 --port 8080 ./out/res",
"touch": "deno run --allow-write=./content/posts ./src/main.ts touch"
}
}
2 changes: 1 addition & 1 deletion src/djot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ ${pre}
const children = get_string_content(node)
.split("+")
.map((it) => `<kbd>${it}</kbd>`)
.join(" + ");
.join("+");
return `<kbd>${children}</kbd>`;
}
if (has_class(node, "menu")) {
Expand Down
10 changes: 9 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ async function main() {
};

const subcommand = Deno.args[0];
if (subcommand === "touch") {
const slug = Deno.args[1];
const date = new Date().toISOString().split("T")[0];
const path = `./content/posts/${date}-${slug}.dj`;
console.log(`touching ${path}`);
await Deno.writeTextFile(path, "#\n");
return;
}

let i = 1;
for (; i < Deno.args.length; i++) {
Expand Down Expand Up @@ -144,7 +152,7 @@ async function build(params: {
}

const redirects = [
["/2024/09/32/-what-is-io-uring.html", "/2024/09/23/what-is-io-uring.html"]
["/2024/09/32/-what-is-io-uring.html", "/2024/09/23/what-is-io-uring.html"],
];

for (const [from, to] of redirects) {
Expand Down

0 comments on commit a51b369

Please sign in to comment.