Skip to content

Conversation

@Moosieus
Copy link
Collaborator

@Moosieus Moosieus commented Dec 16, 2025

This pull request overhauls the way progress reporting is handled in Expert. The top-level API remains largely the same for callers.

The prior architecture started a per-project stateful GenServer that handled progress tracking logic internally:

  • It was difficult to reason about due to the depth and statefulness of the call stack.
    • Or at least, I found it difficult. YMMV, but my brain works better with shallow call stacks.
  • The Progress GenServers were initiated per-project, even though workDoneTokens aren’t scoped as such in LSP.
  • Had no means to handle client-initiated workDoneTokens
    • I had a with_client_progress function incorporated here, but I removed it for the sake of review. There’s already a lot here.
  • Had no means to deallocate workDoneTokens should they be cancel-able.
    • Requests for long-running tasks can be canceled by the client, in which case it cancels the request by ID, but not any workDoneTokens associated.
      • Hence why the work-done-tokens now live in the caller’s context.

The new progress reporting modules are organized as follows:

  • Forge.Progress - a behaviour that defines the with_progress and with_tracked_progress helper functions, permitting the implementing module provides the following functions:
    • begin/2
    • report/2
    • complete/2
  • Expert.Progress - a process-less module that directly handles progress reporting, implements the aforementioned
  • Engine.Progress - a thin wrapper that calls Expert.Progress via erpc.
  • Forge.Progress.Tracker - An ephemeral GenServer for reporting progress between concurrently running tasks.
    • Supports straight percentage reporting by default, or custom callbacks like turbo-encabulating files: 3/25 should that be preferable.

All in all: Less processes, more sequential Elixir, more flexibility, better alignment with LSP semantics.

Some additional changes:

  • Make tests that wait the project_compiled message less flaky by increasing the timeout from 100ms -> 5s.
  • Debounce namespacing logs
  • When the engine starts, store its manager name in a persistent term and use that, instead of building it.

* Overhaul `Expert.Project.Progress` with a new progress reporting API.
  * Progress reporting only informs users of long-running work and doesn’t drive behavior. New interface accordingly KISS.
* Removed releveant `__using__` macros in favor of simple `alias` + function calls.
* Use plain tuples instead of `defrecord` for progress report messages between the server node and engine node.
* Debounce namespace build logs to minimize spam.
* Store the manager node name on startup in the engine node. This fixes up some tests that were faltering due to the introduced rpc calls. This is more stable anyway, imo.
* give project compilation more generous timeouts in testing (5s instead of 100ms)
@Moosieus Moosieus changed the title Moo/work done progress Make progress tracking better follow LSP semantics Dec 16, 2025
@Moosieus Moosieus closed this Dec 16, 2025
@Moosieus Moosieus reopened this Dec 16, 2025
@Moosieus Moosieus changed the title Make progress tracking better follow LSP semantics refactor: Make progress tracking better follow LSP semantics Dec 16, 2025
@Moosieus Moosieus changed the title refactor: Make progress tracking better follow LSP semantics chore: Make progress tracking better follow LSP semantics Dec 16, 2025
@Moosieus Moosieus changed the title chore: Make progress tracking better follow LSP semantics chore: make progress tracking better follow LSP semantics Dec 16, 2025
@Moosieus Moosieus marked this pull request as ready for review December 16, 2025 04:21
@mhanberg mhanberg changed the title chore: make progress tracking better follow LSP semantics refactor: make progress tracking better follow LSP semantics Dec 16, 2025
{:ok, response} <- Expert.Protocol.Convert.to_lsp(response) do
Task.Supervisor.start_child(:expert_task_queue, fn ->
# dirty sleep to allow initialize response to return before progress reports
Process.sleep(50)
Copy link
Contributor

@badosu badosu Dec 16, 2025

Choose a reason for hiding this comment

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

Hmm.. Perhaps it would be an interesting addition for gen_lsp to support something like:

{:reply, reply, {:continue, term, state}} |
{:noreply, {:continue, term, state}}

Then support a handle_continue callback?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Matching GenServer semantics? Yeah, that'd be sweet.

The key to resolving the timing issue here is to make sure the language-client gets the initialize response, before the language-server initializes anymore workDoneTokens.

Copy link
Contributor

@badosu badosu Dec 16, 2025

Choose a reason for hiding this comment

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

Yeah, that would be the idea. So that we can support a situation like this where we want to perform work on a continuation after the current work is done, with some guarantee the reply was sent - although there might be other valid reasons to want to do a continuation I guess. E.g.:

def handle_request(req, lsp) do
  reply = get_reply(req)
  {:reply, reply, {:continue, :initialize_engine, lsp}}
end

def handle_continue(:initialize_engine, lsp) do
  # maybe something related but we want to offload that particular loop iteration
  # maybe something unrelated and we want to separate scope
  # maybe something that should run in some continuous "work" manner
  # maybe we wanted some guarantee the reply was sent
  {:noreply, lsp}
end

Anyway, not an issue for this particular PR, I wondered if you'd tell me if this sounds like a reasonable "solution" for the sleep issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I took a crack at this: https://github.com/Moosieus/gen_lsp/tree/moo/handle-continue

Not tested in-anger against Expert at time of writing, though.

Copy link
Contributor

@badosu badosu Dec 17, 2025

Choose a reason for hiding this comment

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

Look's great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants