Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Steel as an optional plugin system #8675

Draft
wants to merge 117 commits into
base: master
Choose a base branch
from

Conversation

mattwparas
Copy link

Notes:

  • I still need to rebase up with the latest master changes, however doing so causes some headache with the lock file, so I'll do it after some initial feedback. Also, this depends on the event system in Add an event system #8021.
  • The large diff size is a combination of lock file changes + the dependency on the event system PR. The diff has ended up quite large with all of the other stuff
  • I'm currently pointing to the master branch of steel as a dependency. This will point to a stable release on crates once I cut a release.

Opening this just to track progress on the effort and gather some feedback. There is still work to be done but I would like to gather some opinions on the direction before I continue more.

You can see my currently functioning helix config here and there are instructions listed in the STEEL.md file. The main repo for steel lives here, however much documentation is in works and will be added soon.

The bulk of the implementation lies in the engine.rs and scheme.rs files.

Design

Given prior conversation about developing a custom language implementation, I attempted to make the integration with Steel as agnostic of the engine as possible to keep that door open.

The interface I ended up with (which is subject to change and would love feedback on) is the following:

pub trait PluginSystem {
    /// If any initialization needs to happen prior to the initialization script being run,
    /// this is done here. This is run before the context is available.
    fn initialize(&self) {}

    fn engine_name(&self) -> PluginSystemKind;

    /// Post initialization, once the context is available. This means you should be able to
    /// run anything here that could modify the context before the main editor is available.
    fn run_initialization_script(&self, _cx: &mut Context) {}

    /// Allow the engine to directly handle a keymap event. This is some of the tightest integration
    /// with the engine, directly intercepting any keymap events. By default, this just delegates to the
    /// editors default keybindings.
    #[inline(always)]
    fn handle_keymap_event(
        &self,
        _editor: &mut ui::EditorView,
        _mode: Mode,
        _cxt: &mut Context,
        _event: KeyEvent,
    ) -> Option<KeymapResult> {
        None
    }

    /// This attempts to call a function in the engine with the name `name` using the args `args`. The context
    /// is available here. Returns a bool indicating whether the function exists or not.
    #[inline(always)]
    fn call_function_if_global_exists(
        &self,
        _cx: &mut Context,
        _name: &str,
        _args: &[Cow<str>],
    ) -> bool {
        false
    }

    /// This is explicitly for calling a function via the typed command interface, e.g. `:vsplit`. The context here
    /// that is available is more limited than the context available in `call_function_if_global_exists`. This also
    /// gives the ability to handle in progress commands with `PromptEvent`.
    #[inline(always)]
    fn call_typed_command_if_global_exists<'a>(
        &self,
        _cx: &mut compositor::Context,
        _input: &'a str,
        _parts: &'a [&'a str],
        _event: PromptEvent,
    ) -> bool {
        false
    }

    /// Given an identifier, extract the documentation from the engine.
    #[inline(always)]
    fn get_doc_for_identifier(&self, _ident: &str) -> Option<String> {
        None
    }

    /// Fuzzy match the input against the fuzzy matcher, used for handling completions on typed commands
    #[inline(always)]
    fn available_commands<'a>(&self) -> Vec<Cow<'a, str>> {
        Vec::new()
    }

    /// Retrieve a theme for a given name
    #[inline(always)]
    fn load_theme(&self, _name: &str) -> Option<Theme> {
        None
    }

    /// Retrieve the list of themes that exist within the runtime
    #[inline(always)]
    fn themes(&self) -> Option<Vec<String>> {
        None
    }

    /// Fetch the language configuration as monitored by the plugin system.
    ///
    /// For now - this maintains backwards compatibility with the existing toml configuration,
    /// and as such the toml error is exposed here.
    #[inline(always)]
    fn load_language_configuration(&self) -> Option<Result<Configuration, toml::de::Error>> {
        None
    }
}

If you can implement this, the engine should be able to be embedded within Helix. On top of that, I believe what I have allows the coexistence of multiple scripting engines, with a built in priority for resolving commands / configurations / etc.

As a result, Steel here is entirely optional and also remains completely backwards compatible with the existing toml configuration. Steel is just another layer on the existing configuration chain, and as such will be applied last. This applies to both the config.toml and the languages.toml. Keybindings can be defined via Steel as well, and these can be buffer specific, language specific, or global. Themes can also be defined from Steel code and enabled, although this is not as rigorously tested and is a relatively recent addition. Otherwise, I have been using this as my daily driver to develop for the last few months.

