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

correctly handle completion rerequest #6594

Merged
merged 6 commits into from May 18, 2023

Conversation

pascalkuthe
Copy link
Member

@pascalkuthe pascalkuthe commented Apr 4, 2023

Alternative to #6511
Closes #6259
Closes #6261
Fixes a similar bug with the typescript LSP reported on matrix where imports would be duplicated.

This PR resolved the same issue as #6511 but keeps the current behavior of previewing the current completion. This is achieved by generating "ghost" transactions. Ghost transactions are normal transactions that are never sent to the LS.

Maintaining such transactions is tricky/dangerous because we always need to undo them (with the undo transaction also not sent to the LS) before we apply any other transaction to the document. Otherwise, we would send invalid changes to LS with incremental sync and therefore break the server (until :lsp-restart).

To that end I used the more generic savepoints introduced in #6173. The completion menu now saves a second savepoint before a completion option is first selected with tab/s-tab. The completion preview (so what you see when hitting tab but before pressing <ret>) is now such a ghost transaction. To ensure these transactions are always removed there have been some behavioral changes:

  • both c-c and esc now reset the document to the savepoint created before previewing a transaction (or do nothing otherwise)
  • As soon as a completion is selected with "tab" (previewed) it becomes "active" as soon as any key not captured by the completion menu is pressed the completion is fully applied (same as pressing enter) automatically. So there are no "half applied" completions anymore

This behavior matches Kakoune (which also automatically applies the transaction when hitting enter as discovered by @the-mikedavis).

This approach is safe regarding the hazards mentioned above because as soon as a ghost transaction is applied it will always be realized when pressing any key (that isn't consumed by the completion menu). The only remaining hazard are "async" edits (or savepoints like completions request) that are created without an explicit key press. These might pickup the ghost document state. I think there aren't really any of those (besides completions themselves where I habe taken great care to cover all edge-cases) because any async transaction would require a savepoint (otherwise the transaction would refer to a potentially outdated document state) and the only use of savepoint is completions/completion replay (which always require a keypress). If we ever want to add other async transaction we might think about generalizing the system a bit but that will require some more complex design. For now I simply expanded the existing Editor::last_complete field to cover the usecase here.

The type script import problem was actually a special case of the more general problem fixed by this PR. It was caused by resolving completions after they were already applied. The general problem was the same (we send didChange and then rerequested a completion) but fixing it furthermore required moving the request in a3a4645. It seems other clients are running into this issue too (for example nvim) so getting this right is difficult but vscode seems to handle this correctly (also using some ghost text it seems). See typescript-language-server/typescript-language-server#699 (the examples there also cause bugs on master but work with this PR or #6511).

#6261 was caused by us resolving a completion multiple times. The LSP standard doesn't explicitly state it but the intention seems to be that completion items are only resolved once (since resolving is intended to delay expensive calculations). Helix now separately tracks whether a completion item has been resolved and therefore mirrors vscode in that regard. I think that this was really always the intention of the helix code (continuously querying the server is quite inefficient) and the check we did just wasn't quite correct

@pascalkuthe pascalkuthe added A-language-server Area: Language server client A-helix-term Area: Helix term improvements E-medium Call for participation: Experience needed to fix: Medium / intermediate S-waiting-on-review Status: Awaiting review from a maintainer. labels Apr 4, 2023
@archseer archseer added this to the 23.03.1 milestone Apr 6, 2023
Copy link
Member

@the-mikedavis the-mikedavis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks and feels great 👍

I only have some minor comments on the comments

helix-term/src/ui/completion.rs Show resolved Hide resolved
helix-term/src/ui/completion.rs Outdated Show resolved Hide resolved
helix-view/src/document.rs Outdated Show resolved Hide resolved
helix-term/src/ui/editor.rs Show resolved Hide resolved
helix-view/src/editor.rs Outdated Show resolved Hide resolved
@pascalkuthe pascalkuthe force-pushed the robust_completions branch 3 times, most recently from b413364 to a80c4ff Compare April 7, 2023 14:17
pascalkuthe and others added 6 commits April 7, 2023 17:16
When re requesting a completion that already has a selected item we
reuse that selections savepoint. However, the selection has likely
changed since that savepoint which requires us to use the selection
from that savepoint
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
poliorcetics added a commit to poliorcetics/helix that referenced this pull request Apr 15, 2023
poliorcetics added a commit to poliorcetics/helix that referenced this pull request Apr 18, 2023
Copy link
Contributor

@Philipp-M Philipp-M left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... esc now reset the document to the savepoint created before previewing a transaction

Doesn't do that for me, esc applies the current state (selected completion) and exits to normal mode for me, but I think this behavior is fine IMHO, C-c works as described though (which IMHO also makes sense). The PR description might not be up to date after having a second look, as this seems to be handled in the comments.

Took some time to wrap my head around this change.

I still think that showing the completion in the document should probably not be an actual edit of the document until the completion item is really applied in a logic sense at least, but this is nitpicking, and doesn't really have anything to do with this PR (as it's existing behavior)...

