Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
83c901b
Support multiple root projects
doorgan May 26, 2025
3ba3c91
Use saner parent_path? implementation and add tests
doorgan May 28, 2025
63ceee1
Merge main
doorgan Jun 20, 2025
3582869
Start projects dynamically
doorgan Jun 20, 2025
c0c879a
Fix credo issue
doorgan Jun 21, 2025
74a24fb
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Jul 4, 2025
c0aa69b
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Jul 4, 2025
cf379a5
chore: use lsp workspace folder functionality
doorgan Jul 5, 2025
b4b54a8
fix: support the case where the root folder contains subprojects
doorgan Jul 5, 2025
5f32c6a
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Jul 9, 2025
90ab70a
chore: add tests
doorgan Jul 9, 2025
d6279f4
fix: use GenLSP logging utility
doorgan Jul 9, 2025
e25b98b
chore: remove leftover code
doorgan Jul 9, 2025
962b351
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Jul 10, 2025
3fa0ea4
chore: remove tests from new fixture projects
doorgan Jul 14, 2025
a4370d5
fix: prevent tests from randomly not running
doorgan Jul 15, 2025
88e884b
fix: fix flaky test
doorgan Jul 15, 2025
9aa0ca7
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Jul 29, 2025
3b1dfba
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Aug 21, 2025
51ebda9
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Aug 21, 2025
1cf3bd9
fix: fallback to heuristics if workspace_folders is not set
doorgan Aug 21, 2025
04968a1
fix: remove heuristics and just default to no workspace folders
doorgan Aug 21, 2025
7e342cb
fix: try to start projects in a task
doorgan Aug 21, 2025
a3201f2
chore: format
doorgan Aug 21, 2025
4c812a6
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Aug 21, 2025
1092e9f
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Aug 22, 2025
e32c101
Merge remote-tracking branch 'origin/main' into doorgan/multiroot_sup…
doorgan Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/expert/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,3 @@ if Code.ensure_loaded?(LoggerFileBackend) do
else
:ok
end

require Logger
20 changes: 12 additions & 8 deletions apps/expert/lib/expert.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ defmodule Expert do
GenLSP.Notifications.TextDocumentDidChange,
GenLSP.Notifications.WorkspaceDidChangeConfiguration,
GenLSP.Notifications.WorkspaceDidChangeWatchedFiles,
GenLSP.Notifications.WorkspaceDidChangeWorkspaceFolders,
GenLSP.Notifications.TextDocumentDidClose,
GenLSP.Notifications.TextDocumentDidOpen,
GenLSP.Notifications.TextDocumentDidSave,
GenLSP.Notifications.Exit,
GenLSP.Notifications.Initialized,
GenLSP.Requests.Shutdown
]

Expand Down Expand Up @@ -47,13 +47,6 @@ defmodule Expert do

case State.initialize(state, request) do
{:ok, response, state} ->
# TODO: this should be gated behind the dynamic registration in the initialization params
registrations = registrations()

if nil != GenLSP.request(lsp, registrations) do
Logger.error("Failed to register capability")
end

lsp = assign(lsp, state: state)
{:ok, response} = Forge.Protocol.Convert.to_lsp(response)

Expand Down Expand Up @@ -119,6 +112,17 @@ defmodule Expert do
end
end

def handle_notification(%GenLSP.Notifications.Initialized{}, lsp) do
Logger.info("Server initialized, registering capabilities")
registrations = registrations()

if nil != GenLSP.request(lsp, registrations) do
Logger.error("Failed to register capability")
end

{:noreply, lsp}
end

def handle_notification(%mod{} = notification, lsp) when mod in @server_specific_messages do
with {:ok, notification} <- Convert.to_native(notification),
{:ok, state} <- apply_to_state(assigns(lsp).state, notification) do
Expand Down
50 changes: 50 additions & 0 deletions apps/expert/lib/expert/active_projects.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule Expert.ActiveProjects do
@moduledoc """
A cache to keep track of active projects.

Since GenLSP events happen asynchronously, we use an ets table to keep track of
them and avoid race conditions when we try to update the list of active projects.
"""

use GenServer

def child_spec(_) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, []}
}
end

def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

def init(_) do
__MODULE__ = :ets.new(__MODULE__, [:set, :named_table, :public, read_concurrency: true])
{:ok, nil}
end

def projects do
__MODULE__
|> :ets.tab2list()
|> Enum.map(fn {_, project} -> project end)
end

def add_projects(new_projects) when is_list(new_projects) do
for new_project <- new_projects do
# We use `:ets.insert_new/2` to avoid overwriting the cached project's entropy
:ets.insert_new(__MODULE__, {new_project.root_uri, new_project})
end
end

def remove_projects(removed_projects) when is_list(removed_projects) do
for removed_project <- removed_projects do
:ets.delete(__MODULE__, removed_project.root_uri)
end
end

