Skip to content
This repository was archived by the owner on Oct 8, 2025. It is now read-only.

Commit a7e9bc6

Browse files
authored
feat: completions (#289)
## Description Implements basic autocompletion. Current completion candidates supported - Global modules - Global Structs - Struct fields - Remote functions (w/ documentation) - Special Forms - Bitstring modifiers - filesystem paths in strings More features, particularly features that rely on contextual information about the code itself (meaning, which identifiers, aliases, imports are available) will come in subsequent patches. Partially addresses #45 ## Experimental This patch also introduces a new initialization option, `experimental`. This feature will be gated as an experimental feature as it's built out. The purpose of this is so that early-early adopters can try it out and report bugs, but folks who would rather wait for something more stable won't have it affect their workflows. To enable this feature, toggle the completions experiment in your editor. ### Nvim (elixir-tools.nvim) ```lua require("elixir").setup({ nextls = { enable = true, init_options = { experimental = { completions = { enable = true } } } }, elixirls = {enable = false} }) ``` ### Visual Studio Code (elixir-tools.vscode) ```json { "elixir-tools.nextLS.experimental.completions.enable": true } ``` ### Other editors Not sure 😅 ## Demos TODO: record them my guy ## TODO - [x] integration tests - [ ] update elixir-tools.dev with instructions - [x] update README with instructions ## Acknowedgements This feature is initially based on `IEx.Autocomplete`. Huge thanks to the Elixir core team's efforts to help kickstart this feature. More deviations will likely occur as we gain more contextual parsing for things like imports, aliases and variables.
1 parent dace852 commit a7e9bc6

File tree

9 files changed

+2226
-4
lines changed

9 files changed

+2226
-4
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Still in heavy development, currently supporting the following features:
1818
- Extensions
1919
- Credo
2020
- Hover
21+
- Completions †‡
22+
23+
† - denotes an experimental feature, which can be toggled on/off.
24+
‡ - denotes a partially implemented feature.
2125

2226
## Supported Elixir Versions
2327

lib/next_ls.ex

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ defmodule NextLS do
1616
alias GenLSP.Notifications.WorkspaceDidChangeWorkspaceFolders
1717
alias GenLSP.Requests.Initialize
1818
alias GenLSP.Requests.Shutdown
19+
alias GenLSP.Requests.TextDocumentCompletion
1920
alias GenLSP.Requests.TextDocumentDefinition
2021
alias GenLSP.Requests.TextDocumentDocumentSymbol
2122
alias GenLSP.Requests.TextDocumentFormatting
@@ -117,6 +118,14 @@ defmodule NextLS do
117118
save: %SaveOptions{include_text: true},
118119
change: TextDocumentSyncKind.full()
119120
},
121+
completion_provider:
122+
if init_opts.experimental.completions.enable do
123+
%GenLSP.Structures.CompletionOptions{
124+
trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"]
125+
}
126+
else
127+
nil
128+
end,
120129
document_formatting_provider: true,
121130
hover_provider: true,
122131
workspace_symbol_provider: true,
@@ -504,6 +513,60 @@ defmodule NextLS do
504513
resp
505514
end
506515

516+
def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
517+
document = lsp.assigns.documents[uri]
518+
519+
document_slice =
520+
document
521+
|> Enum.take(position.line + 1)
522+
|> Enum.reverse()
523+
|> then(fn [last_line | rest] ->
524+
{line, _forget} = String.split_at(last_line, position.character)
525+
[line | rest]
526+
end)
527+
|> Enum.reverse()
528+
|> Enum.join("\n")
529+
530+
results =
531+
lsp.assigns.registry
532+
|> dispatch(:runtimes, fn entries ->
533+
[result] =
534+
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
535+
NextLS.Autocomplete.expand(document_slice |> String.to_charlist() |> Enum.reverse(), runtime)
536+
end
537+
538+
case result do
539+
{:yes, entries} -> entries
540+
_ -> []
541+
end
542+
end)
543+
|> Enum.map(fn %{name: name, kind: kind} = symbol ->
544+
{label, kind, docs} =
545+
case kind do
546+
:struct -> {name, GenLSP.Enumerations.CompletionItemKind.struct(), ""}
547+
:function -> {"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs}
548+
:module -> {name, GenLSP.Enumerations.CompletionItemKind.module(), ""}
549+
:variable -> {name, GenLSP.Enumerations.CompletionItemKind.variable(), ""}
550+
:dir -> {name, GenLSP.Enumerations.CompletionItemKind.folder(), ""}
551+
:file -> {name, GenLSP.Enumerations.CompletionItemKind.file(), ""}
552+
:keyword -> {name, GenLSP.Enumerations.CompletionItemKind.field(), ""}
553+
_ -> {name, GenLSP.Enumerations.CompletionItemKind.text(), ""}
554+
end
555+
556+
%GenLSP.Structures.CompletionItem{
557+
label: label,
558+
kind: kind,
559+
insert_text: name,
560+
documentation: docs
561+
}
562+
end)
563+
564+
{:reply, results, lsp}
565+
rescue
566+
_ ->
567+
{:reply, [], lsp}
568+
end
569+
507570
def handle_request(%Shutdown{}, lsp) do
508571
{:reply, nil, assign(lsp, exit_code: 0)}
509572
end
@@ -1019,18 +1082,30 @@ defmodule NextLS do
10191082
# penalty for unmatched letter
10201083
defp calc_unmatched_penalty(score, _traits), do: score - 1
10211084

1085+
defmodule InitOpts.Experimental do
1086+
@moduledoc false
1087+
defstruct completions: %{enable: false}
1088+
end
1089+
10221090
defmodule InitOpts do
10231091
@moduledoc false
10241092
import Schematic
10251093

1026-
defstruct mix_target: "host", mix_env: "dev"
1094+
defstruct mix_target: "host", mix_env: "dev", experimental: %NextLS.InitOpts.Experimental{}
10271095

10281096
def validate(opts) do
10291097
schematic =
10301098
nullable(
10311099
schema(__MODULE__, %{
10321100
optional(:mix_target) => str(),
1033-
optional(:mix_env) => str()
1101+
optional(:mix_env) => str(),
1102+
optional(:experimental) =>
1103+
schema(NextLS.InitOpts.Experimental, %{
1104+
optional(:completions) =>
1105+
map(%{
1106+
{"enable", :enable} => bool()
1107+
})
1108+
})
10341109
})
10351110
)
10361111

0 commit comments

Comments
 (0)