Skip to content

Commit 7ec6f0d

Browse files
committed
Steel plugin support
This is a squashed version of helix-editor#8675. 2270457
1 parent 09f6bfb commit 7ec6f0d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+14890
-416
lines changed

Cargo.lock

Lines changed: 1371 additions & 322 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ package.helix-term.opt-level = 2
4040
tree-house = { version = "0.3.0", default-features = false }
4141
nucleo = "0.5.0"
4242
slotmap = "1.0.7"
43+
steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.7.0", features = ["anyhow", "dylibs", "sync", "triomphe", "imbl"] }
4344
thiserror = "2.0"
4445
tempfile = "3.22.0"
4546
bitflags = "2.9"

STEEL.md

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# Building
2+
3+
You will need:
4+
5+
* A clone of this fork, on the branch `steel-event-system`
6+
7+
## Installing helix
8+
9+
Just run
10+
11+
`cargo xtask steel`
12+
13+
To install the `hx` executable, with steel as a plugin language. This also includes:
14+
15+
The `steel` executable, the steel language server, the steel dylib installer, and the steel package manager `forge`.
16+
17+
## Developing
18+
19+
The easiest way to contribute would be to adjust the default features on the `helix-term` crate:
20+
21+
```toml
22+
[features]
23+
features = ["git", "steel"]
24+
```
25+
26+
## Setting up configurations for helix
27+
28+
There are 2 important files you'll want, which should be auto generated during the installation process if they don't already exist:
29+
30+
* `~/.config/helix/helix.scm`
31+
* `~/.config/helix/init.scm`
32+
33+
Note - these both live inside the same directory that helix sets up for runtime configurations.
34+
35+
### `helix.scm`
36+
37+
The `helix.scm` module will be loaded first before anything else, the runtime will `require` this module, and any functions exported will now be available
38+
to be used as typed commands. For example:
39+
40+
41+
```scheme
42+
# helix.scm
43+
(require "helix/editor.scm")
44+
(require (prefix-in helix. "helix/commands.scm"))
45+
(require (prefix-in helix.static. "helix/static.scm"))
46+
47+
(provide shell git-add open-helix-scm open-init-scm)
48+
49+
;;@doc
50+
;; Specialized shell implementation, where % is a wildcard for the current file
51+
(define (shell cx . args)
52+
;; Replace the % with the current file
53+
(define expanded (map (lambda (x) (if (equal? x "%") (current-path cx) x)) args))
54+
(apply helix.run-shell-command expanded))
55+
56+
;;@doc
57+
;; Adds the current file to git
58+
(define (git-add cx)
59+
(shell cx "git" "add" "%"))
60+
61+
(define (current-path)
62+
(let* ([focus (editor-focus)]
63+
[focus-doc-id (editor->doc-id focus)])
64+
(editor-document->path focus-doc-id)))
65+
66+
;;@doc
67+
;; Open the helix.scm file
68+
(define (open-helix-scm)
69+
(helix.open (helix.static.get-helix-scm-path)))
70+
71+
;;@doc
72+
;; Opens the init.scm file
73+
(define (open-init-scm)
74+
(helix.open (helix.static.get-init-scm-path)))
75+
76+
77+
```
78+
79+
Now, if you'd like to add the current file you're editing to git, simply type `:git-add` - you'll see the doc pop up with it since we've annotated the function
80+
with the `@doc` symbol. Hitting enter will execute the command.
81+
82+
You can also conveniently open the `helix.scm` file by using the typed command `:open-helix-scm`.
83+
84+
85+
### `init.scm`
86+
87+
The `init.scm` file is run at the top level, immediately after the `helix.scm` module is `require`d. The helix context is available here, so you can interact with the editor.
88+
89+
The helix context is bound to the top level variable `*helix.cx*`.
90+
91+
For example, if we wanted to select a random theme at startup:
92+
93+
```scheme
94+
# init.scm
95+
96+
(require-builtin steel/random as rand::)
97+
(require (prefix-in helix. "helix/commands.scm"))
98+
(require (prefix-in helix.static. "helix/static.scm"))
99+
100+
;; Picking one from the possible themes
101+
(define possible-themes '("ayu_mirage" "tokyonight_storm" "catppuccin_macchiato"))
102+
103+
(define (select-random lst)
104+
(let ([index (rand::rng->gen-range 0 (length lst))]) (list-ref lst index)))
105+
106+
(define (randomly-pick-theme options)
107+
;; Randomly select the theme from the possible themes list
108+
(helix.theme (select-random options)))
109+
110+
(randomly-pick-theme possible-themes)
111+
112+
```
113+
114+
### Libraries for helix
115+
116+
There are a handful of extra libraries in development for extending helix, and can be found here https://github.com/mattwparas/helix-config.
117+
118+
If you'd like to use them, create a directory called `cogs` in your `.config/helix` directory, and copy the files in there.
119+
120+
### options.scm
121+
122+
If you'd like to override configurations from your toml config:
123+
124+
125+
```scheme
126+
# init.scm
127+
128+
(require "helix/configuration.scm")
129+
130+
(file-picker (fp-hidden #f))
131+
(cursorline #t)
132+
(soft-wrap (sw-enable #t))
133+
134+
```
135+
136+
137+
### keymaps.scm
138+
139+
Applying custom keybindings for certain file extensions:
140+
141+
```scheme
142+
# init.scm
143+
144+
(require "cogs/keymaps.scm")
145+
(require (only-in "cogs/file-tree.scm" FILE-TREE-KEYBINDINGS FILE-TREE))
146+
(require (only-in "cogs/recentf.scm" recentf-open-files get-recent-files recentf-snapshot))
147+
148+
;; Set the global keybinding for now
149+
(add-global-keybinding (hash "normal" (hash "C-r" (hash "f" ":recentf-open-files"))))
150+
151+
(define scm-keybindings (hash "insert" (hash "ret" ':scheme-indent "C-l" ':insert-lambda)))
152+
153+
;; Grab whatever the existing keybinding map is
154+
(define standard-keybindings (deep-copy-global-keybindings))
155+
156+
(define file-tree-base (deep-copy-global-keybindings))
157+
158+
(merge-keybindings standard-keybindings scm-keybindings)
159+
(merge-keybindings file-tree-base FILE-TREE-KEYBINDINGS)
160+
161+
(set-global-buffer-or-extension-keymap (hash "scm" standard-keybindings FILE-TREE file-tree-base))
162+
163+
```
164+
165+
In insert mode, this overrides the `ret` keybinding to instead use a custom scheme indent function. Functions _must_ be available as typed commands, and are referred to
166+
as symbols. So in this case, the `scheme-indent` function was exported by my `helix.scm` module.
167+
168+
169+
## Writing a plugin
170+
171+
### Getting setup
172+
173+
Before you start, you should make sure that your configuration for the steel lsp is wired up correctly. This will give you
174+
access to the documentation that will help you as you write your plugin. To configure the LSP, you can add this to your
175+
`init.scm`:
176+
177+
```scheme
178+
(require "helix/configuration.scm")
179+
(define-lsp "steel-language-server" (command "steel-language-server") (args '()))
180+
(define-language "scheme"
181+
(language-servers '("steel-language-server")))
182+
```
183+
184+
This will give you an interactive setup that can help you run plugins as you go. I also like to evaluate commands
185+
via the buffer, by either typing them in to the command prompt or by loading the current buffer. To load the current
186+
buffer, you can type `:eval-buffer`, or to evaluate an individual command, you can run `:evalp` - note, in your init.scm, you
187+
may need to add:
188+
189+
```scheme
190+
(require (only-in "helix/ext" evalp eval-buffer))
191+
```
192+
193+
This brings those functions to the top level scope so that you can interact with them. You may also be keen to peruse all of the steel
194+
functions and modules available. Those can be found in `steel-docs.md`.
195+
196+
197+
### Command API
198+
199+
There are two levels of the functionality exposed to plugins. The first is simply based around
200+
chaining builtin commands, as if you're a lightning fast human typing commands very quickly. The other level
201+
is a bit lower, and deals directly with the component API that helix uses to draw the text editor and various
202+
popups, like the file picker or buffer selection.
203+
204+
To understand the first level, which is accessing typed commands and static commands, i.e. commands that you
205+
typically type via `:`, or static commands, commands which are bound to keybindings, you can look at the modules:
206+
207+
* helix/commands.scm
208+
* helix/static.scm
209+
210+
Every function here implicitly has access to a context, the helix context. This assumes that you're focused onto
211+
some buffer, and any actions are assumed to be done within that context. For example, calling `vsplit` will
212+
split the currently focused into a second, and move your focus to that window. Keeping track of that is important
213+
to understand where your focus is.
214+
215+
In general, these functions do not return anything, given that they're purely for side effects. There are some functions
216+
that do, and they should be documented as such. The API will need to be improved to return useful things where relevant.
217+
218+
### The UI
219+
220+
A good rule of thumb is to not block the UI. During the execution of a steel function, the helix context is exclusively
221+
available to that executing function. As a result, you should not have long running functions there (note - if you end
222+
up having an infinite loop of some kind, `ctrl-c` should break you out).
223+
224+
Luckily, there are a handful of ways we can accomplish more sophisticated plugins:
225+
226+
* Futures
227+
* Threads
228+
229+
There are a handful of primitives that accept a future + a callback, where the callback will get executed once the future
230+
is complete. The future will get scheduled on to the helix event loop, so the UI won't be blocked. (TODO: Document this more!)
231+
232+
Another way we can accomplish this is with native threads. Steel supports native threads, which means we can spawn a function
233+
off on to another thread to run some code. Consider the following example which won't work:
234+
235+
236+
```scheme
237+
(spawn-native-thread (lambda () (time/sleep-ms 1000) (theme "focus_nova"))) ;; Note, this won't work!
238+
```
239+
240+
This appears to spawn a thread, sleep for 1 second, and then change the theme. The issue here is that this thread does not
241+
have control over the helix context. So what we'll have to do instead, is schedule a function to be run on the main thread:
242+
243+
244+
```scheme
245+
(require "helix/ext.scm")
246+
(require-builtin steel/time)
247+
248+
(spawn-native-thread
249+
(lambda ()
250+
(hx.block-on-task
251+
(lambda ()
252+
(time/sleep-ms 1000)
253+
(theme "focus_nova")))))
254+
```
255+
256+
`hx.block-on-task` will check if we're running on the main thread. If we are already, it doesn't do anything - but otherwise,
257+
it enqueues a callback that schedules itself onto the main thread, and waits till it can acquire the helix context. The function
258+
is then run, and the value returned back to this thread of control.
259+
260+
261+
There is also `hx.with-context` which does a similar thing, except it does _not_ block the current thread.
262+
263+
### Components
264+
265+
Coming soon!

