Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
42 changes: 4 additions & 38 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 projects: [],
support: nil,
defstruct support: nil,
client_name: nil,
additional_watched_extensions: nil,
dialyzer_enabled?: false

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

@dialyzer {:nowarn_function, set_dialyzer_enabled: 2}

@spec new([Structures.WorkspaceFolder.t()], map(), String.t() | nil) :: t
def new(workspace_folders, %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)

projects =
for %{uri: uri} <- workspace_folders do
Project.new(uri)
end

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

Expand Down Expand Up @@ -147,30 +139,4 @@ defmodule Expert.Configuration do
defp maybe_add_watched_extensions(%__MODULE__{} = old_config, _) do
{:ok, old_config}
end

@spec add_projects(t, [Project.t()]) :: t
def add_projects(%__MODULE__{} = config, projects) do
new_config =
for project <- projects, reduce: config do
config ->
if Enum.any?(config.projects, &(&1.root_uri == project.root_uri)) do
config
else
projects = [project | config.projects]
%__MODULE__{config | projects: projects}
end
end

set(new_config)

new_config
end

@spec remove_projects(t, [String.t()]) :: t
def remove_projects(%__MODULE__{} = config, projects) do
new_projects = Enum.reject(config.projects, &(&1.root_uri in projects))
new_config = %__MODULE__{config | projects: new_projects}
set(new_config)
new_config
end
end
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/code_action.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.CodeAction do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.CodeAction
Expand All @@ -8,10 +9,11 @@ defmodule Expert.Provider.Handlers.CodeAction do

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

code_actions =
Expand Down
6 changes: 4 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,10 +15,11 @@ 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)
project = Project.project_for_document(config.projects, document)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

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

Expand Down
9 changes: 6 additions & 3 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,14 +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 ->
project_names = Enum.map_join(config.projects, ", ", &Project.name/1)
project_names = Enum.map_join(projects, ", ", &Project.name/1)
Logger.info("Reindex #{project_names}")
reindex_all(config.projects)
reindex_all(projects)

invalid ->
message = "#{invalid} is not a valid command"
Expand Down
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/completion.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.Completion do
alias Expert.ActiveProjects
alias Expert.CodeIntelligence
alias Expert.Configuration
alias Forge.Ast
Expand All @@ -14,10 +15,11 @@ defmodule Expert.Provider.Handlers.Completion do
%Requests.TextDocumentCompletion{
params: %Structures.CompletionParams{} = params
},
%Configuration{} = config
%Configuration{}
) do
document = Document.Container.context_document(params, nil)
project = Project.project_for_document(config.projects, document)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

completions =
CodeIntelligence.Completion.complete(
Expand Down
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/document_symbols.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.DocumentSymbols do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.CodeIntelligence.Symbols
Expand All @@ -8,9 +9,10 @@ defmodule Expert.Provider.Handlers.DocumentSymbols do
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)
project = Project.project_for_document(config.projects, document)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

symbols =
project
Expand Down
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/find_references.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.FindReferences do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.Ast
Expand All @@ -11,10 +12,11 @@ defmodule Expert.Provider.Handlers.FindReferences do

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

locations =
Expand Down
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/formatting.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.Formatting do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.Document.Changes
Expand All @@ -10,10 +11,11 @@ defmodule Expert.Provider.Handlers.Formatting do

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

case EngineApi.format(project, document) do
{:ok, %Changes{} = document_edits} ->
Expand Down
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/go_to_definition.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.GoToDefinition do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.Project
Expand All @@ -11,10 +12,11 @@ defmodule Expert.Provider.Handlers.GoToDefinition do
%Requests.TextDocumentDefinition{
params: %Structures.DefinitionParams{} = params
},
%Configuration{} = config
%Configuration{}
) do
document = Forge.Document.Container.context_document(params, nil)
project = Project.project_for_document(config.projects, document)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

case EngineApi.definition(project, document, params.position) do
{:ok, native_location} ->
Expand Down
6 changes: 4 additions & 2 deletions apps/expert/lib/expert/provider/handlers/hover.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.Hover do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Expert.Provider.Markdown
Expand All @@ -17,10 +18,11 @@ defmodule Expert.Provider.Handlers.Hover do
%Requests.TextDocumentHover{
params: %Structures.HoverParams{} = params
},
%Configuration{} = config
%Configuration{}
) do
document = Document.Container.context_document(params, nil)
project = Project.project_for_document(config.projects, document)
projects = ActiveProjects.projects()
project = Project.project_for_document(projects, document)

maybe_hover =
with {:ok, _document, %Ast.Analysis{} = analysis} <-
Expand Down
7 changes: 5 additions & 2 deletions apps/expert/lib/expert/provider/handlers/workspace_symbol.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Expert.Provider.Handlers.WorkspaceSymbol do
alias Expert.ActiveProjects
alias Expert.Configuration
alias Expert.EngineApi
alias Forge.CodeIntelligence.Symbols
Expand All @@ -11,11 +12,13 @@ defmodule Expert.Provider.Handlers.WorkspaceSymbol do

def handle(
%Requests.WorkspaceSymbol{params: %Structures.WorkspaceSymbolParams{} = params} = request,
%Configuration{} = config
%Configuration{}
) do
projects = ActiveProjects.projects()

symbols =
if String.length(params.query) > 1 do
Enum.flat_map(config.projects, &gather_symbols(&1, request))
Enum.flat_map(projects, &gather_symbols(&1, request))
else
[]
end
Expand Down
Loading
Loading