From ee4e7524c9191ea2f701736be35e460329f88b27 Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Sun, 15 Oct 2023 14:08:18 -0400 Subject: [PATCH] Allow empty completions for non-VS Code clients 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. --- .../server/code_intelligence/completion.ex | 48 +++++++++++++++---- .../lib/lexical/server/configuration.ex | 10 ++-- apps/server/lib/lexical/server/state.ex | 8 +++- .../support/lexical/test/completion_case.ex | 4 +- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/apps/server/lib/lexical/server/code_intelligence/completion.ex b/apps/server/lib/lexical/server/code_intelligence/completion.ex index 74c473370..d68a71549 100644 --- a/apps/server/lib/lexical/server/code_intelligence/completion.ex +++ b/apps/server/lib/lexical/server/code_intelligence/completion.ex @@ -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 @@ -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) -> @@ -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. @@ -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 @@ -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 diff --git a/apps/server/lib/lexical/server/configuration.ex b/apps/server/lib/lexical/server/configuration.ex index 3572f2b4c..6b898ff90 100644 --- a/apps/server/lib/lexical/server/configuration.ex +++ b/apps/server/lib/lexical/server/configuration.ex @@ -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() } @@ -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 diff --git a/apps/server/lib/lexical/server/state.ex b/apps/server/lib/lexical/server/state.ex index f9aebac44..deb7d9b33 100644 --- a/apps/server/lib/lexical/server/state.ex +++ b/apps/server/lib/lexical/server/state.ex @@ -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}") diff --git a/apps/server/test/support/lexical/test/completion_case.ex b/apps/server/test/support/lexical/test/completion_case.ex index 3da439ca2..431c76cca 100644 --- a/apps/server/test/support/lexical/test/completion_case.ex +++ b/apps/server/test/support/lexical/test/completion_case.ex @@ -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)