helix-core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ homepage.workspace = true
1414
[features]
1515
unicode-lines = ["ropey/unicode_lines"]
1616
integration = []
17+
steel = ["dep:steel-core"]
1718

1819
[dependencies]
1920
helix-stdx = { path = "../helix-stdx" }
@@ -53,6 +54,7 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
5354

5455
textwrap = "0.16.2"
5556

57+
steel-core = { workspace = true, optional = true }
5658
nucleo.workspace = true
5759
parking_lot.workspace = true
5860
globset = "0.4.16"

helix-core/src/command_line.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,13 @@ impl<'a> Args<'a> {
766766
}
767767
}
768768

769+
pub fn raw(positionals: Vec<Cow<'a, str>>) -> Self {
770+
Self {
771+
positionals,
772+
..Self::default()
773+
}
774+
}
775+
769776
/// Reads the next token out of the given parser.
770777
///
771778
/// If the command's signature sets a maximum number of positionals (via `raw_after`) then
@@ -872,7 +879,7 @@ impl<'a> Args<'a> {
872879

873880
/// Performs any validations that must be done after the input args are finished being pushed
874881
/// with `Self::push`.
875-
fn finish(&self) -> Result<(), ParseArgsError<'a>> {
882+
pub fn finish(&self) -> Result<(), ParseArgsError<'a>> {
876883
if !self.validate {
877884
return Ok(());
878885
};
@@ -1123,7 +1130,7 @@ mod test {
11231130
assert_incomplete_tokens(r#"echo %{hello {{} world}"#, &["echo", "hello {{} world}"]);
11241131
}
11251132

1126-
fn parse_signature<'a>(
1133+
pub fn parse_signature<'a>(
11271134
input: &'a str,
11281135
signature: Signature,
11291136
) -> Result<Args<'a>, Box<dyn std::error::Error + 'a>> {

0 commit comments

Comments
 (0)