Skip to content

Commit

Permalink
Allow empty completions for non-VS Code clients
Browse files Browse the repository at this point in the history
This commit reverts some of #398 in order to retain the previous, better
UX that users had in other editors, while still allowing completions to
work in VS Code.
  • Loading branch information
zachallaun committed Oct 16, 2023
1 parent 7c48883 commit ee4e752
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 16 deletions.
48 changes: 38 additions & 10 deletions apps/server/lib/lexical/server/code_intelligence/completion.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
alias Lexical.RemoteControl.Completion.Candidate
alias Lexical.RemoteControl.Modules.Predicate
alias Lexical.Server.CodeIntelligence.Completion.Builder
alias Lexical.Server.Configuration
alias Lexical.Server.Project.Intelligence
alias Mix.Tasks.Namespace

Expand Down Expand Up @@ -50,7 +51,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
prefix_tokens = Env.prefix_tokens(env, 1)

cond do
prefix_tokens == [] or not context_will_give_meaningful_completions?(env) ->
prefix_tokens == [] or not should_emit_completions?(env) ->
[]

should_emit_do_end_snippet?(env) ->
Expand All @@ -74,6 +75,41 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
end
end

defp should_emit_completions?(%Env{} = env) do
always_emit_completions?() or has_meaningful_completions?(env)
end

defp always_emit_completions? do
# If VS Code receives an empty completion list, it will never issue
# a new request, even if `is_incomplete: true` is specified.
# https://github.com/lexical-lsp/lexical/issues/400
Configuration.get().client_name == "Visual Studio Code"
end

defp has_meaningful_completions?(%Env{} = env) do
case Code.Fragment.cursor_context(env.prefix) do
:none ->
false

{:unquoted_atom, name} ->
length(name) > 1

{:local_or_var, name} ->
local_length = length(name)
surround_begin = max(1, env.position.character - local_length - 1)

local_length > 1 or has_surround_context?(env.prefix, 1, surround_begin)

_ ->
true
end
end

defp has_surround_context?(fragment, line, column)
when is_binary(fragment) and line >= 1 and column >= 1 do
Code.Fragment.surround_context(fragment, {line, column}) != :none
end

# We emit a do/end snippet if the prefix token is the do operator and
# there is a space before the token preceding it on the same line. This
# handles situations like `@do|` where a do/end snippet would be invalid.
Expand Down Expand Up @@ -113,14 +149,6 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
|> List.wrap()
end

defp context_will_give_meaningful_completions?(%Env{} = env) do
Code.Fragment.cursor_context(env.prefix) != :none && not single_char_atom_prefix?(env)
end

defp single_char_atom_prefix?(env) do
match?([{:atom, [_], _}], Env.prefix_tokens(env, 1))
end

defp displayable?(%Project{} = project, result) do
suggested_module =
case result do
Expand Down Expand Up @@ -218,6 +246,6 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
end

defp completion_list(items \\ []) do
Completion.List.new(items: items, is_incomplete: true)
Completion.List.new(items: items, is_incomplete: items == [])
end
end
10 changes: 7 additions & 3 deletions apps/server/lib/lexical/server/configuration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ defmodule Lexical.Server.Configuration do

defstruct project: nil,
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,
dialyzer_enabled?: boolean()
}
Expand All @@ -29,11 +31,13 @@ defmodule Lexical.Server.Configuration do

@dialyzer {:nowarn_function, set_dialyzer_enabled: 2}

@spec new(Lexical.uri(), map()) :: t
def new(root_uri, %ClientCapabilities{} = client_capabilities) do
@spec new(Lexical.uri(), map(), String.t() | nil) :: t
def new(root_uri, %ClientCapabilities{} = client_capabilities, client_name) do
support = Support.new(client_capabilities)
project = Project.new(root_uri)
%__MODULE__{support: support, project: project} |> tap(&set/1)

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

@spec new() :: t
Expand Down
8 changes: 7 additions & 1 deletion apps/server/lib/lexical/server/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ defmodule Lexical.Server.State do
def initialize(%__MODULE__{initialized?: false} = state, %Initialize{
lsp: %Initialize.LSP{} = event
}) do
config = Configuration.new(event.root_uri, event.capabilities)
client_name =
case event.client_info do
%{name: name} -> name
_ -> nil
end

config = Configuration.new(event.root_uri, event.capabilities, client_name)
new_state = %__MODULE__{state | configuration: config, initialized?: true}
Logger.info("Starting project at uri #{config.project.root_uri}")

Expand Down
4 changes: 2 additions & 2 deletions apps/server/test/support/lexical/test/completion_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ defmodule Lexical.Test.Server.CompletionCase do
if is_binary(trigger_character) do
CompletionContext.new(
trigger_kind: :trigger_character,
trigger_character: nil
trigger_character: trigger_character
)
else
CompletionContext.new(trigger_kind: :trigger_character)
CompletionContext.new(trigger_kind: :invoked)
end

result = Completion.complete(project, document, position, context)
Expand Down

0 comments on commit ee4e752

Please sign in to comment.