For now I think this is a good approach to the issue(s) at hand, though it would be nice at some point in the future to untangle complexity a little bit (editor <-> completion menu <-> current doc state)

@pascalkuthe
Copy link
Member Author

pascalkuthe commented Apr 22, 2023

I still think that showing the completion in the document should probably not be an actual edit of the document until the completion item is really applied in a logic sense at least

This would definitely be simpler but I think this preview behavior is quite useful and many people prefer it to the VSCode/Intelij approach. I don't think the virtual text is really a good fit here (it would need to be concealed which is not implemented yet and conceal hides the cursor) and would be very complex to implement.

For now I think this is a good approach to the issue(s) at hand, though it would be nice at some point in the future to untangle complexity a little bit (editor <-> completion menu <-> current doc state)

I am not sure that's really possible. I can't think of a way at least. And if we ever have any other operation that "previews" changes like this then this general system is needed so these are really three separate systems: Create document snapshots you can cheaply rollback to (already exists, arbitrarily many possible), create a snapshot after which edits are not sent to the LS (must be stored on the editor since there can be only one of those) and the completion menu then just strings these general concepts together.

If you change the document using potentially asynchronously computed edits that are purposefully unsynchronized that's fundamentally just very complex to handle.

@Philipp-M
Copy link
Contributor

This would definitely be simpler but I think this preview behavior is quite useful and many people prefer it to the VSCode/Intelij approach. I don't think the virtual text is really a good fit here (it would need to be concealed which is not implemented yet and conceal hides the cursor) and would be very complex to implement.

Yeah we should definitely keep that behavior, thanks for the explanation regarding complexity with a potential virtual text implementation, with snippets and jumping of the cursor etc. this would certainly make things more difficult, I can imagine.

... If you change the document using potentially asynchronously computed edits that are purposefully unsynchronized that's fundamentally just very complex to handle.

Yeah it's definitely not trivial and may require a bigger refactor. I was thinking a little about having some kind of branching and merging behavior for all kinds of edits and likely the whole history(-tree) (maybe by using CRDTs, for generally easier async handling of edits/transactions). So it would be something like this:

  • completion -> snapshot/fork of current state, the document and the language-server use this snapshot.
  • when selecting completions the editor forks temporary branches from this snapshot with the application of the selected completion item, and when actually applied the "actual" state of the editor is updated to this temporary branch
  • this temporary branch is used for rendering the document in the meantime (however this would be implemented)

It's already kind of going this way but I think it may be easier to comprehend, when this logic is abstracted away at a lower level (I may be wrong though).

I think this behavior could be beneficial for different kinds of applications as well (e.g. previewing code completions before applying them).

@poliorcetics
Copy link
Contributor

I have been testing this branch for a few weeks now and have encountered the following problem:

