From e169017eef7c3c7dc9711ec89b3e17e11cde92db Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 30 Dec 2023 15:57:21 +0100 Subject: [PATCH 01/11] debounce parse made async --- .../lib/language_server/parser.ex | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index 3381ef1e0..8bb61b664 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -250,13 +250,27 @@ defmodule ElixirLS.LanguageServer.Parser do file = Map.fetch!(files, uri) Logger.debug("Parsing #{uri} after debounce: languageId #{file.source_file.language_id}") - updated_file = - file - |> do_parse() + parent = self() + + # TODO store pid, monitor? + + spawn(fn -> + updated_file = do_parse(file) + send(parent, {:parse_file_done, uri, updated_file}) + end) + + state = %{state | debounce_refs: Map.delete(debounce_refs, uri)} + {:noreply, state} + end + + def handle_info( + {:parse_file_done, uri, updated_file}, + %{files: files} = state + ) do updated_files = Map.put(files, uri, updated_file) - state = %{state | files: updated_files, debounce_refs: Map.delete(debounce_refs, uri)} + state = %{state | files: updated_files} notify_diagnostics_updated(updated_files) From f8d9daea1fffae23b80e39169d323cd752005a23 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 30 Dec 2023 15:58:12 +0100 Subject: [PATCH 02/11] extract common function --- .../lib/language_server/parser.ex | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index 8bb61b664..9f5e5f2bf 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -87,16 +87,14 @@ defmodule ElixirLS.LanguageServer.Parser do end @impl true - def handle_cast({:closed, uri}, state = %{files: files, debounce_refs: debounce_refs}) do - {maybe_ref, updated_debounce_refs} = Map.pop(debounce_refs, uri) + def handle_cast({:closed, uri}, state = %{files: files}) do + state = cancel_debounce(state, uri) - if maybe_ref do - Process.cancel_timer(maybe_ref, info: false) - end + # TODO maybe cancel parse updated_files = Map.delete(files, uri) notify_diagnostics_updated(updated_files) - {:noreply, %{state | files: updated_files, debounce_refs: updated_debounce_refs}} + {:noreply, %{state | files: updated_files}} end def handle_cast({:parse_with_debounce, uri, source_file = %SourceFile{}}, state) do @@ -133,15 +131,13 @@ defmodule ElixirLS.LanguageServer.Parser do def handle_call( {:parse_immediate, uri, source_file = %SourceFile{}}, _from, - %{files: files, debounce_refs: debounce_refs} = state + %{files: files} = state ) do {reply, state} = if should_parse?(uri, source_file) do - {maybe_ref, updated_debounce_refs} = Map.pop(debounce_refs, uri) + state = cancel_debounce(state, uri) - if maybe_ref do - Process.cancel_timer(maybe_ref, info: false) - end + # TODO cancel or wait for parse end? current_version = source_file.version @@ -165,7 +161,7 @@ defmodule ElixirLS.LanguageServer.Parser do notify_diagnostics_updated(updated_files) - state = %{state | files: updated_files, debounce_refs: updated_debounce_refs} + state = %{state | files: updated_files} {file, state} end else @@ -187,15 +183,13 @@ defmodule ElixirLS.LanguageServer.Parser do def handle_call( {:parse_immediate, uri, source_file = %SourceFile{}, position}, _from, - %{files: files, debounce_refs: debounce_refs} = state + %{files: files} = state ) do {reply, state} = if should_parse?(uri, source_file) do - {maybe_ref, updated_debounce_refs} = Map.pop(debounce_refs, uri) + state = cancel_debounce(state, uri) - if maybe_ref do - Process.cancel_timer(maybe_ref, info: false) - end + # TODO cancel or wait for parse end? current_version = source_file.version @@ -206,7 +200,7 @@ defmodule ElixirLS.LanguageServer.Parser do updated_files = Map.put(files, uri, file) # no change to diagnostics, only update stored metadata - state = %{state | files: updated_files, debounce_refs: updated_debounce_refs} + state = %{state | files: updated_files} {file, state} _other -> @@ -223,7 +217,7 @@ defmodule ElixirLS.LanguageServer.Parser do notify_diagnostics_updated(updated_files) - state = %{state | files: updated_files, debounce_refs: updated_debounce_refs} + state = %{state | files: updated_files} {file, state} end else @@ -435,6 +429,16 @@ defmodule ElixirLS.LanguageServer.Parser do end end + defp cancel_debounce(state = %{debounce_refs: debounce_refs}, uri) do + {maybe_ref, updated_debounce_refs} = Map.pop(debounce_refs, uri) + + if maybe_ref do + Process.cancel_timer(maybe_ref, info: false) + end + + %{state | debounce_refs: updated_debounce_refs} + end + defp notify_diagnostics_updated(updated_files) do updated_files |> Map.new(fn {uri, %Context{diagnostics: diagnostics, parsed_version: version}} -> From b9c5b5929a5e24159dd81ea395a7e6f24ad01b1d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 30 Dec 2023 16:50:32 +0100 Subject: [PATCH 03/11] don't go through the server for not parsable documents --- .../lib/language_server/parser.ex | 204 +++++++++--------- 1 file changed, 101 insertions(+), 103 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index 9f5e5f2bf..fb3a8e688 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -39,21 +39,48 @@ defmodule ElixirLS.LanguageServer.Parser do end def parse_with_debounce(uri, source_file = %SourceFile{}) do - GenServer.cast(__MODULE__, {:parse_with_debounce, uri, source_file}) + if should_parse?(uri, source_file) do + GenServer.cast(__MODULE__, {:parse_with_debounce, uri, source_file}) + else + Logger.debug("Not parsing #{uri} with debounce: languageId #{source_file.language_id}") + :ok + end end def parse_immediate(uri, source_file = %SourceFile{}) do - GenServer.call(__MODULE__, {:parse_immediate, uri, source_file}, @parse_timeout) + if should_parse?(uri, source_file) do + GenServer.call(__MODULE__, {:parse_immediate, uri, source_file}, @parse_timeout) + else + Logger.debug("Not parsing #{uri} immediately: languageId #{source_file.language_id}") + # not parsing - respond with empty struct + %Context{ + source_file: source_file, + path: get_path(uri), + ast: @dummy_ast, + metadata: @dummy_metadata + } + end end def parse_immediate(uri, source_file = %SourceFile{}, position) do - GenServer.call(__MODULE__, {:parse_immediate, uri, source_file, position}, @parse_timeout) + if should_parse?(uri, source_file) do + GenServer.call(__MODULE__, {:parse_immediate, uri, source_file, position}, @parse_timeout) + else + Logger.debug("Not parsing #{uri} immediately: languageId #{source_file.language_id}") + # not parsing - respond with empty struct + %Context{ + source_file: source_file, + path: get_path(uri), + ast: @dummy_ast, + metadata: @dummy_metadata + } + end end @impl true def init(_args) do # TODO get source files on start? - {:ok, %{files: %{}, debounce_refs: %{}}} + {:ok, %{files: %{}, debounce_refs: %{}, parse_pids: %{}}} end @impl GenServer @@ -99,30 +126,24 @@ defmodule ElixirLS.LanguageServer.Parser do def handle_cast({:parse_with_debounce, uri, source_file = %SourceFile{}}, state) do state = - if should_parse?(uri, source_file) do - state = - update_in(state.debounce_refs[uri], fn old_ref -> - if old_ref do - Process.cancel_timer(old_ref, info: false) - end + update_in(state.debounce_refs[uri], fn old_ref -> + if old_ref do + Process.cancel_timer(old_ref, info: false) + end - Process.send_after(self(), {:parse_file, uri}, @debounce_timeout) - end) + Process.send_after(self(), {:parse_file, uri}, @debounce_timeout) + end) - update_in(state.files[uri], fn - nil -> - %Context{ - source_file: source_file, - path: get_path(uri) - } + state = update_in(state.files[uri], fn + nil -> + %Context{ + source_file: source_file, + path: get_path(uri) + } - old_file -> - %Context{old_file | source_file: source_file} - end) - else - Logger.debug("Not parsing #{uri} with debounce: languageId #{source_file.language_id}") - state - end + old_file -> + %Context{old_file | source_file: source_file} + end) {:noreply, state} end @@ -133,49 +154,35 @@ defmodule ElixirLS.LanguageServer.Parser do _from, %{files: files} = state ) do - {reply, state} = - if should_parse?(uri, source_file) do - state = cancel_debounce(state, uri) + state = cancel_debounce(state, uri) - # TODO cancel or wait for parse end? + # TODO cancel or wait for parse end? - current_version = source_file.version + current_version = source_file.version - case files[uri] do - %Context{parsed_version: ^current_version} = file -> - Logger.debug("#{uri} already parsed") - # current version already parsed - {file, state} + {reply, state} = case files[uri] do + %Context{parsed_version: ^current_version} = file -> + Logger.debug("#{uri} already parsed") + # current version already parsed + {file, state} - _other -> - Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") - # overwrite everything - file = - %Context{ - source_file: source_file, - path: get_path(uri) - } - |> do_parse() + _other -> + Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") + # overwrite everything + file = + %Context{ + source_file: source_file, + path: get_path(uri) + } + |> do_parse() - updated_files = Map.put(files, uri, file) + updated_files = Map.put(files, uri, file) - notify_diagnostics_updated(updated_files) + notify_diagnostics_updated(updated_files) - state = %{state | files: updated_files} - {file, state} - end - else - Logger.debug("Not parsing #{uri} immediately: languageId #{source_file.language_id}") - # not parsing - respond with empty struct - reply = %Context{ - source_file: source_file, - path: get_path(uri), - ast: @dummy_ast, - metadata: @dummy_metadata - } - - {reply, state} - end + state = %{state | files: updated_files} + {file, state} + end {:reply, reply, state} end @@ -185,53 +192,44 @@ defmodule ElixirLS.LanguageServer.Parser do _from, %{files: files} = state ) do - {reply, state} = - if should_parse?(uri, source_file) do - state = cancel_debounce(state, uri) - - # TODO cancel or wait for parse end? - - current_version = source_file.version - - case files[uri] do - %Context{parsed_version: ^current_version} = file -> - Logger.debug("#{uri} already parsed for cursor position #{inspect(position)}") - file = maybe_fix_missing_env(file, position) - - updated_files = Map.put(files, uri, file) - # no change to diagnostics, only update stored metadata - state = %{state | files: updated_files} - {file, state} + state = cancel_debounce(state, uri) - _other -> - Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") - # overwrite everything - file = - %Context{ - source_file: source_file, - path: get_path(uri) - } - |> do_parse(position) + # TODO cancel or wait for parse end? - updated_files = Map.put(files, uri, file) + current_version = source_file.version - notify_diagnostics_updated(updated_files) + {reply, state} = case files[uri] do + %Context{parsed_version: ^current_version} = file -> + Logger.debug("#{uri} already parsed for cursor position #{inspect(position)}") + file = maybe_fix_missing_env(file, position) - state = %{state | files: updated_files} - {file, state} - end - else - Logger.debug("Not parsing #{uri} immediately: languageId #{source_file.language_id}") - # not parsing - respond with empty struct - reply = %Context{ - source_file: source_file, - path: get_path(uri), - ast: @dummy_ast, - metadata: @dummy_metadata - } + updated_files = Map.put(files, uri, file) + # no change to diagnostics, only update stored metadata + state = %{state | files: updated_files} + {file, state} - {reply, state} - end + _other -> + Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") + # overwrite everything + file = + %Context{ + source_file: source_file, + path: get_path(uri) + } + |> do_parse(position) + + # spawn(fn -> + # updated_file = do_parse(file) + # send(parent, {:parse_file_done, uri, updated_file}) + # end) + + updated_files = Map.put(files, uri, file) + + notify_diagnostics_updated(updated_files) + + state = %{state | files: updated_files} + {file, state} + end {:reply, reply, state} end From 195b217c9d3993567fe69657689e5b877002395b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 30 Dec 2023 17:00:26 +0100 Subject: [PATCH 04/11] use common code path for calls with and without position --- .../lib/language_server/parser.ex | 58 +------------------ 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index fb3a8e688..b5e139b19 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -47,22 +47,7 @@ defmodule ElixirLS.LanguageServer.Parser do end end - def parse_immediate(uri, source_file = %SourceFile{}) do - if should_parse?(uri, source_file) do - GenServer.call(__MODULE__, {:parse_immediate, uri, source_file}, @parse_timeout) - else - Logger.debug("Not parsing #{uri} immediately: languageId #{source_file.language_id}") - # not parsing - respond with empty struct - %Context{ - source_file: source_file, - path: get_path(uri), - ast: @dummy_ast, - metadata: @dummy_metadata - } - end - end - - def parse_immediate(uri, source_file = %SourceFile{}, position) do + def parse_immediate(uri, source_file = %SourceFile{}, position \\ nil) do if should_parse?(uri, source_file) do GenServer.call(__MODULE__, {:parse_immediate, uri, source_file, position}, @parse_timeout) else @@ -149,44 +134,6 @@ defmodule ElixirLS.LanguageServer.Parser do end @impl true - def handle_call( - {:parse_immediate, uri, source_file = %SourceFile{}}, - _from, - %{files: files} = state - ) do - state = cancel_debounce(state, uri) - - # TODO cancel or wait for parse end? - - current_version = source_file.version - - {reply, state} = case files[uri] do - %Context{parsed_version: ^current_version} = file -> - Logger.debug("#{uri} already parsed") - # current version already parsed - {file, state} - - _other -> - Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") - # overwrite everything - file = - %Context{ - source_file: source_file, - path: get_path(uri) - } - |> do_parse() - - updated_files = Map.put(files, uri, file) - - notify_diagnostics_updated(updated_files) - - state = %{state | files: updated_files} - {file, state} - end - - {:reply, reply, state} - end - def handle_call( {:parse_immediate, uri, source_file = %SourceFile{}, position}, _from, @@ -200,7 +147,7 @@ defmodule ElixirLS.LanguageServer.Parser do {reply, state} = case files[uri] do %Context{parsed_version: ^current_version} = file -> - Logger.debug("#{uri} already parsed for cursor position #{inspect(position)}") + Logger.debug("#{uri} already parsed") file = maybe_fix_missing_env(file, position) updated_files = Map.put(files, uri, file) @@ -274,6 +221,7 @@ defmodule ElixirLS.LanguageServer.Parser do source_file.language_id in ["elixir", "eex", "html-eex"] end + defp maybe_fix_missing_env(%Context{} = file, nil), do: file defp maybe_fix_missing_env( %Context{metadata: metadata, flag: flag, source_file: source_file = %SourceFile{}} = file, From c7e5d061fb8644d8ab89b786951e8252cef1c1ed Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 30 Dec 2023 17:11:21 +0100 Subject: [PATCH 05/11] handle immediate requests asynchronously --- .../lib/language_server/parser.ex | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index b5e139b19..a0ec86930 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -136,7 +136,7 @@ defmodule ElixirLS.LanguageServer.Parser do @impl true def handle_call( {:parse_immediate, uri, source_file = %SourceFile{}, position}, - _from, + from, %{files: files} = state ) do state = cancel_debounce(state, uri) @@ -144,8 +144,9 @@ defmodule ElixirLS.LanguageServer.Parser do # TODO cancel or wait for parse end? current_version = source_file.version + parent = self() - {reply, state} = case files[uri] do + case files[uri] do %Context{parsed_version: ^current_version} = file -> Logger.debug("#{uri} already parsed") file = maybe_fix_missing_env(file, position) @@ -153,32 +154,27 @@ defmodule ElixirLS.LanguageServer.Parser do updated_files = Map.put(files, uri, file) # no change to diagnostics, only update stored metadata state = %{state | files: updated_files} - {file, state} + + {:reply, file, state} _other -> Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") - # overwrite everything - file = - %Context{ - source_file: source_file, - path: get_path(uri) - } - |> do_parse(position) - - # spawn(fn -> - # updated_file = do_parse(file) - # send(parent, {:parse_file_done, uri, updated_file}) - # end) - - updated_files = Map.put(files, uri, file) - - notify_diagnostics_updated(updated_files) - - state = %{state | files: updated_files} - {file, state} + + # TODO monitor, store from + + spawn(fn -> + # overwrite everything + updated_file = + %Context{ + source_file: source_file, + path: get_path(uri) + } + |> do_parse(position) + send(parent, {:parse_file_done, uri, updated_file, from}) + end) + + {:noreply, state} end - - {:reply, reply, state} end @impl GenServer @@ -195,7 +191,7 @@ defmodule ElixirLS.LanguageServer.Parser do spawn(fn -> updated_file = do_parse(file) - send(parent, {:parse_file_done, uri, updated_file}) + send(parent, {:parse_file_done, uri, updated_file, nil}) end) state = %{state | debounce_refs: Map.delete(debounce_refs, uri)} @@ -204,13 +200,15 @@ defmodule ElixirLS.LanguageServer.Parser do end def handle_info( - {:parse_file_done, uri, updated_file}, + {:parse_file_done, uri, updated_file, from}, %{files: files} = state ) do - updated_files = Map.put(files, uri, updated_file) + if from do + GenServer.reply(from, updated_file) + end + updated_files = Map.put(files, uri, updated_file) state = %{state | files: updated_files} - notify_diagnostics_updated(updated_files) {:noreply, state} From 8a330153480bfc5d6467394f20e02af8324ecabb Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 31 Dec 2023 11:35:46 +0100 Subject: [PATCH 06/11] store refs and respond on async process down --- .../lib/language_server/parser.ex | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index a0ec86930..f68feb2be 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -65,7 +65,7 @@ defmodule ElixirLS.LanguageServer.Parser do @impl true def init(_args) do # TODO get source files on start? - {:ok, %{files: %{}, debounce_refs: %{}, parse_pids: %{}}} + {:ok, %{files: %{}, debounce_refs: %{}, parse_pids: %{}, parse_uris: %{}}} end @impl GenServer @@ -159,10 +159,8 @@ defmodule ElixirLS.LanguageServer.Parser do _other -> Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") - - # TODO monitor, store from - spawn(fn -> + {pid, ref} = spawn_monitor(fn -> # overwrite everything updated_file = %Context{ @@ -173,7 +171,8 @@ defmodule ElixirLS.LanguageServer.Parser do send(parent, {:parse_file_done, uri, updated_file, from}) end) - {:noreply, state} + {:noreply, %{state | parse_pids: Map.put(state.parse_pids, uri, {pid, ref, from}), + parse_uris: Map.put(state.parse_uris, ref, uri)}} end end @@ -187,14 +186,13 @@ defmodule ElixirLS.LanguageServer.Parser do parent = self() - # TODO store pid, monitor? - - spawn(fn -> + {pid, ref} = spawn_monitor(fn -> updated_file = do_parse(file) send(parent, {:parse_file_done, uri, updated_file, nil}) end) - state = %{state | debounce_refs: Map.delete(debounce_refs, uri)} + state = %{state | debounce_refs: Map.delete(debounce_refs, uri), parse_pids: Map.put(state.parse_pids, uri, {pid, ref, nil}), + parse_uris: Map.put(state.parse_uris, ref, uri)} {:noreply, state} end @@ -214,6 +212,22 @@ defmodule ElixirLS.LanguageServer.Parser do {:noreply, state} end + def handle_info( + {:DOWN, ref, :process, pid, reason}, + %{parse_pids: parse_pids, parse_uris: parse_uris} = state + ) do + {uri, updated_parse_uris} = Map.pop!(parse_uris, ref) + {{^pid, ^ref, from}, updated_parse_pids} = Map.pop!(parse_pids, uri) + + if reason != :normal and from != nil do + GenServer.reply(from, :error) + end + + state = %{state | parse_pids: updated_parse_pids, parse_uris: updated_parse_uris} + + {:noreply, state} + end + defp should_parse?(uri, source_file) do String.ends_with?(uri, [".ex", ".exs", ".eex"]) or source_file.language_id in ["elixir", "eex", "html-eex"] From 41c0c5c44bf04bf0396f446385472cd7ca184c92 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 31 Dec 2023 11:47:04 +0100 Subject: [PATCH 07/11] cleanup error codes :parse_error is reserved for invalid JSON-RPC requests and not LSP errors --- .../lib/language_server/json_rpc.ex | 15 ++++++++++++--- .../providers/execute_command/apply_spec.ex | 4 ++-- .../providers/execute_command/manipulate_pipes.ex | 8 ++++---- .../providers/execute_command/mix_clean.ex | 2 +- .../language_server/lib/language_server/server.ex | 8 ++++---- .../execute_command/manipulate_pipes_test.exs | 4 ++-- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/apps/language_server/lib/language_server/json_rpc.ex b/apps/language_server/lib/language_server/json_rpc.ex index 918847002..95abf943f 100644 --- a/apps/language_server/lib/language_server/json_rpc.ex +++ b/apps/language_server/lib/language_server/json_rpc.ex @@ -226,16 +226,25 @@ defmodule ElixirLS.LanguageServer.JsonRpc do end end + # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#errorCodes + + # Defined by JSON-RPC defp error_code_and_message(:parse_error), do: {-32700, "Parse error"} defp error_code_and_message(:invalid_request), do: {-32600, "Invalid Request"} defp error_code_and_message(:method_not_found), do: {-32601, "Method not found"} defp error_code_and_message(:invalid_params), do: {-32602, "Invalid params"} defp error_code_and_message(:internal_error), do: {-32603, "Internal error"} - defp error_code_and_message(:server_error), do: {-32000, "Server error"} + + # -32099 - -32000 - JSON-RPC reserved error codes + # No LSP error codes should be defined between the start and end range. + # For backwards compatibility the `ServerNotInitialized` and the `UnknownErrorCode` + # are left in the range. defp error_code_and_message(:server_not_initialized), do: {-32002, "Server not initialized"} defp error_code_and_message(:unknown_error_code), do: {-32001, "Unknown error code"} - defp error_code_and_message(:request_cancelled), do: {-32800, "Request cancelled"} + # -32899 - -32800 - LSP reserved error codes + defp error_code_and_message(:request_failed), do: {-32803, "Request cancelled"} + defp error_code_and_message(:server_cancelled), do: {-32802, "Server cancelled"} defp error_code_and_message(:content_modified), do: {-32801, "Content modified"} - defp error_code_and_message(:code_lens_error), do: {-32900, "Error while building code lenses"} + defp error_code_and_message(:request_cancelled), do: {-32800, "Request cancelled"} end diff --git a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex index bd42753ab..7000dc2ed 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex @@ -96,11 +96,11 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do {:ok, nil} other -> - {:error, :server_error, + {:error, :request_failed, "cannot insert spec, workspace/applyEdit returned #{inspect(other)}", true} end else - {:error, :server_error, + {:error, :content_modified, "cannot insert spec, function definition has moved since suggestion was generated. " <> "Try again after file has been recompiled.", false} end diff --git a/apps/language_server/lib/language_server/providers/execute_command/manipulate_pipes.ex b/apps/language_server/lib/language_server/providers/execute_command/manipulate_pipes.ex index c36961480..7d072cbcd 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/manipulate_pipes.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/manipulate_pipes.ex @@ -52,16 +52,16 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ManipulatePipes do {:ok, nil} else {:error, :pipe_not_found} -> - {:error, :parse_error, "Pipe operator not found at cursor", false} + {:error, :request_failed, "Pipe operator not found at cursor", false} {:error, :function_call_not_found} -> - {:error, :parse_error, "Function call not found at cursor", false} + {:error, :request_failed, "Function call not found at cursor", false} {:error, :invalid_code} -> - {:error, :parse_error, "Malformed code", false} + {:error, :request_failed, "Malformed code", false} error -> - {:error, :server_error, + {:error, :request_failed, "Cannot execute pipe conversion, workspace/applyEdit returned #{inspect(error)}", true} end end diff --git a/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex b/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex index 570950374..011639258 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex @@ -5,7 +5,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixClean do def execute([clean_deps?], state) do case ElixirLS.LanguageServer.Build.clean(state.project_dir, clean_deps?) do :ok -> {:ok, %{}} - {:error, reason} -> {:error, :server_error, "Mix clean failed: #{inspect(reason)}", true} + {:error, reason} -> {:error, :request_failed, "Mix clean failed: #{inspect(reason)}", true} end end end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 7f8d02e86..c4876763d 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -407,7 +407,7 @@ defmodule ElixirLS.LanguageServer.Server do if id do {{^pid, ^ref, command, _start_time}, updated_requests} = Map.pop!(requests, id) error_msg = Exception.format_exit(reason) - JsonRpc.respond_with_error(id, :server_error, error_msg) + JsonRpc.respond_with_error(id, :internal_error, error_msg) do_sanity_check(state) @@ -415,7 +415,7 @@ defmodule ElixirLS.LanguageServer.Server do "lsp_request_error", %{ "elixir_ls.lsp_command" => String.replace(command, "/", "_"), - "elixir_ls.lsp_error" => :server_error, + "elixir_ls.lsp_error" => :internal_error, "elixir_ls.lsp_error_message" => error_msg }, %{} @@ -854,7 +854,7 @@ defmodule ElixirLS.LanguageServer.Server do kind, payload -> {payload, stacktrace} = Exception.blame(kind, payload, __STACKTRACE__) error_msg = Exception.format(kind, payload, stacktrace) - JsonRpc.respond_with_error(id, :server_error, error_msg) + JsonRpc.respond_with_error(id, :internal_error, error_msg) do_sanity_check(state) @@ -862,7 +862,7 @@ defmodule ElixirLS.LanguageServer.Server do "lsp_request_error", %{ "elixir_ls.lsp_command" => String.replace(command, "/", "_"), - "elixir_ls.lsp_error" => :server_error, + "elixir_ls.lsp_error" => :internal_error, "elixir_ls.lsp_error_message" => error_msg }, %{} diff --git a/apps/language_server/test/providers/execute_command/manipulate_pipes_test.exs b/apps/language_server/test/providers/execute_command/manipulate_pipes_test.exs index 45ac17edd..213ed5ce9 100644 --- a/apps/language_server/test/providers/execute_command/manipulate_pipes_test.exs +++ b/apps/language_server/test/providers/execute_command/manipulate_pipes_test.exs @@ -577,7 +577,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ManipulatePipesTest d assert_never_raises(text, uri, "toPipe") - assert {:error, :parse_error, "Function call not found at cursor", false} = + assert {:error, :request_failed, "Function call not found at cursor", false} = ManipulatePipes.execute( ["toPipe", uri, 4, 13], %Server{ @@ -1144,7 +1144,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ManipulatePipesTest d assert_never_raises(text, uri, "fromPipe") - assert {:error, :parse_error, "Pipe operator not found at cursor", false} = + assert {:error, :request_failed, "Pipe operator not found at cursor", false} = ManipulatePipes.execute( ["fromPipe", uri, 4, 16], %Server{ From ce06a2dae1f0dc1da0139b1455d062f3a4c478ae Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 31 Dec 2023 12:18:50 +0100 Subject: [PATCH 08/11] improve LSP compatibility - fill error.data on initialize error --- .../lib/language_server/json_rpc.ex | 18 +++++++- .../lib/language_server/server.ex | 41 +++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/apps/language_server/lib/language_server/json_rpc.ex b/apps/language_server/lib/language_server/json_rpc.ex index 95abf943f..59ccb80f0 100644 --- a/apps/language_server/lib/language_server/json_rpc.ex +++ b/apps/language_server/lib/language_server/json_rpc.ex @@ -67,6 +67,16 @@ defmodule ElixirLS.LanguageServer.JsonRpc do end end + defmacro error_response(id, code, message, data) do + quote do + %{ + "error" => %{"code" => unquote(code), "message" => unquote(message), "data" => unquote(data)}, + "id" => unquote(id), + "jsonrpc" => "2.0" + } + end + end + ## Utils def notify(method, params) do @@ -77,9 +87,13 @@ defmodule ElixirLS.LanguageServer.JsonRpc do WireProtocol.send(response(id, result)) end - def respond_with_error(id, type, message \\ nil) do + def respond_with_error(id, type, message \\ nil, data \\ nil) do {code, default_message} = error_code_and_message(type) - WireProtocol.send(error_response(id, code, message || default_message)) + if data do + WireProtocol.send(error_response(id, code, message || default_message, data)) + else + WireProtocol.send(error_response(id, code, message || default_message)) + end end def show_message(type, message) do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index c4876763d..3f36dda9b 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -748,15 +748,42 @@ defmodule ElixirLS.LanguageServer.Server do case packet do initialize_req(_id, _root_uri, _client_capabilities) -> - {:ok, result, state} = handle_request(packet, state) - elapsed = System.monotonic_time(:millisecond) - start_time - JsonRpc.respond(id, result) + try do + {:ok, result, state} = handle_request(packet, state) + elapsed = System.monotonic_time(:millisecond) - start_time + JsonRpc.respond(id, result) - JsonRpc.telemetry("lsp_request", %{"elixir_ls.lsp_command" => "initialize"}, %{ - "elixir_ls.lsp_request_time" => elapsed - }) + JsonRpc.telemetry("lsp_request", %{"elixir_ls.lsp_command" => "initialize"}, %{ + "elixir_ls.lsp_request_time" => elapsed + }) - state + state + catch + kind, payload -> + {payload, stacktrace} = Exception.blame(kind, payload, __STACKTRACE__) + error_msg = Exception.format(kind, payload, stacktrace) + + # on error in initialize the protocol requires to respond with + # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeError + # the initialize request can fail on broken OTP installs, no point in retrying + JsonRpc.respond_with_error(id, :internal_error, error_msg, %{ + "retry" => false + }) + + do_sanity_check(state) + + JsonRpc.telemetry( + "lsp_request_error", + %{ + "elixir_ls.lsp_command" => "initialize", + "elixir_ls.lsp_error" => :internal_error, + "elixir_ls.lsp_error_message" => error_msg + }, + %{} + ) + + state + end _ -> JsonRpc.respond_with_error(id, :server_not_initialized) From bb0715879ce968a65ae889acd6a48dc75a8432da Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 31 Dec 2023 13:49:27 +0100 Subject: [PATCH 09/11] include version in key --- .../lib/language_server/parser.ex | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index f68feb2be..03057facb 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -139,6 +139,7 @@ defmodule ElixirLS.LanguageServer.Parser do from, %{files: files} = state ) do + # TODO cancel if version greater? state = cancel_debounce(state, uri) # TODO cancel or wait for parse end? @@ -158,7 +159,7 @@ defmodule ElixirLS.LanguageServer.Parser do {:reply, file, state} _other -> - Logger.debug("Parsing #{uri} immediately: languageId #{source_file.language_id}") + Logger.debug("Parsing #{uri} version #{current_version} languageId #{source_file.language_id} immediately") {pid, ref} = spawn_monitor(fn -> # overwrite everything @@ -171,8 +172,8 @@ defmodule ElixirLS.LanguageServer.Parser do send(parent, {:parse_file_done, uri, updated_file, from}) end) - {:noreply, %{state | parse_pids: Map.put(state.parse_pids, uri, {pid, ref, from}), - parse_uris: Map.put(state.parse_uris, ref, uri)}} + {:noreply, %{state | parse_pids: Map.put(state.parse_pids, {uri, current_version}, {pid, ref, from}), + parse_uris: Map.put(state.parse_uris, ref, {uri, current_version})}} end end @@ -182,7 +183,8 @@ defmodule ElixirLS.LanguageServer.Parser do %{files: files, debounce_refs: debounce_refs} = state ) do file = Map.fetch!(files, uri) - Logger.debug("Parsing #{uri} after debounce: languageId #{file.source_file.language_id}") + version = file.source_file.version + Logger.debug("Parsing #{uri} version #{version} languageId #{file.source_file.language_id} after debounce") parent = self() @@ -191,8 +193,8 @@ defmodule ElixirLS.LanguageServer.Parser do send(parent, {:parse_file_done, uri, updated_file, nil}) end) - state = %{state | debounce_refs: Map.delete(debounce_refs, uri), parse_pids: Map.put(state.parse_pids, uri, {pid, ref, nil}), - parse_uris: Map.put(state.parse_uris, ref, uri)} + state = %{state | debounce_refs: Map.delete(debounce_refs, uri), parse_pids: Map.put(state.parse_pids, {uri, version}, {pid, ref, nil}), + parse_uris: Map.put(state.parse_uris, ref, {uri, version})} {:noreply, state} end @@ -216,8 +218,8 @@ defmodule ElixirLS.LanguageServer.Parser do {:DOWN, ref, :process, pid, reason}, %{parse_pids: parse_pids, parse_uris: parse_uris} = state ) do - {uri, updated_parse_uris} = Map.pop!(parse_uris, ref) - {{^pid, ^ref, from}, updated_parse_pids} = Map.pop!(parse_pids, uri) + {{uri, version}, updated_parse_uris} = Map.pop!(parse_uris, ref) + {{^pid, ^ref, from}, updated_parse_pids} = Map.pop!(parse_pids, {uri, version}) if reason != :normal and from != nil do GenServer.reply(from, :error) From 8839a42872b0843eeea46cb94084d637e8856b4f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 1 Jan 2024 10:41:18 +0100 Subject: [PATCH 10/11] enqueue parsing requests to the same uri --- .../lib/language_server/json_rpc.ex | 9 +- .../lib/language_server/parser.ex | 169 ++++++++++++------ 2 files changed, 124 insertions(+), 54 deletions(-) diff --git a/apps/language_server/lib/language_server/json_rpc.ex b/apps/language_server/lib/language_server/json_rpc.ex index 59ccb80f0..f45d7f651 100644 --- a/apps/language_server/lib/language_server/json_rpc.ex +++ b/apps/language_server/lib/language_server/json_rpc.ex @@ -70,7 +70,11 @@ defmodule ElixirLS.LanguageServer.JsonRpc do defmacro error_response(id, code, message, data) do quote do %{ - "error" => %{"code" => unquote(code), "message" => unquote(message), "data" => unquote(data)}, + "error" => %{ + "code" => unquote(code), + "message" => unquote(message), + "data" => unquote(data) + }, "id" => unquote(id), "jsonrpc" => "2.0" } @@ -89,6 +93,7 @@ defmodule ElixirLS.LanguageServer.JsonRpc do def respond_with_error(id, type, message \\ nil, data \\ nil) do {code, default_message} = error_code_and_message(type) + if data do WireProtocol.send(error_response(id, code, message || default_message, data)) else @@ -252,7 +257,7 @@ defmodule ElixirLS.LanguageServer.JsonRpc do # -32099 - -32000 - JSON-RPC reserved error codes # No LSP error codes should be defined between the start and end range. # For backwards compatibility the `ServerNotInitialized` and the `UnknownErrorCode` - # are left in the range. + # are left in the range. defp error_code_and_message(:server_not_initialized), do: {-32002, "Server not initialized"} defp error_code_and_message(:unknown_error_code), do: {-32001, "Unknown error code"} diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index 03057facb..69676026f 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -12,7 +12,7 @@ defmodule ElixirLS.LanguageServer.Parser do require Logger @debounce_timeout 300 - @parse_timeout 30_000 + @parse_timeout 120_000 @dummy_source "" @dummy_ast Code.string_to_quoted!(@dummy_source) @@ -42,7 +42,10 @@ defmodule ElixirLS.LanguageServer.Parser do if should_parse?(uri, source_file) do GenServer.cast(__MODULE__, {:parse_with_debounce, uri, source_file}) else - Logger.debug("Not parsing #{uri} with debounce: languageId #{source_file.language_id}") + Logger.debug( + "Not parsing #{uri} version #{source_file.version} languageId #{source_file.language_id} with debounce" + ) + :ok end end @@ -51,7 +54,10 @@ defmodule ElixirLS.LanguageServer.Parser do if should_parse?(uri, source_file) do GenServer.call(__MODULE__, {:parse_immediate, uri, source_file, position}, @parse_timeout) else - Logger.debug("Not parsing #{uri} immediately: languageId #{source_file.language_id}") + Logger.debug( + "Not parsing #{uri} version #{source_file.version} languageId #{source_file.language_id} immediately" + ) + # not parsing - respond with empty struct %Context{ source_file: source_file, @@ -65,7 +71,7 @@ defmodule ElixirLS.LanguageServer.Parser do @impl true def init(_args) do # TODO get source files on start? - {:ok, %{files: %{}, debounce_refs: %{}, parse_pids: %{}, parse_uris: %{}}} + {:ok, %{files: %{}, debounce_refs: %{}, parse_pids: %{}, parse_uris: %{}, queue: []}} end @impl GenServer @@ -109,7 +115,10 @@ defmodule ElixirLS.LanguageServer.Parser do {:noreply, %{state | files: updated_files}} end - def handle_cast({:parse_with_debounce, uri, source_file = %SourceFile{}}, state) do + def handle_cast( + {:parse_with_debounce, uri, source_file = %SourceFile{version: current_version}}, + state + ) do state = update_in(state.debounce_refs[uri], fn old_ref -> if old_ref do @@ -119,16 +128,18 @@ defmodule ElixirLS.LanguageServer.Parser do Process.send_after(self(), {:parse_file, uri}, @debounce_timeout) end) - state = update_in(state.files[uri], fn - nil -> - %Context{ - source_file: source_file, - path: get_path(uri) - } - - old_file -> - %Context{old_file | source_file: source_file} - end) + state = + update_in(state.files[uri], fn + nil -> + %Context{ + source_file: source_file, + path: get_path(uri) + } + + %Context{source_file: %SourceFile{version: old_version}} = old_file + when current_version > old_version -> + %Context{old_file | source_file: source_file} + end) {:noreply, state} end @@ -139,41 +150,60 @@ defmodule ElixirLS.LanguageServer.Parser do from, %{files: files} = state ) do - # TODO cancel if version greater? state = cancel_debounce(state, uri) - # TODO cancel or wait for parse end? - current_version = source_file.version parent = self() - case files[uri] do - %Context{parsed_version: ^current_version} = file -> - Logger.debug("#{uri} already parsed") + case {files[uri], Map.has_key?(state.parse_pids, {uri, current_version})} do + {%Context{parsed_version: ^current_version} = file, _} -> + Logger.debug( + "#{uri} version #{current_version} languageId #{source_file.language_id} already parsed" + ) + file = maybe_fix_missing_env(file, position) - updated_files = Map.put(files, uri, file) - # no change to diagnostics, only update stored metadata - state = %{state | files: updated_files} - {:reply, file, state} - _other -> - Logger.debug("Parsing #{uri} version #{current_version} languageId #{source_file.language_id} immediately") - - {pid, ref} = spawn_monitor(fn -> - # overwrite everything - updated_file = - %Context{ - source_file: source_file, - path: get_path(uri) - } - |> do_parse(position) - send(parent, {:parse_file_done, uri, updated_file, from}) - end) - - {:noreply, %{state | parse_pids: Map.put(state.parse_pids, {uri, current_version}, {pid, ref, from}), - parse_uris: Map.put(state.parse_uris, ref, {uri, current_version})}} + {_, true} -> + Logger.debug( + "#{uri} version #{current_version} languageId #{source_file.language_id} is currently being parsed" + ) + + state = %{state | queue: state.queue ++ [{{uri, current_version, position}, from}]} + {:noreply, state} + + {other, _} -> + Logger.debug( + "Parsing #{uri} version #{current_version} languageId #{source_file.language_id} immediately" + ) + + updated_file = + case other do + nil -> + %Context{ + source_file: source_file, + path: get_path(uri) + } + + %Context{source_file: %SourceFile{version: old_version}} = old_file + when old_version <= current_version -> + %Context{old_file | source_file: source_file} + end + + {pid, ref} = + spawn_monitor(fn -> + updated_file = do_parse(updated_file, position) + send(parent, {:parse_file_done, uri, updated_file, from}) + end) + + {:noreply, + %{ + state + | files: Map.put(files, uri, updated_file), + parse_pids: Map.put(state.parse_pids, {uri, current_version}, {pid, ref, from}), + parse_uris: Map.put(state.parse_uris, ref, {uri, current_version}) + }} end end @@ -184,17 +214,25 @@ defmodule ElixirLS.LanguageServer.Parser do ) do file = Map.fetch!(files, uri) version = file.source_file.version - Logger.debug("Parsing #{uri} version #{version} languageId #{file.source_file.language_id} after debounce") + + Logger.debug( + "Parsing #{uri} version #{version} languageId #{file.source_file.language_id} after debounce" + ) parent = self() - {pid, ref} = spawn_monitor(fn -> - updated_file = do_parse(file) - send(parent, {:parse_file_done, uri, updated_file, nil}) - end) + {pid, ref} = + spawn_monitor(fn -> + updated_file = do_parse(file) + send(parent, {:parse_file_done, uri, updated_file, nil}) + end) - state = %{state | debounce_refs: Map.delete(debounce_refs, uri), parse_pids: Map.put(state.parse_pids, {uri, version}, {pid, ref, nil}), - parse_uris: Map.put(state.parse_uris, ref, {uri, version})} + state = %{ + state + | debounce_refs: Map.delete(debounce_refs, uri), + parse_pids: Map.put(state.parse_pids, {uri, version}, {pid, ref, nil}), + parse_uris: Map.put(state.parse_uris, ref, {uri, version}) + } {:noreply, state} end @@ -207,11 +245,37 @@ defmodule ElixirLS.LanguageServer.Parser do GenServer.reply(from, updated_file) end - updated_files = Map.put(files, uri, updated_file) - state = %{state | files: updated_files} - notify_diagnostics_updated(updated_files) + parsed_file_version = updated_file.parsed_version - {:noreply, state} + state = + case files[uri] do + nil -> + # file got closed, no need to do anything + state + + %Context{source_file: %SourceFile{version: version}} when version > parsed_file_version -> + # result is from stale request, discard it + state + + _ -> + updated_files = Map.put(files, uri, updated_file) + notify_diagnostics_updated(updated_files) + %{state | files: updated_files} + end + + queue = + Enum.reduce(state.queue, [], fn + {{^uri, ^parsed_file_version, position}, from}, acc -> + file = maybe_fix_missing_env(updated_file, position) + GenServer.reply(from, file) + acc + + {request, from}, acc -> + [{request, from} | acc] + end) + |> Enum.reverse() + + {:noreply, %{state | queue: queue}} end def handle_info( @@ -236,6 +300,7 @@ defmodule ElixirLS.LanguageServer.Parser do end defp maybe_fix_missing_env(%Context{} = file, nil), do: file + defp maybe_fix_missing_env( %Context{metadata: metadata, flag: flag, source_file: source_file = %SourceFile{}} = file, From db00b0c638614f755f3e550c8722055cf704406a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 1 Jan 2024 16:50:38 +0100 Subject: [PATCH 11/11] fix a few edge cases --- .../lib/language_server/parser.ex | 48 ++++++++++++++----- .../lib/language_server/server.ex | 13 +++++ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/apps/language_server/lib/language_server/parser.ex b/apps/language_server/lib/language_server/parser.ex index 69676026f..e2aef5f4d 100644 --- a/apps/language_server/lib/language_server/parser.ex +++ b/apps/language_server/lib/language_server/parser.ex @@ -52,7 +52,15 @@ defmodule ElixirLS.LanguageServer.Parser do def parse_immediate(uri, source_file = %SourceFile{}, position \\ nil) do if should_parse?(uri, source_file) do - GenServer.call(__MODULE__, {:parse_immediate, uri, source_file, position}, @parse_timeout) + case GenServer.call( + __MODULE__, + {:parse_immediate, uri, source_file, position}, + @parse_timeout + ) do + :error -> raise "parser error" + :stale -> raise Server.ContentModifiedError, uri + %Context{} = context -> context + end else Logger.debug( "Not parsing #{uri} version #{source_file.version} languageId #{source_file.language_id} immediately" @@ -120,12 +128,16 @@ defmodule ElixirLS.LanguageServer.Parser do state ) do state = - update_in(state.debounce_refs[uri], fn old_ref -> - if old_ref do - Process.cancel_timer(old_ref, info: false) - end + update_in(state.debounce_refs[uri], fn + nil -> + {Process.send_after(self(), {:parse_file, uri}, @debounce_timeout), current_version} - Process.send_after(self(), {:parse_file, uri}, @debounce_timeout) + {old_ref, ^current_version} -> + {old_ref, current_version} + + {old_ref, old_version} when old_version < current_version -> + Process.cancel_timer(old_ref, info: false) + {Process.send_after(self(), {:parse_file, uri}, @debounce_timeout), current_version} end) state = @@ -139,6 +151,9 @@ defmodule ElixirLS.LanguageServer.Parser do %Context{source_file: %SourceFile{version: old_version}} = old_file when current_version > old_version -> %Context{old_file | source_file: source_file} + + %Context{source_file: %SourceFile{version: old_version}} = old_file -> + old_file end) {:noreply, state} @@ -150,8 +165,6 @@ defmodule ElixirLS.LanguageServer.Parser do from, %{files: files} = state ) do - state = cancel_debounce(state, uri) - current_version = source_file.version parent = self() @@ -173,14 +186,20 @@ defmodule ElixirLS.LanguageServer.Parser do state = %{state | queue: state.queue ++ [{{uri, current_version, position}, from}]} {:noreply, state} + {%Context{source_file: %SourceFile{version: old_version}}, _} + when old_version > current_version -> + {:reply, :stale, state} + {other, _} -> - Logger.debug( - "Parsing #{uri} version #{current_version} languageId #{source_file.language_id} immediately" - ) + state = cancel_debounce(state, uri) updated_file = case other do nil -> + Logger.debug( + "Parsing #{uri} version #{current_version} languageId #{source_file.language_id} immediately" + ) + %Context{ source_file: source_file, path: get_path(uri) @@ -188,6 +207,10 @@ defmodule ElixirLS.LanguageServer.Parser do %Context{source_file: %SourceFile{version: old_version}} = old_file when old_version <= current_version -> + Logger.debug( + "Parsing #{uri} version #{current_version} languageId #{source_file.language_id} immediately" + ) + %Context{old_file | source_file: source_file} end @@ -458,7 +481,8 @@ defmodule ElixirLS.LanguageServer.Parser do {maybe_ref, updated_debounce_refs} = Map.pop(debounce_refs, uri) if maybe_ref do - Process.cancel_timer(maybe_ref, info: false) + {ref, _version} = maybe_ref + Process.cancel_timer(ref, info: false) end %{state | debounce_refs: updated_debounce_refs} diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 3f36dda9b..94c028b22 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -91,6 +91,16 @@ defmodule ElixirLS.LanguageServer.Server do end end + defmodule ContentModifiedError do + defexception [:uri, :message] + + @impl true + def exception(uri) do + msg = "document URI: #{inspect(uri)} modified" + %ContentModifiedError{message: msg, uri: uri} + end + end + @default_watched_extensions [ ".ex", ".exs", @@ -1217,6 +1227,9 @@ defmodule ElixirLS.LanguageServer.Server do rescue e in InvalidParamError -> {:error, :invalid_params, e.message, true} + + e in ContentModifiedError -> + {:error, :content_modified, e.message, true} end GenServer.call(parent, {:request_finished, id, result}, :infinity)