Skip to content

Commit

Permalink
feat: all harps have built-in relativity
Browse files Browse the repository at this point in the history
  • Loading branch information
Axlefublr committed Nov 8, 2024
1 parent cb9e855 commit e78fa65
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 291 deletions.
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ members = [
"xtask",
]

default-members = [
"helix-term"
]
default-members = ["helix-term"]

[profile.release]
lto = "thin"
Expand Down
217 changes: 56 additions & 161 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,215 +138,110 @@ If you find another use for them, nice! But in the general case you'll want to u
> If it contains spaces, that *may* be a problem, depending on what command you're using the expansion in.
> You may want to quote expansions, in that case.
### Harp
### Harp integration

My favorite feature I had in nvim! \
Inspired by [`harp-nvim`](https://github.com/Axlefublr/harp-nvim), implemented using the [`harp`](https://github.com/Axlefublr/harp) library. \
I will be using concepts from `harp` in my explanation, I expect you to have read the readme of that project. \
You don't have to be familiar with `harp-nvim`, but if you are, you'll get what I'm going to be talking about easier.
Inspired by [`harp-nvim`](https://github.com/Axlefublr/harp-nvim), implemented using the [`harp`](https://github.com/Axlefublr/harp) library.

You can think of "harps" as storage units. You can store some information related to the editor in a harp, to then use it later. \
Harps are *persistent*. Once you set a harp, it stays forever, until overwritten in the future (by you, again) and gets retained across helix sessions. \
Even if you have multiple helix sessions open at a time, if you set a harp in one session, it will *immediately* become available in another session.
A "harp" is essentially a storage unit. It lets you store some information from the editor to then use later. \
Harps are *persistent*. Once you set a harp, it stays forever (until overwritten by you) and gets shared across helix sessions. \
Even if you have multiple helix sessions open at a time, if you set a harp in one session, it will *immediately* become available in all sessions.

To organize harps, there are harp "sections". \
Sections exist to organize all the different types of harps, to allow you to use one harp name across multiple different harp *types*.
Each individual harp is given its name by *you*, interactively. \
So, you can store some file in a "file harp" called `a`, and then store your latest search in a "search harp" called also `a`; these will not conflict, and let you consistently use short names for maximum efficiency (if that is your preference). \
You can either *get* a harp (take the information stored in a harp and use it somehow) or *set* a harp (take some information from the environment and store it in a harp).
Harp "sections" exist to organize multiple different sets of harps. \
If all harps were stored in a single place, that would lead to name collisions: if you set a [file harp](#file-harps) `a`, you wouldn't be able to set a [register harp](#register-harps) `a` — the latter would override the former. \
So, all harp types are stored within their own "harp section", letting you use the same harp names without name collisions.

#### Why are harps helpful?
Each harp type has two actions: `get` and `set`. \
Both of them place you into an input field to type the name of the harp into. \
`set` takes some information from the environment (for example, the current buffer's filepath), and stores it in a harp.
`get` takes that information from a harp, and applies it somehow (for example, `:open`s the stored filepath).

Typing things in takes a long time, while being done so often.
The main idea of all harp types, is to let you store information by aliasing it: \
Instead of typing in a long file path, search pattern, or plain text, you can *store* it under a shorter, and more convenient alias.

Let's take `:open` for example. \
If you want to open a file with a really long path, you will ideally want to type in the minimum amount of characters possible, to then press <kbd>Tab</kbd> to autocomplete, on *each* path component. \
This consistently gets annoying when the string you need to type in to tab complete is long too. \
If you want to complete `~/downgrade/test.lua`, you need to type in `downg` to tab complete, because `Downloads` also exists in that directory. \
Then maybe in `downgrade`, there is also `test.py`. Now you have to type in `test.l` and at that point, it's not even worth completing.

Fuzzy searching is better but has a different issue. \
First of all, it's dependent on your current working directory: `file_picker` of helix, or `telescope` of nvim will usually open from your cwd. \
If you need to open a file from somewhere else, you're fucked.

In the case with helix, you can `:open` a directory to fuzzy search it, but at that point you're just mixing in two non-perfect methods.

The second issue with the fuzzy file picker, is the min-maxed fuzzy strings you end up creating and memorizing to get to the file you want the most efficiently (similar to the min-maxed tab complete strings in `:open`). \
Those can get pretty arbitrary; to get to `helix/generator.py` I have in my dotfiles, the fuzzy search for it is `raty`. \
Worse yet, it might change in the future, if I create a new file that matches `raty` more closely. \
Well, I don't have to deal with that anymore! Now it's literally just `c` and I can get to it from *anywhere* **instantly** using a file harp.

Simple, direct, fast.

Harps don't magically remove your usage of `:open` or the fuzzy search picker, they *minimize* it. \
First you get to some file using one of the two, and store it in a harp. \
Now you get a "bookmark" of the file, that lets you completely circumvent having to dance around with `:open`/fzf ever again with that file. \
The different harp types allow you to express *what* you're storing and relative to *where* you're storing it, giving you a lot of flexibility *and* speed.

Stop thinking about *how* to get to your file, let your muscle memory move you there.

### File harps
#### File harps

```
harp_file_set
harp_file_get
```

`harp_file_set` to store the full path of the current buffer into a harp in the `harp_files` section. \
`harp_file_get` to `:open` the file stored in the harp.

This is really useful for files that you know you want to open from *anywhere*. \
Perfect usecase is for dotfiles. \
Say you were just editing some code and opened lazygit. \
Now you realize that you want to change some setting in your lazygit config. \
But oh no! You were working on some project, and really can't be bothered to go locate the lazygit config file, so you just think "meh, I'll do that later" and forget about it. \
I always found that really annoying!

Well, if you previously stored that config file in a *file harp*, you don't need to be in this situation anymore. \
Just `harp_file_get`, change the setting you wanted, and go back to the file you were just editing in the project, continuing to use your (now reconfigured) lazygit. \
I've been using this for a while in nvim, and from personal experience, I noticed that you get to retain the "flow" state, which is *really* helpful when programming.

### Project file harps

```
harp_project_file_set
harp_project_file_get
```

`harp_project_file_set` to store the full path of the current buffer into a harp in a section\*. \
`harp_project_file_get` to `:open` the file stored in the harp.

\*The section used is built like this: it will always start with `harp_files_`, and after that string, your current working directory will be appended.

So if your current working directory is `~/prog/dotfiles` and your current buffer is `~/prog/dotfiles/colors.css`, when using a project file harp to save that buffer, you will be storing into a section named `harp_files_/home/username/prog/dotfiles`.

You can think of project file harps as file harps that are *relative to the project*.

This is pretty powerful! Normal file harps are mostly meant for files that are important to be accessible from *anywhere*. Things like config files that you may want to edit while in the middle of doing something else.

In this project, I visit `helix-term/src/commands.rs` pretty frequently. It is only ever relevant when my current working directory *is* this project, and yet with a normal file harp, I can't express that. \
I'd want to express it for this reason: I use the register name `c` for another (more commonly visited) harp already. \
I'd love to use `c` for `helix-term/src/commands.rs` too, but it's already taken, so I take the compromise of naming it `com` instead. \
This is not too bad when considered in a vacuum, but as my amount of file harps increases, the need to name them in increasingly complex ways does too.

With a project file harp, I can use `c` again! Matter of fact, I can use `c` in literally every different project I have, if I want to. There will be no conflicts, as they're stored in different sections.

When deciding between a file harp and a project file harp, ask yourself this question: "when I want to access this file, what will my current working directory generally be?".
`set` takes the *current buffer*'s filepath, and stores it in a harp. \
`get` takes it, and `:open`s it.

### Relative file harps
#### Relative file harps

```
harp_relative_file_set
harp_relative_file_get
```

`harp_relative_file_set` takes current buffer's path, relative to cwd, and stores it in a harp in the `harp_relative_files` section. \
`harp_relative_file_get` takes the stored path, and opens it (relative to cwd).
`set` takes the current buffer's filepath, and stores it in a harp. \
HOWEVER, it stores only the part of the full path, that's relative to current working directory.

Project structure tends to repeat: rust projects will have a `Cargo.toml`, `src/main.rs` or `src/lib.rs`; *every* project will have a `README.md`, maybe a `CONTRIBUTING.MD`; almost every git repo will have a `.gitignore`, and you may also use `.git/info/exclude`.
Say your current buffer path is `~/prog/dotfiles/colors.css` and your current working directory is `~/prog/dotfiles`. \
If you use a normal [file harp](#file-harps), you will store the full path: `~/prog/dotfiles/colors.css`. \
If you use a *relative* file harp, you will store the *relative* path: `colors.css`.

As you use project file harps for these, you'll quickly notice that doing so is inefficient. \
Project file harps make the most sense for files that are "special" to a specific project, while all of these are fairly regular.
So then, the `get` action will just open that path relatively — as if you did `:o colors.css`. This will end up opening a different file depending on your current working directory.

*Relative* file harps let you store just the *relative* part of a file in a harp. \
If your current buffer is `~/prog/dotfiles/README.md`, and your current working directory is `~/prog/dotfiles`, \
A *relative* file harp will store just `README.md`, while a normal file harp would store the *entire* path: `~/prog/dotfiles/README.md`.
The design idea behind this, is to store paths that repeat in *project structures*.

What this lets you do is lovely: you can store a file just once, and then continue reusing the harp for it for all of your projects. \
Instead of having to type in `:e .gitignore` to get to it, you just use the relative file harp `g` (for example) and immediately jump to the gitignore file of *the current* project.
Look at these paths for example: `.gitignore`, `src/main.rs`, `src/lib.rs`, `Cargo.toml`, `.git/info/exclude`, `README.md`, `CONTRIBUTING.md` \
All of these tend to repeat in a lot of projects — they're not particularly unique paths.
So it doesn't make sense to store them in normal file harps, that are *designed* for unique paths. \
Instead with relative file harps, you get to efficiently refer to "the same file", which ends up being a different *actual* file depending on your current working directory.

### Cwd harps
#### Cwd harps

```
harp_cwd_set
harp_cwd_get
```

`harp_cwd_set` takes your current working directory (like from `:pwd`), and stores it in a harp in the `harp_dirs` section. \
`harp_cwd_get` takes the stored directory, and `:cd`s into it.
`set` stores your current working directory in a harp, `get` `:cd`s into a stored working directory.

Development is pretty projectual, and jumping through a bunch of commonly visited directories can be a chore. Closing and reopening helix just to fuzzy search some file somewhere is a bit too much effort.

I have a very specific example:

I use `lazygit`, and have a config for it stored in `~/prog/dotfiles/lazygit.yml`. \
I change things in it every so often, and to make myself not have to google the default config every time, I store it as a file in `~/prog/backup/default/lazygit.yml`.

This one time, let's `:cd ~/prog/backup` and use `harp_cwd_set` to store that working directory as a cwd harp named `b`. \
Next time, when I want to access that file while being in `~/prog/dotfiles`, everything gets easier!

Instead of:
* close helix
* travel to `~/prog/backup`
* open helix
* fuzzy search for `lazygit`

I just do
* `harp_cwd_get` -> `b`
* fuzzy search for `lazygit`

Quite a bit nicer! Even better than that, is that assuming we set `dotfiles` too, we can easily come *back* as well!

You might point out that a normal file harp would suffice. You would be correct! In a vaccuum, using a file harp to get to a file is optimal. \
However, that `backup` directory of mine contains a lot of useful files, and it's simply more *cost effective* (in terms of my brain memory) to just mark the directory, rather than coming up with appropriate names for each individual file. \
If I figure out that the default lazygit config, in specific, I visit often enough to warrant a file harp, I *still* benefit from the cwd harp I set, \
because *getting* to the file to then set the file harp for it still gets easier!

The more obvious usecase for this feature, of course, is if you work on a bunch of projects at the same time, you can switch between them more easily. But, like, duh.

### Search harps
#### Search harps

```
harp_search_set
harp_search_get
```

`harp_search_set` gets your latest search (stored in the `/` register) and stores it in a harp in the `harp_searches` section. \
`harp_search_get` puts the stored search into your `/` register, effectively "making a search".
`set` takes your latest search pattern from register `/` and stores it in a harp. \
`get` takes a stored search pattern, and puts it back into register `/`, effectively "making a search".

#### Register harps

A really obvious thing to want to do in an editor is to trim trailing whitespace. \
In helix it's a bit of a hassle: `%s[ \t]+$<CR>d`, where `<CR>` means <kbd>Enter</kbd>. \
Focusing on the pattern: `[ \t]+$` is just slightly too much to type in for me. \
`\s` won't work there because the final newline in the file gets matched. \
I could I guess rely on helix to append it, but that seems a bit wack to do anyway. \
`[ \t]` is too much to type in, so I might opt for just matching spaces. \
But then I might miss some rogue tab!
```
harp_register_set
harp_register_get
```

`set` puts the contents of your default register (`"`) into a harp. \
`get` puts the stored text back into your default register (`"`)

With search harps, I can store this search in a harp. `t`, for example. \
Now instead of having to type in `[ \t]+$`, I can press my mapping for search harps, do `t<CR>`, \
and then when I `%s`, I'll see the pattern I want as an autosuggestion. \
I can press <kbd>Enter</kbd> twice, and bob's my uncle.
If you use `set` while having multiple selections, they are joined into a single one with newlines.

You might argue that this is a pretty small binding optimization, and you might be right about that. \
For some reason though, grabbing a pattern like this still *feels* better to me than typing it in, even though it's not *that* long.
---

A better example would be some more complex / long pattern, that is simply unreasonable to type in, kinda ever. \
Or maybe a pattern that you will forget, but could remember the harp name of.
Now that you're familiar with all the harp types, let me introduce you to the feature of relativity.

Because search harps just put the output into your `/` register, the searches end up as autosuggestions in a lot of places. \
For example, you can search for `struct HarpOutput` to match the definition of a struct you have *somewhere* in the codebase, and then save it in the `hout` search harp. \
When you get that `hout` search harp in the future, you can open the global_search picker to see `struct HarpOutput` autosuggested. \
Press <kbd>Enter</kbd> twice and blammo. \
This way, you can get to the definition of the struct in a faster way, than relying on your lsp. Especially true with rust, but generally raw text matching will happen faster than your lsp responding.
Normally-named harps are "global" harps. Harps that are not relative to anything.

Probably the most useful example: \
Search for `(TODO|FIXME|HACK):` and store it in a search harp. \
Now you have a very convenient way to look through TODOs of any project: just `get` the search harp for it, open `global_search`, press enter, and see all of your results.
If a harp name starts with a `.`, it becomes relative to your current working directory. \
If starts with `,`, relative to the current buffer. \
If starts with `;`, relative to the filetype (run `:lang` to check the filetype of the current buffer).

### Register harps
Remember how different harp types are stored in different sections to fight against name collisions? \
The same thing happens here: "relativity" is made by appending the directory path / buffer path / filetype onto the name of each section.

```
harp_register_set
harp_register_get
```
This way, you can have "global" file harps, but also file harps that are specific to the current project you're working on.

`harp_register_set` puts the contents of your default register (`"`) into a harp, in section `harp_registers` \
`harp_register_get` puts text stored in a harp into your default helix register (`"`)
Useful global searches like `(TODO|FIXME|HACK|MOVE):?`, and buffer-specific searches like `// asdf I left off here`.

Have you ever been annoyed that registers don't persist across sessions? I sure have!
Project-specific register harps, as a way to gain register session persistence, and filetype-specific register harps, that can act as a basic snippet implementation.

Register harps essentially solve that, letting you store some text ✨forever✨. \
If you make a mapping for `harp_register_set` in insert mode, you can even use that as a very basic snippets implementation.
How you use relativity is up to you! In some cases relativity doesn't make sense logically, but this approach lets me implement flexible functionality that *you* may, in some cases, use in ways that I didn't think of.

---

Expand Down
6 changes: 2 additions & 4 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,12 +580,10 @@ impl MappableCommand {
harp_file_set, "Set a file harp to the current buffer",
harp_relative_file_get, "Open a relative file harp",
harp_relative_file_set, "Set a relative file harp to the current buffer",
harp_project_file_get, "Open a relative to cwd file harp",
harp_project_file_set, "Set a relative to cwd file harp to the current buffer",
harp_cwd_get, ":cd into a cwd harp",
harp_cwd_get, "Change directory to a cwd harp",
harp_cwd_set, "Update cwd harp to be the current working directory",
harp_search_get, "Search for a stored search harp",
harp_search_set, "Set a search harp to the contents of your `/` register",
harp_search_set, "Set a search harp to your last search",
harp_register_get, "Get a register harp into default register",
harp_register_set, "Set a register harp from default register",
shell_replace_with_output, "Replace selections with the output of a shell command",
Expand Down
Loading

0 comments on commit e78fa65

Please sign in to comment.