Skip to content

Commit

Permalink
feat: completions (#289)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
mhanberg committed Oct 19, 2023
1 parent dace852 commit a7e9bc6
Show file tree
Hide file tree
Showing 9 changed files with 2,226 additions and 4 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Still in heavy development, currently supporting the following features:
- Extensions
- Credo
- Hover
- Completions †‡

† - denotes an experimental feature, which can be toggled on/off.
‡ - denotes a partially implemented feature.

## Supported Elixir Versions

Expand Down
79 changes: 77 additions & 2 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule NextLS do
alias GenLSP.Notifications.WorkspaceDidChangeWorkspaceFolders
alias GenLSP.Requests.Initialize
alias GenLSP.Requests.Shutdown
alias GenLSP.Requests.TextDocumentCompletion
alias GenLSP.Requests.TextDocumentDefinition
alias GenLSP.Requests.TextDocumentDocumentSymbol
alias GenLSP.Requests.TextDocumentFormatting
Expand Down Expand Up @@ -117,6 +118,14 @@ defmodule NextLS do
save: %SaveOptions{include_text: true},
change: TextDocumentSyncKind.full()
},
completion_provider:
if init_opts.experimental.completions.enable do
%GenLSP.Structures.CompletionOptions{
trigger_characters: [".", "@", "&", "%", "^", ":", "!", "-", "~", "/", "{"]
}
else
nil
end,
document_formatting_provider: true,
hover_provider: true,
workspace_symbol_provider: true,
Expand Down Expand Up @@ -504,6 +513,60 @@ defmodule NextLS do
resp
end

def handle_request(%TextDocumentCompletion{params: %{text_document: %{uri: uri}, position: position}}, lsp) do
document = lsp.assigns.documents[uri]

document_slice =
document
|> Enum.take(position.line + 1)
|> Enum.reverse()
|> then(fn [last_line | rest] ->
{line, _forget} = String.split_at(last_line, position.character)
[line | rest]
end)
|> Enum.reverse()
|> Enum.join("\n")

results =
lsp.assigns.registry
|> dispatch(:runtimes, fn entries ->
[result] =
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
NextLS.Autocomplete.expand(document_slice |> String.to_charlist() |> Enum.reverse(), runtime)
end

case result do
{:yes, entries} -> entries
_ -> []
end
end)
|> Enum.map(fn %{name: name, kind: kind} = symbol ->
{label, kind, docs} =
case kind do
:struct -> {name, GenLSP.Enumerations.CompletionItemKind.struct(), ""}
:function -> {"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs}
:module -> {name, GenLSP.Enumerations.CompletionItemKind.module(), ""}
:variable -> {name, GenLSP.Enumerations.CompletionItemKind.variable(), ""}
:dir -> {name, GenLSP.Enumerations.CompletionItemKind.folder(), ""}
:file -> {name, GenLSP.Enumerations.CompletionItemKind.file(), ""}
:keyword -> {name, GenLSP.Enumerations.CompletionItemKind.field(), ""}
_ -> {name, GenLSP.Enumerations.CompletionItemKind.text(), ""}
end

%GenLSP.Structures.CompletionItem{
label: label,
kind: kind,
insert_text: name,
documentation: docs
}
end)

{:reply, results, lsp}
rescue
_ ->
{:reply, [], lsp}
end

def handle_request(%Shutdown{}, lsp) do
{:reply, nil, assign(lsp, exit_code: 0)}
end
Expand Down Expand Up @@ -1019,18 +1082,30 @@ defmodule NextLS do
# penalty for unmatched letter
defp calc_unmatched_penalty(score, _traits), do: score - 1

defmodule InitOpts.Experimental do
@moduledoc false
defstruct completions: %{enable: false}
end

defmodule InitOpts do
@moduledoc false
import Schematic

defstruct mix_target: "host", mix_env: "dev"
defstruct mix_target: "host", mix_env: "dev", experimental: %NextLS.InitOpts.Experimental{}

def validate(opts) do
schematic =
nullable(
schema(__MODULE__, %{
optional(:mix_target) => str(),
optional(:mix_env) => str()
optional(:mix_env) => str(),
optional(:experimental) =>
schema(NextLS.InitOpts.Experimental, %{
optional(:completions) =>
map(%{
{"enable", :enable} => bool()
})
})
})
)

Expand Down
Loading

0 comments on commit a7e9bc6

Please sign in to comment.