When editing Rust code, at some point there is an edit in Helix that is not synchronized with Rust-Analyzer, which is then crashes in two different ways (I haven't taken the time to reliably reproduce them yet):

Way 1, ServerError(-32801)
2023-04-20T12:01:07.545 helix_view::editor [ERROR] editor error: Async job failed: protocol error: ServerError(-32801): content modified
2023-04-20T12:01:22.428 helix_lsp::transport [ERROR] <- ServerError(-32801): content modified
2023-04-20T12:01:22.429 helix_view::editor [ERROR] editor error: Async job failed: protocol error: ServerError(-32801): content modified
2023-04-20T12:01:22.502 helix_lsp::transport [ERROR] Tried sending response into a closed channel (id=Num(715)), original request likely timed out
2023-04-20T12:01:25.979 helix_lsp::transport [ERROR] Tried sending response into a closed channel (id=Num(728)), original request likely timed out
2023-04-20T12:01:53.574 helix_lsp::transport [ERROR] Tried sending response into a closed channel (id=Num(751)), original request likely timed out
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "Panic context:\n"
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "> \n"
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "version: 0.0.0 (2400b36a2 2023-04-19)\n"
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "notification: textDocument/didChange\n"
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "\n"
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "thread 'LspServer' panicked at 'assertion failed: self.is_char_boundary(n)', /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/alloc/src/string.rs:1819:29\n"
2023-04-20T12:04:23.038 helix_lsp::transport [ERROR] err <- "stack backtrace:\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "   0: rust_begin_unwind\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:575:5\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "   1: core::panicking::panic_fmt\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:64:14\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "   2: core::panicking::panic\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:114:5\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "   3: alloc::string::String::replace_range\n"
2023-04-20T12:04:23.064 helix_lsp::transport [ERROR] err <- "   4: rust_analyzer::lsp_utils::apply_document_changes\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   5: core::ops::function::FnOnce::call_once\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   6: rust_analyzer::dispatch::NotificationDispatcher::on\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   7: rust_analyzer::main_loop::::run\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   8: rust_analyzer::main_loop::main_loop\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   9: rust_analyzer::run_server\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "thread '' panicked at 'called `Result::unwrap()` on an `Err` value: \"SendError(..)\"', lib/lsp-server/src/stdio.rs:26:37\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "stack backtrace:\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   0: rust_begin_unwind\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:575:5\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   1: core::panicking::panic_fmt\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:64:14\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "   2: core::result::unwrap_failed\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/result.rs:1790:5\n"
2023-04-20T12:04:23.065 helix_lsp::transport [ERROR] err <- "note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n"
2023-04-20T12:04:23.907 helix_lsp::transport [ERROR] err: <- StreamClosed
2023-04-20T12:04:23.907 helix_view::editor [ERROR] editor error: Async job failed: server closed the stream
2023-04-20T12:04:23.909 helix_view::editor [ERROR] editor error: Async job failed: server closed the stream
2023-04-20T12:04:23.910 helix_view::editor [ERROR] editor error: Async job failed: server closed the stream
2023-04-20T12:04:24.081 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
Way 2, Crash on a windows-looking path, happens while cross compiling for windows from macOS
2023-04-20T17:35:33.215 helix_view::editor [ERROR] editor error: Async job failed: protocol error: ServerError(-32801): content modified
2023-04-20T17:37:00.187 helix_lsp::transport [ERROR] <- ServerError(-32801): content modified
2023-04-20T17:37:00.191 helix_view::editor [ERROR] editor error: Async job failed: protocol error: ServerError(-32801): content modified
2023-04-20T17:37:02.259 helix_lsp::transport [ERROR] <- ServerError(-32801): content modified
2023-04-20T17:37:02.261 helix_view::editor [ERROR] editor error: Async job failed: protocol error: ServerError(-32801): content modified
2023-04-20T17:37:16.224 helix_lsp::transport [ERROR] Tried sending response into a closed channel (id=Num(580)), original request likely timed out
2023-04-20T17:37:44.396 helix_lsp::transport [ERROR] err <- "[ERROR rust_analyzer::main_loop] flycheck 0: File with cargo diagnostic not found in VFS: file not found: /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74\\library\\core\\src\\result.rs\n"
2023-04-20T17:37:44.397 helix_lsp::transport [ERROR] err <- "[ERROR rust_analyzer::main_loop] flycheck 0: File with cargo diagnostic not found in VFS: file not found: /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74\\library\\core\\src\\result.rs\n"
2023-04-20T17:38:03.396 helix_lsp::transport [ERROR] Tried sending response into a closed channel (id=Num(693)), original request likely timed out
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "Panic context:\n"
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "> \n"
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "version: 0.0.0 (2400b36a2 2023-04-19)\n"
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "notification: textDocument/didChange\n"
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "\n"
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "thread 'LspServer' panicked at 'assertion failed: self.is_char_boundary(n)', /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/alloc/src/string.rs:1819:29\n"
2023-04-20T17:38:14.461 helix_lsp::transport [ERROR] err <- "stack backtrace:\n"
2023-04-20T17:38:14.514 helix_lsp::transport [ERROR] err <- "   0: rust_begin_unwind\n"
2023-04-20T17:38:14.514 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:575:5\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   1: core::panicking::panic_fmt\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:64:14\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   2: core::panicking::panic\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:114:5\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   3: alloc::string::String::replace_range\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   4: rust_analyzer::lsp_utils::apply_document_changes\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   5: core::ops::function::FnOnce::call_once\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   6: rust_analyzer::dispatch::NotificationDispatcher::on\n"
2023-04-20T17:38:14.515 helix_lsp::transport [ERROR] err <- "   7: rust_analyzer::main_loop::::run\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "   8: rust_analyzer::main_loop::main_loop\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "   9: rust_analyzer::run_server\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "thread '' panicked at 'called `Result::unwrap()` on an `Err` value: \"SendError(..)\"', lib/lsp-server/src/stdio.rs:26:37\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "stack backtrace:\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "   0: rust_begin_unwind\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/std/src/panicking.rs:575:5\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "   1: core::panicking::panic_fmt\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/panicking.rs:64:14\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "   2: core::result::unwrap_failed\n"
2023-04-20T17:38:14.516 helix_lsp::transport [ERROR] err <- "             at /rustc/9eb3afe9ebe9c7d2b84b71002d44f4a0edac95e0/library/core/src/result.rs:1790:5\n"
2023-04-20T17:38:14.517 helix_lsp::transport [ERROR] err <- "note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.\n"
2023-04-20T17:38:15.553 helix_lsp::transport [ERROR] err: <- StreamClosed
2023-04-20T17:38:15.553 helix_view::editor [ERROR] editor error: Async job failed: server closed the stream
2023-04-20T17:38:15.555 helix_view::editor [ERROR] editor error: Async job failed: server closed the stream
2023-04-20T17:38:15.556 helix_view::editor [ERROR] editor error: Async job failed: server closed the stream
2023-04-20T17:38:15.696 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:15.938 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:16.187 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:19.350 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:19.642 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:20.363 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:20.606 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:21.419 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:22.670 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:22.670 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:22.670 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:22.670 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })
2023-04-20T17:38:22.671 helix_lsp::transport [ERROR] err: <- IO(Os { code: 32, kind: BrokenPipe, message: "Broken pipe" })