I opted for a two tiered approach, centered around a handful of design ideas that I'd like feedback on:

The first, there is a init.scm and a helix.scm file - the helix.scm module is where you define any commands that you would like to use at all. Any function exposed via that module is eligible to be used as a typed command or via a keybinding. For example:

;; helix.scm

(provide shell)

;;@doc
;; Specialized shell - also be able to override the existing definition, if possible.
(define (shell cx . args)
  ;; Replace the % with the current file
  (define expanded (map (lambda (x) (if (equal? x "%") (current-path cx) x)) args))
  (helix.run-shell-command cx expanded helix.PromptEvent::Validate))

This would then make the command :shell available, and it will just replace the % with the current file. The documentation listed in the @doc doc comment will also pop up explaining what the command does:

image

Once the helix.scm module is require'd - then the init.scm file is run. One thing to note is that the helix.scm module does not have direct access to a running helix context. It must act entirely stateless of anything related to the helix context object. Running init.scm gives access to a helix object, currently defined as *helix.cx*. This is something I'm not sure I particularly love, as it makes async function calls a bit odd - I think it might make more sense to make the helix context just a global inside of a module. This would also save the hassle that every function exposed has to accept a cx parameter - this ends up with a great deal of boilerplate that I don't love. Consider the following:

;;@doc
;; Create a file under wherever we are
(define (create-file cx)
  (when (currently-in-labelled-buffer? cx FILE-TREE)
    (define currently-selected (list-ref *file-tree* (helix.static.get-current-line-number cx)))
    (define prompt
      (if (is-dir? currently-selected)
          (string-append "New file: " currently-selected "/")
          (string-append "New file: "
                         (trim-end-matches currently-selected (file-name currently-selected)))))

    (helix-prompt!
     cx
     prompt
     (lambda (cx result)
       (define file-name (string-append (trim-start-matches prompt "New file: ") result))
       (temporarily-switch-focus cx
                                 (lambda (cx)
                                   (helix.vsplit-new cx '() helix.PromptEvent::Validate)
                                   (helix.open cx (list file-name) helix.PromptEvent::Validate)
                                   (helix.write cx (list file-name) helix.PromptEvent::Validate)
                                   (helix.quit cx '() helix.PromptEvent::Validate)))

       (enqueue-thread-local-callback cx refresh-file-tree)))))

Every function call to helix built ins requires passing in the cx object - I think just having them be able to reference the global behind the scenes would make this a bit ergonomic. The integration with the helix runtime would make sure whether that variable actually points to a legal context, since we pass this in via reference, so it is only alive for the duration of the call to the engine.

Async functions

Steel has support for async functions, and has successfully been integrated with the tokio runtime used within helix, however it requires constructing manually the callback function yourself, rather than elegantly being able to use something like await. More to come on this, since the eventual design will depend on the decision to use a local context variable vs a global one.

Built in functions

The basic built in functions are first all of the function that are typed and static - i.e. everything here:

However, these functions don't return values so aren't particularly useful for anything but their side effects to the editor state. As a result, I've taken the liberty of defining functions as I've needed/wanted them. Some care will need to be decided what those functions actually exposed are.

Examples

Here are some examples of plugins that I have developed using Steel:

File tree

Source can be found here

filetree.webm

Recent file picker

Source can be found here

recent-files.webm

This persists your recent files between sessions.

Scheme indent

Since steel is a scheme, there is a relatively okay scheme indent mode that only applied on .scm files, which can be found here. The implementation requires a little love, but worked enough for me to use helix to write scheme code 😄

Terminal emulator

I did manage to whip up a terminal emulator, however paused the development of it while focusing on other things. When I get it back into working shape, I will post a video of it here. I am not sure what the status is with respect to a built in terminal emulator, but the one I got working did not attempt to do complete emulation, but rather just maintained a shell to interact with non-interactively (e.g. don't try to launch helix in it, you'll have a bad time 😄 )

Steel as a choice for a language

I understand that there is skepticism around something like Steel, however I have been working diligently on improving it. My current projects include shoring up the documentation, and working on an LSP for it to make development easier - but I will do that in parallel with maintaining this PR. If Steel is not chosen and a different language is picked, in theory the API I've exposed should do the trick at least with matching the implementation behavior that I've outlined here.

Pure rust plugins

As part of this, I spent some time trying to expose a C ABI from helix to do rust to rust plugins directly in helix without a scripting engine, with little success. Steel supports loading dylibs over a stable abi (will link to documentation once I've written it). I used this to develop the proof of concept terminal emulator. So, you might not be a huge fan of scheme code, but in theory you can write mostly Rust and use Steel as glue if you'd like - you would just be limited to the abi compatible types.

System compatibility

I develop off of Linux and Mac - but have not tested on windows. I have access to a windows system, and will get around to testing on that when the time comes.

@jyn514
Copy link
Contributor

jyn514 commented Aug 13, 2024

For the language server, it will depend on how you've installed. I think the setup I have recently should install the language server, but I didn't provide any setup there. You can add something like this to your languages.toml:

thanks! this worked, although i also had to follow the instructions in the steel-lsp readme to install it and the required config - it would be nice if cargo xtask steel did that for me. the server crashed at first because i didn't define STEEL_LSP_HOME; it would be nice if it fell back to just assuming there are no globals in that case, as if the directory were empty.

now that i've done that LSP configuration is mostly working, except that it complains that Document-path isn't defined. i tried adding

(define configuration (#%module "helix/core/configuration"))
(define misc (#%module "helix/core/misc"))

to globals.scm, but it didn't help - i think this may still only be defined in the source and not anywhere in ~/.config/helix?

anyway, this is working quite well enough for me to use it :) thanks for all your hard work on this!

@jyn514
Copy link
Contributor

jyn514 commented Aug 13, 2024

this worked, although i also had to follow the instructions in the steel-lsp readme to install it and the required config - it would be nice if cargo xtask steel did that for me

hmm, i suppose setting STEEL_LSP_HOME in a shell-independent way is hard. languages.toml does support a language-server.*.environment key, but it doesn't support any kind of variables, so you'd have to hard-code the absolute path which seems kinda icky.

@mattwparas
Copy link
Author

mattwparas commented Aug 13, 2024

I did do this whole setup on a new machine and found this issue out with the lsp home as well - I have some ideas for making this smoother. I also am about halfway through documenting the functions - now when you run cargo xtask code-gen, it should spit out a steel-doc.md which has the functions and their doc strings if they exist. Unfortunately if there isn't a doc, it won't show it, so I'm working on the remaining docs (and also having it just dump all functions, but that's a separate issue)

So tl;dr I'll have a fix for your above issues soon!

@merisbahti
Copy link
Contributor

How do I get the steel globals required for the LSP to work correctly?

image

the contents of my globals are:

(define keymaps (#%module "helix/core/keymaps"))

(define (register-values module values)
  (map (lambda (ident) (#%module-add module (symbol->string ident) void)) values))

(register-values keymaps
  '(helix-current-keymap *buffer-or-extension-keybindings*
    *reverse-buffer-map*
    helix-merge-keybindings
    helix-string->keymap
    *global-keybinding-map*
    helix-deep-copy-keymap))

(define typable-commands (#%module "helix/core/typable"))
(define static-commands (#%module "helix/core/static"))
(define editor (#%module "helix/core/editor"))

(register-values typable-commands '())

(define configuration (#%module "helix/core/configuration"))
(define misc (#%module "helix/core/misc"))

(#%ignore-unused-identifier "_")

keymaps

@MatrixManAtYrService
Copy link

MatrixManAtYrService commented Aug 23, 2024

Would it be reasonable to have ./.helix among the places that are consulted for helix.scm and init.scm? I rather like how languages.toml can be placed there (see item 3 here). This lets me have different configs for different projects. The same kind of cwd-based switching could be handy for developing different plugins without reorganizing your config dir all the time. (it's also possible that I've misunderstood these files 😬)

I've started putting together a helix plugin dev env--but I'm struggling to make the transition in/out of the env seamless because although I can have a per-project ./.helix/helix.scm, I can't have a per-project ~/.config/helix/helix.scm

@3541
Copy link

3541 commented Aug 25, 2024

Is there any way to write a plugin that sends an arbitrary LSP request? #8125 suggested that a plugin would be the best way to support Clangd's extension to switch between headers and source files.

Had a quick glance through engine/steel.rs and did not see anything in that direction.

@mattwparas
Copy link
Author

How do I get the steel globals required for the LSP to work correctly?

image the contents of my globals are:
(define keymaps (#%module "helix/core/keymaps"))

(define (register-values module values)
  (map (lambda (ident) (#%module-add module (symbol->string ident) void)) values))

(register-values keymaps
  '(helix-current-keymap *buffer-or-extension-keybindings*
    *reverse-buffer-map*
    helix-merge-keybindings
    helix-string->keymap
    *global-keybinding-map*
    helix-deep-copy-keymap))

(define typable-commands (#%module "helix/core/typable"))
(define static-commands (#%module "helix/core/static"))
(define editor (#%module "helix/core/editor"))

(register-values typable-commands '())

(define configuration (#%module "helix/core/configuration"))
(define misc (#%module "helix/core/misc"))

(#%ignore-unused-identifier "_")

keymaps

I'm not able to reproduce this unfortunately, is this still an issue?

Would it be reasonable to have ./.helix among the places that are consulted for helix.scm and init.scm? I rather like how languages.toml can be placed there (see item 3 here). This lets me have different configs for different projects. The same kind of cwd-based switching could be handy for developing different plugins without reorganizing your config dir all the time. (it's also possible that I've misunderstood these files 😬)

I've started putting together a helix plugin dev env--but I'm struggling to make the transition in/out of the env seamless because although I can have a per-project ./.helix/helix.scm, I can't have a per-project ~/.config/helix/helix.scm

I think this should be possible. They won't be able to be arbitrarily overlaid, but it should be easy to just prefer one versus the other.

Is there any way to write a plugin that sends an arbitrary LSP request? #8125 suggested that a plugin would be the best way to support Clangd's extension to switch between headers and source files.

Had a quick glance through engine/steel.rs and did not see anything in that direction.

Just looked into this, I think it should be possible, although it would require me to expose some LSP apis that may or may not land with the first pass of this. If you're fine riding on the bleeding edge, I can try to get something out with this.

@3541
Copy link

3541 commented Aug 25, 2024

Just looked into this, I think it should be possible, although it would require me to expose some LSP apis that may or may not land with the first pass of this. If you're fine riding on the bleeding edge, I can try to get something out with this.

Excellent, many thanks. I am prepared to bleed. Will take a look later today.

@3541
Copy link

3541 commented Aug 26, 2024

Magical!

;;@doc
;; Toggles between C/C++ source and header files
(define (alt)
  (send-lsp-command "clangd" "textDocument/switchSourceHeader"
                    (hash "uri" (string-append "file://" (current-path)))
                    (lambda (result)
                             (helix.open (trim-start-matches result "file://")))))

3541 added a commit to 3541/dot that referenced this pull request Aug 26, 2024
This leverages helix-editor/helix#8675 to
implement a typed command :alt which switches between C++ source and
header files.
@godalming123
Copy link

Is it possible to modify the behavior of o/O specifically when in a markdown file with this PR? This would be useful if you want to create a new list item in markdown, and then the dash/number is auto-filled, or a new row in a table, and then all of the column separators are automatically positioned. Maybe with the table example, you could even set tab to move to the next table cell, and align the table columns, but still create indentation when outside a table. The only other way that I can think of to do this is an LSP protocol, but I don't know if one exists, or if so whether it's supported in helix?

@noor-tg
Copy link

noor-tg commented Sep 15, 2024

@godalming123 did you try marksman lsp ?

@godalming123
Copy link

@noor-tg right now I'm using markdown-oxide, does the feature described work with marksman?

@noor-tg
Copy link

noor-tg commented Sep 15, 2024

@godalming123 I don't know.
I think you will need to make changes to marksman . Or make other lsp .

@atahrijouti
Copy link
Contributor

atahrijouti commented Sep 16, 2024

Hi @mattwparas, just a quick reminder not to stress about this PR. Not sure how you're handling the attention and millions of people worldwide at the edge of their seats, so I am going on a limb here and just remind you to take your time while we're enjoying the show.
Tbh, the notifications I've been receiving on this PR feel like I am watching a TV show in progress, and you finishing the work would just spoil the fun of seeing people discover your plugin system.
So please, help us both out here, you take it easy and don't stress and take your time so I keep enjoying the comments on the PR ❤

@amano-kenji
Copy link

People shouldn't pressure him into finishing this as soon as possible, but one misconception that slows people down is the notion of relaxing.

If you are going to do anything, try 100%, or don't do it. Taking it easy won't get things done. Positive stress is good.

@ddogfoodd
Copy link

@amano-kenji this ain't LinkedIn

@MatrixManAtYrService
Copy link

MatrixManAtYrService commented Sep 21, 2024

I get the impression that big picture feedback is the helpful thing at this stage, but in order to come up with that feedback I feel like I need to overcome many tiny problems and use it for a while.

The tiny problem that brought me here:

When I open helix from the plugin fork I see the error below. Probably it's an issue with my steel code, but how to know which file? Does it dump a more verbose log somewhere? Maybe it should?

Error: ArityMismatch: require-builtin malformed - follows the pattern (require-builtin "<module>") or (require-builtin "<module>" as <prefix>

I've defined my environment here, run nix build, nix develop, and hxs (helix w/ steel) to see the error.

Is there a better place (besides this comments section) for discussing such things?

@habruening
Copy link

I am not so convinced of this proposal. I share my thoughts, so you can think if this is helpful. I don't claim to be right.

Scheme is a functional language. If you decide for it, you should embrace the functional style. And I think, Funcional Programming is indeed a good idea for Helix. I try to illustrate what I mean.

In your proposal you pass around the cx. That looks to me a lot like Object Oriented Programming. Compare it for example with Python, where you have to pass around the self object. The fact that you only stir in cx does not make the design stateless. I have nothing against this style. This is how Emacs works. Plugins in Emacs operate on everything. That makes everything quite easy, but gets more complicated, when things need more coordination at runtime or at interface level. Emacs for example has no multi threading. At least it does not profit from it.

For that way of working, I think Janet (https://janet-lang.org/) is a bit less functional and has more batteries included than Scheme and is perhaps a bit more mainstream oriented. It works and many people are happy with it. So I think, there is nothing against it.

But I want to explain, why a proper functional approach in my opinion still fits better.

In your example "Create a file under wherever we are" the plugin code is in full control of everything. It talks directly with the Helix core, requests data, and updates data. But imagine what happens if something there changes. For example you decide to introduce a graphical user interface and now the current mode is no longer FILE-TREE, but maybe NICE-UI-FILE-TREE or your selection is no longer a line number, because you have a GUI with real dialog buttons for that. That is hard to imagine. Why would someone want to develop an alternative UI for the FILE-TREE? But why not? How can you enable him to make his code compatible with your plugin?

In Java you have to define tons of classes and interfaces. But in dynamic languages you don't have to. You can pass functions around or register functions somewhere. Only the name and the signature matter. So when developing an API, I think, you should not just expose all these Rust functions like currently-in-labelled-buffer? or temporarily-switch-focus. That is not an API. That is only another part of your program, which is compiled differently. You should make things abstract and give them abstract names. But luckily it just happens when you do Functional Programming. You don't have to plan thousand aspects of you API upfront. Just start naming things in abstract ways and register them somewhere.

The other point, that I see critical is that the plugin is in charge of everything. In Functional Programming you normally try to have the side effects at the boundaries. In Object Oriented Programming you may have heard of "Hexagonal Architecture" or "Ports and Adapters". That means, that you have one business logic that is in control of everything at a very abstract level. In your plugin, this would mean, you have one function dirname-prompter that knows how to ask for a directory. But it does not actually executes this. It returns a function to the business logic, and the business logic can call it, when it wants. And then you have another function dir-creator that knows how to create a directory. So the business logic is: Asking for a directory and creating it. That is what the business logic has to implement. And that is a very abstract connection between two abstract things. It can also come from a plugin. So the control flow is: 1. The business logic determines what dirname-prompter needs. For example it must know how to use helix-prompt!. So the business logic creates a function around helix-prompt! and passes that function to dirname-prompter. Then dirname-prompter executes everything and delivers back something. The business logic does not even understand, what it gets. But it passes it to dir-creator together with everything else, that is needed by it.

Why is this better? Because you automatically establish interfaces for everything without thinking about them. In Java (and maybe in Rust?) such an architecture is hard to achieve. You have to read thick books to understand how to do it. In Functional Progamming you automatically work like this. Functional Programming automatically brings you on that track. You don't have to train people to do it the right way. There is no choice if you really enforce side effects at the boundaries.

The result is that it is very easy to replace one functionality by another functionality without breaking hundreds of plugins. If the NICE-UI-FILE-TREE has a function tell-me-a-new-dirname it will work, otherwise not. There are no surprises. But of course it only would work if the business logic provides it with the functionalities it needs. For read only file system it may not be possible. And for remote SSH connections it may not known how to create folders, because the plugin developer of the SSH connection did not provide the low level functions for creating folders. So the business logic would know that in these situations the plugin cannot be used.

It sounds complicated. But it is not. You only have to follow the idea that everything, which is side-effecty happens outside. You already have the lambdas. Return them instead of executing them.

And a final though, why I think, Functional Programming fits better: You want Helix be resonsive at any time. Imagine you have 1000 cursors in a file. You write a plugin that does something for each of the cursors. For example fetching something from a database. For each location it takes 0.1s. Helix would be blocked for 100s. That is not acceptable. How can you do it better? We should not let the plugin in the control of this. We should only ask the plugin how to do things. The plugin delivers 1000 functions to the business logic. And the business logic decides when and how to execute them. If for example none of the 1000 working areas overlap, all 1000 functions can be executed in parallel. We can also give them a priority. For example if the user scrolls to a place, where any of the 1000 functions is pending, it can quickly be executed, so that the user can continue working. Or parts of the file that have pending changes can be made read only. You can provide a progress bar. You could even allow speculative editing. You cannot expect all this from a plugin developer. This must come from the business logic. And you only have to think once about these mechanisms. Then they are not complicated. What is the abstract model of this? It is quite simple: You have 1000 selections in the file and 1000 functions that do some local text manipulations on these selections. For plugin developers that would mean, they have to decide whether they do local modifications and follow that model or not. If they do non-local modifications, they will block Helix.

@cychen2021
Copy link

cychen2021 commented Sep 22, 2024 via email

@kirawi
Copy link
Member

kirawi commented Sep 22, 2024

I don't think the maintainers want to support other scripting languages, but from what I understand it is designed with the possibility in mind in case we choose an inhouse scheme dialect.

@mattwparas
Copy link
Author

I get the impression that big picture feedback is the helpful thing at this stage, but in order to come up with that feedback I feel like I need to overcome many tiny problems and use it for a while.

The tiny problem that brought me here:

When I open helix from the plugin fork I see the error below. Probably it's an issue with my steel code, but how to know which file? Does it dump a more verbose log somewhere? Maybe it should?

Error: ArityMismatch: require-builtin malformed - follows the pattern (require-builtin "<module>") or (require-builtin "<module>" as <prefix>

I've defined my environment here, run nix build, nix develop, and hxs (helix w/ steel) to see the error.

Is there a better place (besides this comments section) for discussing such things?

It might be better to just open an issue on my fork, that way I can help go through things without notifying everyone else.

@habruening - I appreciate the commentary, however there are a few points that I'd like to call out.

  • The passing of cx around in the original post is no longer how it works, the cx is implicit. However, I would argue passing the context explicitly is more functional, since you have to have a way to interact with the state of the editor. Having it implicit is just far more convenient from the plugin writing perspective.
  • I'm not quite following the criticism of the APIs designed for the file tree. It was bespoke code written by me, for my own personal consumption, and was by no means branded as a building block for all future file tree implementations. I don't think this is a fair criticism and would appreciate we try to keep this PR discussion more relevant to the Rust implementation I've provided here.
  • Steel both has support for native threads and futures - I use the futures liberally in some of my current plugins. This allows for non blocking interactions with the rest of the editor - it is very easy to yield control back to the editor where relevant, even if not waiting on a future.

For all of your ideas, I welcome an implementation. I don't think this is the direction that I will be taking this PR.

@atahrijouti I appreciate the kind words. I am still working on this, just less publicly for the last few months.

@MatrixManAtYrService
Copy link

It might be better to just open an issue on my fork, that way I can help go through things without notifying everyone else.

I also came prepared to bleed, so I'm not really after support so much as a public place to collect workarounds and document the sharp edges, even if fixing them ends up being low/no priority. Issues will do nicely. Can I trouble you to enable issues in that repo? 😀

@amano-kenji
Copy link

@habruening The best UI paradigm I know is reactive values pioneered by https://github.com/keera-studios/keera-hails

Reactive values are like spreadsheet cells that react to the values of other cells. The reactive values can be unidirectional or bidirectional.

Functional reactive programming is best used for animation.

Since helix editor is a user interface, reactive values are useful.

@Talia-12
Copy link

I'd love to start playing around with this, but currently the flake.nix hasn't been updated to add the required files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state. S-waiting-on-pr Status: This is waiting on another PR to be merged first
Projects
None yet
Development

Successfully merging this pull request may close these issues.