def set_projects(new_projects) when is_list(new_projects) do
:ets.delete_all_objects(__MODULE__)
add_projects(new_projects)
end
end
1 change: 1 addition & 0 deletions apps/expert/lib/expert/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Expert.Application do
{GenLSP.Assigns, [name: Expert.Assigns]},
{Task.Supervisor, name: :expert_task_queue},
{GenLSP.Buffer, name: Expert.Buffer},
{Expert.ActiveProjects, []},
{Expert,
buffer: Expert.Buffer,
task_supervisor: :expert_task_queue,
Expand Down
12 changes: 4 additions & 8 deletions apps/expert/lib/expert/configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@ defmodule Expert.Configuration do

alias Expert.Configuration.Support
alias Expert.Dialyzer
alias Forge.Project
alias Forge.Protocol.Id
alias GenLSP.Notifications.WorkspaceDidChangeConfiguration
alias GenLSP.Requests
alias GenLSP.Structures

defstruct project: nil,
support: nil,
defstruct support: nil,
client_name: nil,
additional_watched_extensions: nil,
dialyzer_enabled?: false

@type t :: %__MODULE__{
project: Project.t() | nil,
support: support | nil,
client_name: String.t() | nil,
additional_watched_extensions: [String.t()] | nil,
Expand All @@ -29,12 +26,11 @@ defmodule Expert.Configuration do

@dialyzer {:nowarn_function, set_dialyzer_enabled: 2}

@spec new(Forge.uri(), map(), String.t() | nil) :: t
def new(root_uri, %Structures.ClientCapabilities{} = client_capabilities, client_name) do
@spec new(map(), String.t() | nil) :: t
def new(%Structures.ClientCapabilities{} = client_capabilities, client_name) do
support = Support.new(client_capabilities)
project = Project.new(root_uri)

%__MODULE__{support: support, project: project, client_name: client_name}
%__MODULE__{support: support, client_name: client_name}
|> tap(&set/1)
end

Expand Down
8 changes: 6 additions & 2 deletions apps/expert/lib/expert/engine_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ defmodule Expert.EngineSupervisor do
end

def start_link(%Project{} = project) do
DynamicSupervisor.start_link(__MODULE__, project, name: __MODULE__, strategy: :one_for_one)
DynamicSupervisor.start_link(__MODULE__, project, name: name(project), strategy: :one_for_one)
end

defp name(%Project{} = project) do
:"#{Project.name(project)}::project_node_supervisor"
end

def start_project_node(%Project{} = project) do
DynamicSupervisor.start_child(__MODULE__, EngineNode.child_spec(project))
DynamicSupervisor.start_child(name(project), EngineNode.child_spec(project))
end

@impl true
Expand Down
10 changes: 0 additions & 10 deletions apps/expert/lib/expert/project/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,6 @@ defmodule Expert.Project.Supervisor do
alias Expert.Project.SearchListener
alias Forge.Project

# TODO: this module is slightly weird
# it is a module based supervisor, but has lots of dynamic supervisor functions
# what I learned is that in Expert.Application, it is starting an ad hoc
# dynamic supervisor, calling a function from this module
# Later, when the server is initializing, it calls the start function in
# this module, which starts a normal supervisor, which the start_link and
# init callbacks will be called
# my suggestion is to separate the dynamic supervisor functionalities from
# this module into its own module

use Supervisor

def start_link(%Project{} = project) do
Expand Down
8 changes: 6 additions & 2 deletions apps/expert/lib/expert/provider/handlers/code_action.ex
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
defmodule Expert.Provider.Handlers.CodeAction do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.CodeAction
alias Forge.Project
alias GenLSP.Requests
alias GenLSP.Structures

def handle(
%Requests.TextDocumentCodeAction{params: %Structures.CodeActionParams{} = params},
%Configuration{} = config
%Configuration{}
) do
document = Forge.Document.Container.context_document(params, nil)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)
diagnostics = Enum.map(params.context.diagnostics, &to_code_action_diagnostic/1)

code_actions =
EngineApi.code_actions(
config.project,
project,
document,
params.range,
diagnostics,
Expand Down
9 changes: 7 additions & 2 deletions apps/expert/lib/expert/provider/handlers/code_lens.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.CodeLens do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Expert.Provider.Handlers
Expand All @@ -14,12 +15,16 @@ defmodule Expert.Provider.Handlers.CodeLens do

def handle(
%Requests.TextDocumentCodeLens{params: %Structures.CodeLensParams{} = params},
%Configuration{} = config
%Configuration{}
) do
document = Document.Container.context_document(params, nil)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

document = Document.Container.context_document(params, nil)

lenses =
case reindex_lens(config.project, document) do
case reindex_lens(project, document) do
nil -> []
lens -> List.wrap(lens)
end
Expand Down
40 changes: 23 additions & 17 deletions apps/expert/lib/expert/provider/handlers/commands.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.Commands do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.Project
Expand All @@ -25,13 +26,16 @@ defmodule Expert.Provider.Handlers.Commands do

def handle(
%Requests.WorkspaceExecuteCommand{params: %Structures.ExecuteCommandParams{} = params},
%Configuration{} = config
%Configuration{}
) do
projects = ActiveProjects.projects()

response =
case params.command do
@reindex_name ->
Logger.info("Reindex #{Project.name(config.project)}")
reindex(config.project)
project_names = Enum.map_join(projects, ", ", &Project.name/1)
Logger.info("Reindex #{project_names}")
reindex_all(projects)

invalid ->
message = "#{invalid} is not a valid command"
Expand All @@ -41,23 +45,25 @@ defmodule Expert.Provider.Handlers.Commands do
{:reply, response}
end

defp reindex(%Project{} = project) do
case EngineApi.reindex(project) do
:ok ->
{:ok, "ok"}
defp reindex_all(projects) do
Enum.reduce_while(projects, :ok, fn project, _ ->
case EngineApi.reindex(project) do
:ok ->
{:cont, "ok"}

error ->
GenLSP.notify(Expert.get_lsp(), %GenLSP.Notifications.WindowShowMessage{
params: %GenLSP.Structures.ShowMessageParams{
type: GenLSP.Enumerations.MessageType.error(),
message: "Indexing #{Project.name(project)} failed"
}
})
error ->
GenLSP.notify(Expert.get_lsp(), %GenLSP.Notifications.WindowShowMessage{
params: %GenLSP.Structures.ShowMessageParams{
type: GenLSP.Enumerations.MessageType.error(),
message: "Indexing #{Project.name(project)} failed"
}
})

Logger.error("Indexing command failed due to #{inspect(error)}")
Logger.error("Indexing command failed due to #{inspect(error)}")

{:ok, internal_error("Could not reindex: #{error}")}
end
{:halt, internal_error("Could not reindex: #{error}")}
end
end)
end

defp internal_error(message) do
Expand Down
8 changes: 6 additions & 2 deletions apps/expert/lib/expert/provider/handlers/completion.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
defmodule Expert.Provider.Handlers.Completion do
alias Expert.ActiveProjects
alias Expert.CodeIntelligence
alias Expert.Configuration
alias Forge.Ast
alias Forge.Document
alias Forge.Document.Position
alias Forge.Project
alias GenLSP.Enumerations.CompletionTriggerKind
alias GenLSP.Requests
alias GenLSP.Structures
Expand All @@ -13,13 +15,15 @@ defmodule Expert.Provider.Handlers.Completion do
%Requests.TextDocumentCompletion{
params: %Structures.CompletionParams{} = params
},
%Configuration{} = config
%Configuration{}
) do
document = Document.Container.context_document(params, nil)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

completions =
CodeIntelligence.Completion.complete(
config.project,
project,
document_analysis(document, params.position),
params.position,
params.context || %CompletionContext{trigger_kind: CompletionTriggerKind.invoked()}
Expand Down
8 changes: 6 additions & 2 deletions apps/expert/lib/expert/provider/handlers/document_symbols.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
defmodule Expert.Provider.Handlers.DocumentSymbols do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.CodeIntelligence.Symbols
alias Forge.Document
alias Forge.Project
alias GenLSP.Enumerations.SymbolKind
alias GenLSP.Requests
alias GenLSP.Structures

def handle(%Requests.TextDocumentDocumentSymbol{} = request, %Configuration{} = config) do
def handle(%Requests.TextDocumentDocumentSymbol{} = request, %Configuration{}) do
document = Document.Container.context_document(request.params, nil)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

symbols =
config.project
project
|> EngineApi.document_symbols(document)
|> Enum.map(&to_response(&1, document))

Expand Down
8 changes: 6 additions & 2 deletions apps/expert/lib/expert/provider/handlers/find_references.ex
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
defmodule Expert.Provider.Handlers.FindReferences do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.Ast
alias Forge.Document
alias Forge.Project
alias GenLSP.Requests.TextDocumentReferences
alias GenLSP.Structures

require Logger

def handle(
%TextDocumentReferences{params: %Structures.ReferenceParams{} = params},
%Configuration{} = config
%Configuration{}
) do
document = Forge.Document.Container.context_document(params, nil)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)
include_declaration? = !!params.context.include_declaration

locations =
case Document.Store.fetch(document.uri, :analysis) do
{:ok, _document, %Ast.Analysis{} = analysis} ->
EngineApi.references(config.project, analysis, params.position, include_declaration?)
EngineApi.references(project, analysis, params.position, include_declaration?)

_ ->
nil
Expand Down
Loading
Loading