@pascalkuthe
Copy link
Member Author

I have been testing this branch for a few weeks now and have encountered the following problem:

When editing Rust code, at some point there is an edit in Helix that is not synchronized with Rust-Analyzer, which is then crashes in two different ways (I haven't taken the time to reliably reproduce them yet):
Way 1, ServerError(-32801)
Way 2, Crash on a windows-looking path, happens while cross compiling for windows from macOS

We actually had multiple bug reports of the exact same bug report from people on matrix that were just using the latest helix release so I don't think that bug is actually related to this PR at all

@pascalkuthe
Copy link
Member Author

pascalkuthe commented Apr 28, 2023

This would definitely be simpler but I think this preview behavior is quite useful and many people prefer it to the VSCode/Intelij approach. I don't think the virtual text is really a good fit here (it would need to be concealed which is not implemented yet and conceal hides the cursor) and would be very complex to implement.

Yeah we should definitely keep that behavior, thanks for the explanation regarding complexity with a potential virtual text implementation, with snippets and jumping of the cursor etc. this would certainly make things more difficult, I can imagine.

... If you change the document using potentially asynchronously computed edits that are purposefully unsynchronized that's fundamentally just very complex to handle.

Yeah it's definitely not trivial and may require a bigger refactor. I was thinking a little about having some kind of branching and merging behavior for all kinds of edits and likely the whole history(-tree) (maybe by using CRDTs, for generally easier async handling of edits/transactions). So it would be something like this:

* completion -> snapshot/fork of current state, the document and the language-server use this snapshot.

* when selecting completions the editor forks temporary branches from this snapshot with the application of the selected completion item, and when actually applied the "actual" state of the editor is updated to this temporary branch

* this temporary branch is used for rendering the document in the meantime (however this would be implemented)

It's already kind of going this way but I think it may be easier to comprehend, when this logic is abstracted away at a lower level (I may be wrong though).

I think this behavior could be beneficial for different kinds of applications as well (e.g. previewing code completions before applying them).

Creating a lower abstraction that we currently have is very difficult. Helix is fundamentally build around a CRDT like transactions. Every change to a document must be a transaction. This even includes any temporary changes. The entire editor is fundamentally built around that model.

For example when a transaction is applied to a buffer:

  • the diff gutter updates
  • diagnostics are moved around
  • tree sitter parsing rungs
  • changes are sent to the LSP

This "transactional" view of the world is very core so any snapshots we may have must also be updated using this transactional model. The savepoint system is exactly this: A snapshot of a document that is internally represented as an automatically updated CRDT transaction that can be used to revert the buffer to a previous snapshot.

I guess what you are getting at is that there should be somekind of "LSP snapshot" which is automatically used. However, this doesn't really work. Basically all LSP actions require interactiing with the buffer. This can only work if the server and client buffer matches. Completions are very much special here because these "LSP snapshots" are created by the completion menu so for completion rerequest its ok to revert this LSP snapshot. Everything else should just fail/revert the completion snapshot.

Or in other words: the low level snapshot system already exists: It's the savepoint system. The only difference is that completions are very special and require special interaction with its own savepoints.

@pascalkuthe
Copy link
Member Author

@poliorcetics that crash is an existing issue see #6921 that is unrelated to this pr

@Philipp-M
Copy link
Contributor

Thanks for the explanation, though I'm aware of most of that.

What I meant with "CRDTs may be helpful" is the commutative property of transactions (which is pretty much all what CRDTs are about), so that no matter when the transaction occurred it all leads to the same (expected) result. This is not (yet) implemented (and not a trivial task, especially with multi line/file transactions), but would be quite helpful for various different domains (including this).

Anyway all of this is orthogonal to this PR and would require quite a few changes in the core.

Is there anything blocking this PR? AFAIK it's the only missing thing for the (bugfix-)release isn't it?

@pascalkuthe
Copy link
Member Author

Thanks for the explanation, though I'm aware of most of that.

What I meant with "CRDTs may be helpful" is the commutative property of transactions (which is pretty much all what CRDTs are about), so that no matter when the transaction occurred it all leads to the same (expected) result. This is not (yet) implemented (and not a trivial task, especially with multi line/file transactions), but would be quite helpful for various different domains (including this).

Anyway all of this is orthogonal to this PR and would require quite a few changes in the core.

Transaction::map is not yet implemented, it seems attractive and I thaught about implementing it before but its not actually a good fit for completions. The important realization is that the text applied by the user must always be removed before a completion is applied even with a CRDT. The text by the user is just used to filter the completion but its not part of the transactions send by the server. So for example if you complete at foo. type barand selection thefoobarcompletion and used a CRDT to map the completion transaction you would endup withfoo.barfoobar`. So the snapshot based system is not a weak emulation of CRDT but is actually required to make completions work.

I think this PR is ready to merge just waiting until @archseer has time to review it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-helix-term Area: Helix term improvements A-language-server Area: Language server client E-medium Call for participation: Experience needed to fix: Medium / intermediate S-waiting-on-review Status: Awaiting review from a maintainer.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Svelte duplicated auto-imports on tab Snippets insert literal text
6 participants