From 482b6dc85bc2092d467a218cbbd338022e0d6c6e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 27 Sep 2023 23:08:21 +0200 Subject: [PATCH 01/20] fix warnings --- .../language_server/lib/language_server/providers/completion.ex | 2 +- .../lib/language_server/providers/on_type_formatting.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index e6f1263ab..62ad750c9 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -102,7 +102,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do character = SourceFile.lsp_character_to_elixir(line_text, character) text_before_cursor = String.slice(line_text, 0, character - 1) - text_after_cursor = String.slice(line_text, (character - 1)..-1) + text_after_cursor = String.slice(line_text, (character - 1)..-1//1) prefix = get_prefix(text_before_cursor) diff --git a/apps/language_server/lib/language_server/providers/on_type_formatting.ex b/apps/language_server/lib/language_server/providers/on_type_formatting.ex index 1c7d54b18..8f0315478 100644 --- a/apps/language_server/lib/language_server/providers/on_type_formatting.ex +++ b/apps/language_server/lib/language_server/providers/on_type_formatting.ex @@ -24,7 +24,7 @@ defmodule ElixirLS.LanguageServer.Providers.OnTypeFormatting do # Use contents and indentation of the next line to help us guess whether to insert an "end" indentation_suggests_edit? = if Enum.at(prev_tokens, -1) == "do" or "fn" in prev_tokens do - next_line = Enum.find(Enum.slice(lines, (line + 1)..-1), "", &(!blank?(&1))) + next_line = Enum.find(Enum.slice(lines, (line + 1)..-1//1), "", &(!blank?(&1))) next_tokens = tokens(next_line) next_indentation_length = String.length(indentation(next_line)) From 1dc1e1bddc0cba2e953626ae4d277185a521dd32 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 27 Sep 2023 23:08:48 +0200 Subject: [PATCH 02/20] Mix.Dep.load_on_environment no longer exists on 1.16 --- apps/language_server/lib/language_server/build.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 49fde3130..314457093 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -20,7 +20,11 @@ defmodule ElixirLS.LanguageServer.Build do with_diagnostics([log: true], fn -> try do # this call can raise - current_deps = Mix.Dep.load_on_environment([]) + current_deps = if Version.match?(System.version(), "< 1.16-dev") do + Mix.Dep.load_on_environment([]) + else + Mix.Dep.Converger.converge([]) + end purge_changed_deps(current_deps, cached_deps) From d76ff6f634e90e551e80dfc3c732a7bb444c5c15 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 21 Nov 2023 21:37:57 +0100 Subject: [PATCH 03/20] update elixir_sense api --- apps/language_server/lib/language_server/build.ex | 2 +- .../lib/language_server/providers/code_lens/test.ex | 2 +- .../lib/language_server/providers/completion.ex | 2 +- .../lib/language_server/providers/document_symbols.ex | 2 +- .../test/providers/document_symbols_test.exs | 6 ++++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 314457093..97823f000 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -20,7 +20,7 @@ defmodule ElixirLS.LanguageServer.Build do with_diagnostics([log: true], fn -> try do # this call can raise - current_deps = if Version.match?(System.version(), "< 1.16-dev") do + current_deps = if Version.match?(System.version(), "< 1.16.0-dev") do Mix.Dep.load_on_environment([]) else Mix.Dep.Converger.converge([]) diff --git a/apps/language_server/lib/language_server/providers/code_lens/test.ex b/apps/language_server/lib/language_server/providers/code_lens/test.ex index 2475d0b7b..2344f4e60 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -158,7 +158,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do defp parse_source(text) do buffer_file_metadata = text - |> Parser.parse_string(true, true, 1) + |> Parser.parse_string(true, true, {1, 1}) if buffer_file_metadata.error != nil do {:error, buffer_file_metadata} diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 62ad750c9..cbf8d2ae2 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -107,7 +107,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do prefix = get_prefix(text_before_cursor) # Can we use ElixirSense.Providers.Suggestion? ElixirSense.suggestions/3 - metadata = ElixirSense.Core.Parser.parse_string(text, true, true, line) + metadata = ElixirSense.Core.Parser.parse_string(text, true, true, {line, character}) env = ElixirSense.Core.Metadata.get_env(metadata, {line, character}) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 4b15d2261..1ce596cb3 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -49,7 +49,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end defp list_symbols(src) do - case ElixirSense.string_to_quoted(src, 1, @max_parser_errors, line: 1, token_metadata: true) do + case ElixirSense.string_to_quoted(src, {1, 1}, @max_parser_errors, line: 1, token_metadata: true) do {:ok, quoted_form} -> {:ok, extract_modules(quoted_form) |> Enum.reject(&is_nil/1)} {:error, _error} -> {:error, :compilation_error} end diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index bf7f1c3cf..d04ff7da5 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -2465,8 +2465,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end """ - assert {:error, :server_error, message, false} = DocumentSymbols.symbols(uri, text, true) - assert String.contains?(message, "Cannot parse source file") + assert { + :ok, + [%ElixirLS.LanguageServer.Protocol.DocumentSymbol{name: "A", kind: 2, range: %{"end" => %{"character" => 0, "line" => 5}, "start" => %{"character" => 0, "line" => 0}}, selectionRange: %{"end" => %{"character" => 11, "line" => 0}, "start" => %{"character" => 10, "line" => 0}}, children: [%ElixirLS.LanguageServer.Protocol.DocumentSymbol{name: "def hello", kind: 12, range: %{"end" => %{"character" => 3, "line" => 4}, "start" => %{"character" => 2, "line" => 1}}, selectionRange: %{"end" => %{"character" => 11, "line" => 1}, "start" => %{"character" => 6, "line" => 1}}, children: []}]}] + } = DocumentSymbols.symbols(uri, text, true) end test "returns def and defp as a prefix" do From c8a836e1c57f5fe42b8a090b3b8b5c72a2b7f13e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 22 Nov 2023 10:49:30 +0100 Subject: [PATCH 04/20] rescue MismatchedDelimiterError added in 1.16 --- apps/language_server/lib/language_server/server.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index cc6969f56..ba2161f86 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -2274,7 +2274,7 @@ defmodule ElixirLS.LanguageServer.Server do :ok rescue - e in [EEx.SyntaxError, SyntaxError, TokenMissingError] -> + e in [EEx.SyntaxError, SyntaxError, TokenMissingError, MismatchedDelimiterError] -> message = Exception.message(e) diagnostic = %Mix.Task.Compiler.Diagnostic{ From 0aaaf23cfc8809692106ab5e0f4481b5da2a0afe Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 24 Nov 2023 23:42:30 +0100 Subject: [PATCH 05/20] remove diagnostic message normalisation as it breaks 1.16 and 1.15 message format --- .../lib/language_server/diagnostics.ex | 57 ++++++------------ .../language_server/test/diagnostics_test.exs | 59 ++++++++----------- apps/language_server/test/server_test.exs | 12 ++-- 3 files changed, 49 insertions(+), 79 deletions(-) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 5b7ea1c20..a74f261ca 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -2,12 +2,11 @@ defmodule ElixirLS.LanguageServer.Diagnostics do alias ElixirLS.LanguageServer.{SourceFile, JsonRpc} def normalize(diagnostics, root_path, mixfile) do - for diagnostic <- diagnostics do - {type, file, position, description, stacktrace} = + for %Mix.Task.Compiler.Diagnostic{} = diagnostic <- diagnostics do + {type, file, position, stacktrace} = extract_message_info(diagnostic.message, root_path) diagnostic - |> update_message(type, description, stacktrace) |> maybe_update_file(file, mixfile) |> maybe_update_position(type, position, stacktrace) end @@ -26,32 +25,9 @@ defmodule ElixirLS.LanguageServer.Diagnostics do stacktrace = reversed_stacktrace |> Enum.map(&String.trim/1) |> Enum.reverse() {type, message_without_type} = split_type_and_message(message) - {file, position, description} = split_file_and_description(message_without_type, root_path) + {file, position} = get_file_and_position(message_without_type, root_path) - {type, file, position, description, stacktrace} - end - - defp update_message(diagnostic, type, description, stacktrace) do - description = - if type do - "(#{type}) #{description}" - else - description - end - - message = - if stacktrace != [] do - stacktrace = - stacktrace - |> Enum.map_join("\n", &" │ #{&1}") - |> String.trim_trailing() - - description <> "\n\n" <> "Stacktrace:\n" <> stacktrace - else - description - end - - Map.put(diagnostic, :message, message) + {type, file, position, stacktrace} end defp maybe_update_file(diagnostic, path, mixfile) do @@ -67,6 +43,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp maybe_update_position(diagnostic, "TokenMissingError", position, stacktrace) do + # TODO handle line:char? case extract_line_from_missing_hint(diagnostic.message) do line when is_integer(line) and line > 0 -> %{diagnostic | position: line} @@ -104,8 +81,17 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end end - defp split_file_and_description(message, root_path) do - with {file, line, column, description} <- get_message_parts(message), + defp get_file_and_position(message, root_path) do + # this regex won't match filenames with spaces but in elixir 1.16 errors we can't be sure where + # the file name starts e.g. + # invalid syntax found on lib/template.eex:2:5: + file_position = case Regex.run(~r/([^\s:]+):(\d+)(:(\d+))?/su, message) do + [_, file, line] -> {file, line, ""} + [_, file, line, _, column] -> {file, line, column} + _ -> nil + end + + with {file, line, column} <- file_position, {:ok, path} <- file_path(file, root_path) do line = String.to_integer(line) @@ -116,17 +102,10 @@ defmodule ElixirLS.LanguageServer.Diagnostics do true -> {line, String.to_integer(column)} end - {path, position, description} + {path, position} else _ -> - {nil, nil, message} - end - end - - defp get_message_parts(message) do - case Regex.run(~r/^(.*?):(\d+)(:(\d+))?: (.*)/su, message) do - [_, file, line, _, column, description] -> {file, line, column, description} - _ -> nil + {nil, nil} end end diff --git a/apps/language_server/test/diagnostics_test.exs b/apps/language_server/test/diagnostics_test.exs index 90a4144f4..f148ac36d 100644 --- a/apps/language_server/test/diagnostics_test.exs +++ b/apps/language_server/test/diagnostics_test.exs @@ -22,19 +22,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [diagnostic | _] = [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - - assert diagnostic.message == """ - (CompileError) some message - - Hint: Some hint - - Stacktrace: - │ (elixir 1.10.1) lib/macro.ex:304: Macro.pipe/3 - │ (stdlib 3.7.1) lists.erl:1263: :lists.foldl/3 - │ (elixir 1.10.1) expanding macro: Kernel.|>/2 - │ expanding macro: SomeModule.sigil_L/2 - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ end test "update file and position if file is present in the message" do @@ -51,14 +38,33 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message == """ - (CompileError) some message + assert diagnostic.position == 3 + end - Stacktrace: - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ + test "update file and position if file is present in the message - 1.16 format" do + root_path = Path.join(__DIR__, "fixtures/build_errors_on_external_resource") + file = Path.join(root_path, "lib/has_error.ex") + position = 2 - assert diagnostic.position == 3 + message = """ + ** (SyntaxError) invalid syntax found on lib/template.eex:2:5: + error: syntax error before: ',' + │ + 2 │ , + │ ^ + │ + └─ lib/template.eex:2:5 + (eex 1.16.0-rc.0) lib/eex/compiler.ex:332: EEx.Compiler.generate_buffer/4 + lib/has_error.ex:2: (module) + (elixir 1.16.0-rc.0) lib/kernel/parallel_compiler.ex:428: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/8 + """ + + [diagnostic | _] = + [build_diagnostic(message, file, position)] + |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) + + assert diagnostic.position == {2, 5} + assert diagnostic.file == Path.join(root_path, "lib/template.eex") end test "update file and position with column if file is present in the message" do @@ -75,13 +81,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message == """ - (CompileError) some message - - Stacktrace: - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ - assert diagnostic.position == {3, 5} end @@ -100,7 +99,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message =~ "(CompileError) some message" assert diagnostic.file =~ "umbrella/apps/app2/lib/app2.ex" assert diagnostic.position == 5 end @@ -119,13 +117,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message == """ - (CompileError) lib/non_existing.ex:3: some message - - Stacktrace: - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ - assert diagnostic.position == 2 end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 2defe2256..791857ad5 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1661,15 +1661,15 @@ defmodule ElixirLS.LanguageServer.ServerTest do "diagnostics" => [ %{ "message" => - "(CompileError) lib/has_error.ex: cannot compile module" <> _, + "** (CompileError) lib/has_error.ex: cannot compile module" <> _, "range" => %{"end" => %{"line" => 0}, "start" => %{"line" => 0}}, "severity" => 1 }, %{ "message" => "undefined function does_not_exist/0" <> _, "range" => %{ - "end" => %{"character" => 4, "line" => 3}, - "start" => %{"character" => 4, "line" => 3} + "end" => %{"character" => _, "line" => 3}, + "start" => %{"character" => _, "line" => 3} }, "severity" => 1, "source" => "Elixir" @@ -1706,8 +1706,8 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "(TokenMissingError) missing terminator: end" <> _, - "range" => %{"end" => %{"line" => 5}, "start" => %{"line" => 5}}, + "message" => "** (TokenMissingError)" <> _, + "range" => %{"end" => %{"line" => _}, "start" => %{"line" => _}}, "severity" => 1 } ] @@ -1729,7 +1729,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "(SyntaxError) syntax error before: ','" <> _, + "message" => "** (SyntaxError)" <> _, "range" => %{"end" => %{"line" => 1}, "start" => %{"line" => 1}}, "severity" => 1 } From dcf4a873a167a2a77eaeed85e4b58a6115224c91 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 Nov 2023 00:15:20 +0100 Subject: [PATCH 06/20] handle updated message format --- .../lib/language_server/diagnostics.ex | 3 +-- .../language_server/test/diagnostics_test.exs | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index a74f261ca..74904d275 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -43,7 +43,6 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp maybe_update_position(diagnostic, "TokenMissingError", position, stacktrace) do - # TODO handle line:char? case extract_line_from_missing_hint(diagnostic.message) do line when is_integer(line) and line > 0 -> %{diagnostic | position: line} @@ -143,7 +142,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do defp extract_line_from_missing_hint(message) do case Regex.run( - ~r/HINT: it looks like the .+ on line (\d+) does not have a matching /u, + ~r/starting at line (\d+)\)/u, message ) do [_, line] -> String.to_integer(line) diff --git a/apps/language_server/test/diagnostics_test.exs b/apps/language_server/test/diagnostics_test.exs index f148ac36d..fc8590c47 100644 --- a/apps/language_server/test/diagnostics_test.exs +++ b/apps/language_server/test/diagnostics_test.exs @@ -19,7 +19,7 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do lib/my_app/my_module.ex:10: MyApp.MyModule.render/1 """ - [diagnostic | _] = + [_diagnostic | _] = [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) end @@ -168,6 +168,24 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do assert diagnostic.position == 6 end + test "if position is nil and error is TokenMissingError, try to retrieve from the hint - 1.16 format" do + root_path = Path.join(__DIR__, "fixtures/token_missing_error") + file = Path.join(root_path, "lib/has_error.ex") + position = nil + + message = """ + ** (TokenMissingError) token missing on lib/has_error.ex:16:1: + error: missing terminator: end (for "fn" starting at line 6) + └─ lib/has_error.ex:16:1 + """ + + [diagnostic | _] = + [build_diagnostic(message, file, position)] + |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) + + assert diagnostic.position == 6 + end + defp build_diagnostic(message, file, position) do %Mix.Task.Compiler.Diagnostic{ compiler_name: "Elixir", From bc191ce07ee7ccbc3eda16012912db2a4cb36959 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 Nov 2023 00:43:16 +0100 Subject: [PATCH 07/20] sanitize paths passed to wildcards --- .../lib/language_server/diagnostics.ex | 2 +- .../lib/language_server/dialyzer.ex | 2 +- .../lib/language_server/dialyzer/manifest.ex | 2 +- .../lib/language_server/source_file/path.ex | 16 ++++++++++++++++ .../lib/language_server/tracer.ex | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 74904d275..84c7350d9 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -119,7 +119,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp file_path_in_umbrella(file, root_path) do - case [root_path, "apps", "*", file] |> Path.join() |> Path.wildcard() do + case [SourceFile.Path.escape_for_wildcard(root_path), "apps", "*", SourceFile.Path.escape_for_wildcard(file)] |> Path.join() |> Path.wildcard() do [] -> {:error, :file_not_found} diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index 5d80f888f..18e82e308 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -452,7 +452,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end temp_modules = - for file <- Path.wildcard(temp_file_path(root_path, "**/*.beam")), into: %{} do + for file <- Path.wildcard(temp_file_path(SourceFile.Path.escape_for_wildcard(root_path), "**/*.beam")), into: %{} do {String.to_atom(Path.basename(file, ".beam")), to_charlist(file)} end diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index eed0be0a3..5dc7f65e5 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -166,7 +166,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do modules_to_paths = for app <- @erlang_apps ++ @elixir_apps, - path <- Path.join([Application.app_dir(app), "**/*.beam"]) |> Path.wildcard(), + path <- Path.join([SourceFile.Path.escape_for_wildcard(Application.app_dir(app)), "**/*.beam"]) |> Path.wildcard(), into: %{}, do: {pathname_to_module(path), path |> String.to_charlist()} diff --git a/apps/language_server/lib/language_server/source_file/path.ex b/apps/language_server/lib/language_server/source_file/path.ex index 7fe9cccb0..6e055e2d1 100644 --- a/apps/language_server/lib/language_server/source_file/path.ex +++ b/apps/language_server/lib/language_server/source_file/path.ex @@ -161,6 +161,22 @@ defmodule ElixirLS.LanguageServer.SourceFile.Path do end end + def escape_for_wildcard(path) when is_list(path), do: escape_for_wildcard(to_string(path)) + def escape_for_wildcard(path) when is_binary(path) do + # Path.wildcard expects universal separators even on windows + # escape all special chars + path + |> convert_separators_to_universal() + |> String.replace("\\", "\\\\") + |> String.replace("?", "\\?") + |> String.replace("*", "\\*") + |> String.replace("{", "\\{") + |> String.replace("}", "\\}") + |> String.replace("[", "\\[") + |> String.replace("]", "\\]") + |> String.replace(",", "\\,") + end + # the functions below are copied from elixir project # https://github.com/lukaszsamson/elixir/blob/bf3e2fd3ad78235bda059b80994a90d9a4184353/lib/elixir/lib/path.ex # with applied https://github.com/elixir-lang/elixir/pull/13061 diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index d7a09eb41..863c9b37d 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -500,7 +500,7 @@ defmodule ElixirLS.LanguageServer.Tracer do def clean_dets(project_dir) do for path <- - Path.join([project_dir, ".elixir_ls/*.dets"]) + Path.join([SourceFile.Path.escape_for_wildcard(project_dir), ".elixir_ls/*.dets"]) |> Path.wildcard(), do: File.rm_rf!(path) end From 60371a7703a515c76a6a356c4196713c137895c2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 Nov 2023 22:25:57 +0100 Subject: [PATCH 08/20] fix dialyzer error --- apps/language_server/lib/language_server/source_file/path.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/source_file/path.ex b/apps/language_server/lib/language_server/source_file/path.ex index 6e055e2d1..add07abaf 100644 --- a/apps/language_server/lib/language_server/source_file/path.ex +++ b/apps/language_server/lib/language_server/source_file/path.ex @@ -205,7 +205,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.Path do absname(path, &File.cwd!/0) end - @spec absname(t, t) :: binary + @spec absname(t, t | (-> t)) :: binary def absname(path, relative_to) do path = IO.chardata_to_string(path) From c25f9993b7ed7aaea77f560225b1377c00952abf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 Nov 2023 22:26:43 +0100 Subject: [PATCH 09/20] format --- .../lib/language_server/build.ex | 11 +++--- .../lib/language_server/diagnostics.ex | 20 +++++++---- .../lib/language_server/dialyzer.ex | 6 +++- .../lib/language_server/dialyzer/manifest.ex | 4 ++- .../providers/document_symbols.ex | 5 ++- .../lib/language_server/source_file/path.ex | 1 + .../test/providers/document_symbols_test.exs | 34 +++++++++++++++++-- 7 files changed, 64 insertions(+), 17 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 97823f000..39ae346e6 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -20,11 +20,12 @@ defmodule ElixirLS.LanguageServer.Build do with_diagnostics([log: true], fn -> try do # this call can raise - current_deps = if Version.match?(System.version(), "< 1.16.0-dev") do - Mix.Dep.load_on_environment([]) - else - Mix.Dep.Converger.converge([]) - end + current_deps = + if Version.match?(System.version(), "< 1.16.0-dev") do + Mix.Dep.load_on_environment([]) + else + Mix.Dep.Converger.converge([]) + end purge_changed_deps(current_deps, cached_deps) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 84c7350d9..d0669cc66 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -84,11 +84,12 @@ defmodule ElixirLS.LanguageServer.Diagnostics do # this regex won't match filenames with spaces but in elixir 1.16 errors we can't be sure where # the file name starts e.g. # invalid syntax found on lib/template.eex:2:5: - file_position = case Regex.run(~r/([^\s:]+):(\d+)(:(\d+))?/su, message) do - [_, file, line] -> {file, line, ""} - [_, file, line, _, column] -> {file, line, column} - _ -> nil - end + file_position = + case Regex.run(~r/([^\s:]+):(\d+)(:(\d+))?/su, message) do + [_, file, line] -> {file, line, ""} + [_, file, line, _, column] -> {file, line, column} + _ -> nil + end with {file, line, column} <- file_position, {:ok, path} <- file_path(file, root_path) do @@ -119,7 +120,14 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp file_path_in_umbrella(file, root_path) do - case [SourceFile.Path.escape_for_wildcard(root_path), "apps", "*", SourceFile.Path.escape_for_wildcard(file)] |> Path.join() |> Path.wildcard() do + case [ + SourceFile.Path.escape_for_wildcard(root_path), + "apps", + "*", + SourceFile.Path.escape_for_wildcard(file) + ] + |> Path.join() + |> Path.wildcard() do [] -> {:error, :file_not_found} diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index 18e82e308..3c222a328 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -452,7 +452,11 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end temp_modules = - for file <- Path.wildcard(temp_file_path(SourceFile.Path.escape_for_wildcard(root_path), "**/*.beam")), into: %{} do + for file <- + Path.wildcard( + temp_file_path(SourceFile.Path.escape_for_wildcard(root_path), "**/*.beam") + ), + into: %{} do {String.to_atom(Path.basename(file, ".beam")), to_charlist(file)} end diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index 5dc7f65e5..bc02b4bad 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -166,7 +166,9 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do modules_to_paths = for app <- @erlang_apps ++ @elixir_apps, - path <- Path.join([SourceFile.Path.escape_for_wildcard(Application.app_dir(app)), "**/*.beam"]) |> Path.wildcard(), + path <- + Path.join([SourceFile.Path.escape_for_wildcard(Application.app_dir(app)), "**/*.beam"]) + |> Path.wildcard(), into: %{}, do: {pathname_to_module(path), path |> String.to_charlist()} diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 1ce596cb3..b5fad67e3 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -49,7 +49,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end defp list_symbols(src) do - case ElixirSense.string_to_quoted(src, {1, 1}, @max_parser_errors, line: 1, token_metadata: true) do + case ElixirSense.string_to_quoted(src, {1, 1}, @max_parser_errors, + line: 1, + token_metadata: true + ) do {:ok, quoted_form} -> {:ok, extract_modules(quoted_form) |> Enum.reject(&is_nil/1)} {:error, _error} -> {:error, :compilation_error} end diff --git a/apps/language_server/lib/language_server/source_file/path.ex b/apps/language_server/lib/language_server/source_file/path.ex index add07abaf..73cf0dec0 100644 --- a/apps/language_server/lib/language_server/source_file/path.ex +++ b/apps/language_server/lib/language_server/source_file/path.ex @@ -162,6 +162,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.Path do end def escape_for_wildcard(path) when is_list(path), do: escape_for_wildcard(to_string(path)) + def escape_for_wildcard(path) when is_binary(path) do # Path.wildcard expects universal separators even on windows # escape all special chars diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index d04ff7da5..bff485dd3 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -2466,9 +2466,37 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do """ assert { - :ok, - [%ElixirLS.LanguageServer.Protocol.DocumentSymbol{name: "A", kind: 2, range: %{"end" => %{"character" => 0, "line" => 5}, "start" => %{"character" => 0, "line" => 0}}, selectionRange: %{"end" => %{"character" => 11, "line" => 0}, "start" => %{"character" => 10, "line" => 0}}, children: [%ElixirLS.LanguageServer.Protocol.DocumentSymbol{name: "def hello", kind: 12, range: %{"end" => %{"character" => 3, "line" => 4}, "start" => %{"character" => 2, "line" => 1}}, selectionRange: %{"end" => %{"character" => 11, "line" => 1}, "start" => %{"character" => 6, "line" => 1}}, children: []}]}] - } = DocumentSymbols.symbols(uri, text, true) + :ok, + [ + %ElixirLS.LanguageServer.Protocol.DocumentSymbol{ + name: "A", + kind: 2, + range: %{ + "end" => %{"character" => 0, "line" => 5}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 11, "line" => 0}, + "start" => %{"character" => 10, "line" => 0} + }, + children: [ + %ElixirLS.LanguageServer.Protocol.DocumentSymbol{ + name: "def hello", + kind: 12, + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 2, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 11, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + children: [] + } + ] + } + ] + } = DocumentSymbols.symbols(uri, text, true) end test "returns def and defp as a prefix" do From 039e89efa97e73745e059aa68a532be95d3ffd0e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Nov 2023 10:55:50 +0100 Subject: [PATCH 10/20] fix warning --- apps/elixir_ls_debugger/lib/debugger/server.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index ef1abe88b..e0aee1adf 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -1028,7 +1028,7 @@ defmodule ElixirLS.Debugger.Server do _ -> -1 end - stack_frames = Enum.slice(paused_process.stack, start_frame..end_frame) + stack_frames = Enum.slice(paused_process.stack, start_frame..end_frame//1) {state, frame_ids} = ensure_frame_ids(state, pid, stack_frames) stack_frames_json = From 1338c8964b0ec2f4b6dc64cd873915dc65daa863 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Nov 2023 10:56:04 +0100 Subject: [PATCH 11/20] fix test --- apps/language_server/test/diagnostics_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/test/diagnostics_test.exs b/apps/language_server/test/diagnostics_test.exs index fc8590c47..8b9295055 100644 --- a/apps/language_server/test/diagnostics_test.exs +++ b/apps/language_server/test/diagnostics_test.exs @@ -165,7 +165,7 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.position == 6 + assert diagnostic.position == 1 end test "if position is nil and error is TokenMissingError, try to retrieve from the hint - 1.16 format" do From 0e0590aadb942e6d44078cca3ac345dfb9593abc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Nov 2023 10:56:35 +0100 Subject: [PATCH 12/20] fix flaky test revert group leader change --- apps/elixir_ls_debugger/test/debugger_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 9d175b134..c31afdd8f 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -12,6 +12,7 @@ defmodule ElixirLS.Debugger.ServerTest do setup do {:ok, packet_capture} = ElixirLS.Utils.PacketCapture.start_link(self()) + default_group_leader = Process.info(Process.whereis(ElixirLS.Debugger.Output))[:group_leader] Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), packet_capture) {:ok, _} = start_supervised(BreakpointCondition) @@ -19,6 +20,7 @@ defmodule ElixirLS.Debugger.ServerTest do {:ok, server} = Server.start_link(name: Server) on_exit(fn -> + Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), default_group_leader) for mod <- :int.interpreted(), do: :int.nn(mod) :int.auto_attach(false) :int.no_break() From eef2e0d2df37d409738e255e11cfc43ecccb5c4c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Nov 2023 11:18:42 +0100 Subject: [PATCH 13/20] add missing alias --- apps/language_server/lib/language_server/dialyzer/manifest.ex | 2 +- apps/language_server/lib/language_server/tracer.ex | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index bc02b4bad..c93913d8f 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do - alias ElixirLS.LanguageServer.{Dialyzer, Dialyzer.Utils, JsonRpc} + alias ElixirLS.LanguageServer.{Dialyzer, Dialyzer.Utils, JsonRpc, SourceFile} import Record import Dialyzer.Utils require Logger diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 863c9b37d..2b52c53bc 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -3,6 +3,7 @@ defmodule ElixirLS.LanguageServer.Tracer do """ use GenServer alias ElixirLS.LanguageServer.JsonRpc + alias ElixirLS.LanguageServer.SourceFile require Logger @version 3 From 5ebfddc68e7b70e11d78232d531a1cf675471eba Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Nov 2023 22:58:43 +0100 Subject: [PATCH 14/20] fix race conditions during config reload project reload wasn't under build lock and it could execute in parallel with build this made the tests flaky --- .../lib/language_server/build.ex | 30 +++++++++++++++---- .../providers/execute_command/mix_clean.ex | 4 +-- .../lib/language_server/server.ex | 24 +++++++++------ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 39ae346e6..ea074e0fa 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -4,7 +4,7 @@ defmodule ElixirLS.LanguageServer.Build do require Logger def build(parent, root_path, opts) when is_binary(root_path) do - spawn_monitor(fn -> + build_pid_reference = spawn_monitor(fn -> with_build_lock(fn -> {us, result} = :timer.tc(fn -> @@ -129,12 +129,32 @@ defmodule ElixirLS.LanguageServer.Build do }) end) end) + + spawn(fn -> + Process.monitor(parent) + {build_process, _ref} = build_pid_reference + Process.monitor(build_process) + + receive do + {:DOWN, _ref, _, ^build_process, _reason} -> + :ok + {:DOWN, _ref, _, ^parent, _reason} -> + Process.exit(build_process, :kill) + end + end) + + build_pid_reference end - def clean(clean_deps? \\ false) do + def clean(root_path, clean_deps? \\ false) when is_binary(root_path) do with_build_lock(fn -> - Mix.Task.clear() - run_mix_clean(clean_deps?) + mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path) + case reload_project(mixfile, root_path) do + {:ok, _} -> + Mix.Task.clear() + run_mix_clean(clean_deps?) + other -> other + end end) end @@ -142,7 +162,7 @@ defmodule ElixirLS.LanguageServer.Build do :global.trans({__MODULE__, self()}, func) end - def reload_project(mixfile, root_path) do + defp reload_project(mixfile, root_path) do if File.exists?(mixfile) do if module = Mix.Project.get() do build_path = Mix.Project.config()[:build_path] 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 838889ab0..570950374 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 @@ -2,8 +2,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixClean do @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand @impl ElixirLS.LanguageServer.Providers.ExecuteCommand - def execute([clean_deps?], _state) do - case ElixirLS.LanguageServer.Build.clean(clean_deps?) 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} end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index ba2161f86..d0c58b7d2 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -364,7 +364,11 @@ defmodule ElixirLS.LanguageServer.Server do handle_build_result(:error, [Diagnostics.exception_to_diagnostic(reason, path)], state) end - state = if state.needs_build?, do: trigger_build(state), else: state + state = if state.needs_build? do + trigger_build(state) + else + state + end {:noreply, state} end @@ -738,7 +742,11 @@ defmodule ElixirLS.LanguageServer.Server do |> Enum.map(& &1["uri"]) |> WorkspaceSymbols.notify_uris_modified() - if needs_build, do: trigger_build(state), else: state + if needs_build do + trigger_build(state) + else + state + end end defp handle_notification(did_change_watched_files(_changes), state = %__MODULE__{}) do @@ -2128,15 +2136,13 @@ defmodule ElixirLS.LanguageServer.Server do case File.cwd() do {:ok, cwd} -> if SourceFile.Path.absname(cwd) == SourceFile.Path.absname(project_dir) do - mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs()) - try do - case Build.reload_project(mixfile, project_dir) do - {:ok, _} -> - Build.clean(true) + case Build.clean(project_dir) do + :ok -> + :ok - _ -> - # TODO emit diagnostics here? + e -> + Logger.warn("Unable to clean project, databases may not be up to date: #{inspect(e)}") :ok end rescue From d245c25dc45dc462dc0f8da1695c8e07b0860671 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Nov 2023 23:00:30 +0100 Subject: [PATCH 15/20] format on 1.15 --- .../lib/language_server/build.ex | 227 +++++++++--------- .../lib/language_server/server.ex | 17 +- 2 files changed, 127 insertions(+), 117 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index ea074e0fa..a28346ce3 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -4,131 +4,132 @@ defmodule ElixirLS.LanguageServer.Build do require Logger def build(parent, root_path, opts) when is_binary(root_path) do - build_pid_reference = spawn_monitor(fn -> - with_build_lock(fn -> - {us, result} = - :timer.tc(fn -> - Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}") - - # read cache before cleaning up mix state in reload_project - cached_deps = read_cached_deps() - mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path) - - case reload_project(mixfile, root_path) do - {:ok, mixfile_diagnostics} -> - {deps_result, deps_raw_diagnostics} = - with_diagnostics([log: true], fn -> - try do - # this call can raise - current_deps = - if Version.match?(System.version(), "< 1.16.0-dev") do - Mix.Dep.load_on_environment([]) - else - Mix.Dep.Converger.converge([]) + build_pid_reference = + spawn_monitor(fn -> + with_build_lock(fn -> + {us, result} = + :timer.tc(fn -> + Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}") + + # read cache before cleaning up mix state in reload_project + cached_deps = read_cached_deps() + mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path) + + case reload_project(mixfile, root_path) do + {:ok, mixfile_diagnostics} -> + {deps_result, deps_raw_diagnostics} = + with_diagnostics([log: true], fn -> + try do + # this call can raise + current_deps = + if Version.match?(System.version(), "< 1.16.0-dev") do + Mix.Dep.load_on_environment([]) + else + Mix.Dep.Converger.converge([]) + end + + purge_changed_deps(current_deps, cached_deps) + + if Keyword.get(opts, :fetch_deps?) and current_deps != cached_deps do + fetch_deps(current_deps) end - purge_changed_deps(current_deps, cached_deps) - - if Keyword.get(opts, :fetch_deps?) and current_deps != cached_deps do - fetch_deps(current_deps) + state = %{ + get: Mix.Project.get(), + # project_file: Mix.Project.project_file(), + config: Mix.Project.config(), + # config_files: Mix.Project.config_files(), + config_mtime: Mix.Project.config_mtime(), + umbrella?: Mix.Project.umbrella?(), + apps_paths: Mix.Project.apps_paths(), + # deps_path: Mix.Project.deps_path(), + # deps_apps: Mix.Project.deps_apps(), + # deps_scms: Mix.Project.deps_scms(), + deps_paths: Mix.Project.deps_paths(), + # build_path: Mix.Project.build_path(), + manifest_path: Mix.Project.manifest_path() + } + + ElixirLS.LanguageServer.MixProject.store(state) + + :ok + catch + kind, err -> + {payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__) + {:error, kind, payload, stacktrace} end + end) - state = %{ - get: Mix.Project.get(), - # project_file: Mix.Project.project_file(), - config: Mix.Project.config(), - # config_files: Mix.Project.config_files(), - config_mtime: Mix.Project.config_mtime(), - umbrella?: Mix.Project.umbrella?(), - apps_paths: Mix.Project.apps_paths(), - # deps_path: Mix.Project.deps_path(), - # deps_apps: Mix.Project.deps_apps(), - # deps_scms: Mix.Project.deps_scms(), - deps_paths: Mix.Project.deps_paths(), - # build_path: Mix.Project.build_path(), - manifest_path: Mix.Project.manifest_path() - } - - ElixirLS.LanguageServer.MixProject.store(state) - - :ok - catch - kind, err -> - {payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__) - {:error, kind, payload, stacktrace} - end - end) - - deps_diagnostics = - deps_raw_diagnostics - |> Enum.map(&Diagnostics.code_diagnostic/1) - - case deps_result do - :ok -> - if Keyword.get(opts, :compile?) do - {status, compile_diagnostics} = run_mix_compile() - - compile_diagnostics = - Diagnostics.normalize(compile_diagnostics, root_path, mixfile) + deps_diagnostics = + deps_raw_diagnostics + |> Enum.map(&Diagnostics.code_diagnostic/1) - Server.build_finished( - parent, - {status, mixfile_diagnostics ++ deps_diagnostics ++ compile_diagnostics} - ) + case deps_result do + :ok -> + if Keyword.get(opts, :compile?) do + {status, compile_diagnostics} = run_mix_compile() + + compile_diagnostics = + Diagnostics.normalize(compile_diagnostics, root_path, mixfile) + + Server.build_finished( + parent, + {status, mixfile_diagnostics ++ deps_diagnostics ++ compile_diagnostics} + ) - :"mix_compile_#{status}" - else + :"mix_compile_#{status}" + else + Server.build_finished( + parent, + {:ok, mixfile_diagnostics ++ deps_diagnostics} + ) + + :mix_compile_disabled + end + + {:error, kind, err, stacktrace} -> + # TODO get path from exception message Server.build_finished( parent, - {:ok, mixfile_diagnostics ++ deps_diagnostics} + {:error, + mixfile_diagnostics ++ + deps_diagnostics ++ + [ + Diagnostics.error_to_diagnostic( + kind, + err, + stacktrace, + mixfile, + root_path + ) + ]} ) - :mix_compile_disabled - end - - {:error, kind, err, stacktrace} -> - # TODO get path from exception message - Server.build_finished( - parent, - {:error, - mixfile_diagnostics ++ - deps_diagnostics ++ - [ - Diagnostics.error_to_diagnostic( - kind, - err, - stacktrace, - mixfile, - root_path - ) - ]} - ) + :deps_error + end - :deps_error - end + {:error, mixfile_diagnostics} -> + Server.build_finished(parent, {:error, mixfile_diagnostics}) + :mixfile_error - {:error, mixfile_diagnostics} -> - Server.build_finished(parent, {:error, mixfile_diagnostics}) - :mixfile_error - - :no_mixfile -> - Server.build_finished(parent, {:no_mixfile, []}) - :no_mixfile - end - end) + :no_mixfile -> + Server.build_finished(parent, {:no_mixfile, []}) + :no_mixfile + end + end) - if Keyword.get(opts, :compile?) do - Tracer.save() - Logger.info("Compile took #{div(us, 1000)} milliseconds") - else - Logger.info("Mix project load took #{div(us, 1000)} milliseconds") - end + if Keyword.get(opts, :compile?) do + Tracer.save() + Logger.info("Compile took #{div(us, 1000)} milliseconds") + else + Logger.info("Mix project load took #{div(us, 1000)} milliseconds") + end - JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{ - "elixir_ls.build_time" => div(us, 1000) - }) + JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{ + "elixir_ls.build_time" => div(us, 1000) + }) + end) end) - end) spawn(fn -> Process.monitor(parent) @@ -138,6 +139,7 @@ defmodule ElixirLS.LanguageServer.Build do receive do {:DOWN, _ref, _, ^build_process, _reason} -> :ok + {:DOWN, _ref, _, ^parent, _reason} -> Process.exit(build_process, :kill) end @@ -149,11 +151,14 @@ defmodule ElixirLS.LanguageServer.Build do def clean(root_path, clean_deps? \\ false) when is_binary(root_path) do with_build_lock(fn -> mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path) + case reload_project(mixfile, root_path) do {:ok, _} -> Mix.Task.clear() run_mix_clean(clean_deps?) - other -> other + + other -> + other end end) end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d0c58b7d2..5bc0c971c 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -364,11 +364,13 @@ defmodule ElixirLS.LanguageServer.Server do handle_build_result(:error, [Diagnostics.exception_to_diagnostic(reason, path)], state) end - state = if state.needs_build? do - trigger_build(state) - else - state - end + state = + if state.needs_build? do + trigger_build(state) + else + state + end + {:noreply, state} end @@ -2142,7 +2144,10 @@ defmodule ElixirLS.LanguageServer.Server do :ok e -> - Logger.warn("Unable to clean project, databases may not be up to date: #{inspect(e)}") + Logger.warn( + "Unable to clean project, databases may not be up to date: #{inspect(e)}" + ) + :ok end rescue From 30aa227d1144a736932289ff388c4ecf435ebaba Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 27 Nov 2023 20:56:18 +0100 Subject: [PATCH 16/20] use List.improper? when it makes sense --- .../lib/debugger/variables.ex | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/variables.ex b/apps/elixir_ls_debugger/lib/debugger/variables.ex index a06b994f4..c3c1fbd22 100644 --- a/apps/elixir_ls_debugger/lib/debugger/variables.ex +++ b/apps/elixir_ls_debugger/lib/debugger/variables.ex @@ -12,16 +12,11 @@ defmodule ElixirLS.Debugger.Variables do if Keyword.keyword?(var) do :named else - :indexed - - try do - # this call will raise ArgumentError for improper list, no better way to check it - _ = length(var) + if List.improper?(var) do + # improper list has head and tail + :named + else :indexed - rescue - ArgumentError -> - # improper list has head and tail - :named end end end @@ -48,7 +43,7 @@ defmodule ElixirLS.Debugger.Variables do start = start || 0 try do - # this call will raise ArgumentError for improper list, no better way to check it + # this call will raise ArgumentError for improper list max_count = length(var) count = count || max_count @@ -137,6 +132,7 @@ defmodule ElixirLS.Debugger.Variables do def num_children(var) when is_list(var) do try do + # this call will raise ArgumentError for improper list length(var) rescue ArgumentError -> @@ -202,13 +198,10 @@ defmodule ElixirLS.Debugger.Variables do if Keyword.keyword?(var) and var != [] do "keyword" else - try do - # this call will raise ArgumentError for improper list, no better way to check it - _ = length(var) + if List.improper?(var) do + "improper list" + else "list" - rescue - ArgumentError -> - "improper list" end end end From 057393d7953f570ba9938b71bcf70cb5516be64a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 27 Nov 2023 21:51:59 +0100 Subject: [PATCH 17/20] bump elixir_sense --- VERSION | 2 +- apps/language_server/lib/language_server/server.ex | 2 +- dep_versions.exs | 2 +- mix.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 52eacacfb..66333910a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.17.10 +0.18.0 diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 5bc0c971c..58b458761 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -2144,7 +2144,7 @@ defmodule ElixirLS.LanguageServer.Server do :ok e -> - Logger.warn( + Logger.warning( "Unable to clean project, databases may not be up to date: #{inspect(e)}" ) diff --git a/dep_versions.exs b/dep_versions.exs index b5baf22de..3b642e3dd 100644 --- a/dep_versions.exs +++ b/dep_versions.exs @@ -1,5 +1,5 @@ [ - elixir_sense: "16d28f78e5702678394523c6aa17486931740402", + elixir_sense: "02c101d03c0b5a81379b3905e7baa6e685c0fe99", dialyxir_vendored: "d50dcd7101c6ebd37b57b7ee4a7888d8cb634782", jason_v: "c81537e2a5e1acacb915cf339fe400357e3c2aaa", erl2ex_vendored: "073ac6b9a44282e718b6050c7b27cedf9217a12a", diff --git a/mix.lock b/mix.lock index 43fd32463..3deb41b96 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "d50dcd7101c6ebd37b57b7ee4a7888d8cb634782", [ref: "d50dcd7101c6ebd37b57b7ee4a7888d8cb634782"]}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "16d28f78e5702678394523c6aa17486931740402", [ref: "16d28f78e5702678394523c6aa17486931740402"]}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "02c101d03c0b5a81379b3905e7baa6e685c0fe99", [ref: "02c101d03c0b5a81379b3905e7baa6e685c0fe99"]}, "erl2ex_vendored": {:git, "https://github.com/elixir-lsp/erl2ex.git", "073ac6b9a44282e718b6050c7b27cedf9217a12a", [ref: "073ac6b9a44282e718b6050c7b27cedf9217a12a"]}, "erlex_vendored": {:git, "https://github.com/elixir-lsp/erlex.git", "82db0e82ee4896491bc26dec99f5d795f03ab9f4", [ref: "82db0e82ee4896491bc26dec99f5d795f03ab9f4"]}, "jason_v": {:git, "https://github.com/elixir-lsp/jason.git", "c81537e2a5e1acacb915cf339fe400357e3c2aaa", [ref: "c81537e2a5e1acacb915cf339fe400357e3c2aaa"]}, From adb4f79b0bd88ad86dd2429a23963fc8417abd52 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 28 Nov 2023 18:32:08 +0100 Subject: [PATCH 18/20] make the sample more broken --- .../test/providers/document_symbols_test.exs | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index bff485dd3..0a8d170db 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -2458,45 +2458,14 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do uri = "file:///project/test.exs" text = """ - defmodule A do + defmodule aA do def hello do Hello.hi( end end """ - assert { - :ok, - [ - %ElixirLS.LanguageServer.Protocol.DocumentSymbol{ - name: "A", - kind: 2, - range: %{ - "end" => %{"character" => 0, "line" => 5}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 11, "line" => 0}, - "start" => %{"character" => 10, "line" => 0} - }, - children: [ - %ElixirLS.LanguageServer.Protocol.DocumentSymbol{ - name: "def hello", - kind: 12, - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 2, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 11, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - }, - children: [] - } - ] - } - ] - } = DocumentSymbols.symbols(uri, text, true) + assert {:error, :server_error, "Cannot parse source file", false} = DocumentSymbols.symbols(uri, text, true) end test "returns def and defp as a prefix" do From 7edf42861f4ce7c5bdd1a5bfb3bffbbe3a8ad9b5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 28 Nov 2023 21:44:40 +0100 Subject: [PATCH 19/20] attempt to fix errors on <= 1.14 --- apps/language_server/lib/language_server/build.ex | 13 +++++++++++-- apps/language_server/lib/language_server/server.ex | 13 ++++++++----- apps/language_server/lib/language_server/tracer.ex | 4 +++- apps/language_server/test/server_test.exs | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index a28346ce3..4982c630b 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -67,7 +67,7 @@ defmodule ElixirLS.LanguageServer.Build do case deps_result do :ok -> if Keyword.get(opts, :compile?) do - {status, compile_diagnostics} = run_mix_compile() + {status, compile_diagnostics} = run_mix_compile(Keyword.get(opts, :force?, false)) compile_diagnostics = Diagnostics.normalize(compile_diagnostics, root_path, mixfile) @@ -128,6 +128,7 @@ defmodule ElixirLS.LanguageServer.Build do JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{ "elixir_ls.build_time" => div(us, 1000) }) + IO.warn("Releasing build lock") end) end) @@ -265,6 +266,7 @@ defmodule ElixirLS.LanguageServer.Build do Code.put_compiler_option(:no_warn_undefined, :all) # We can get diagnostics if Mixfile fails to load + IO.warn("Building mixfile #{mixfile}") {mixfile_status, mixfile_diagnostics} = case Kernel.ParallelCompiler.compile([mixfile]) do {:ok, _, warnings} -> @@ -351,7 +353,7 @@ defmodule ElixirLS.LanguageServer.Build do end end - defp run_mix_compile do + defp run_mix_compile(force?) do opts = [ "--return-errors", "--ignore-module-conflict", @@ -365,6 +367,13 @@ defmodule ElixirLS.LanguageServer.Build do opts ++ ["--all-warnings"] end + opts = + if force? do + opts ++ ["--force"] + else + opts + end + case Mix.Task.run("compile", opts) do {status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) -> {status, diagnostics} diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 58b458761..5c915deaf 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -54,6 +54,7 @@ defmodule ElixirLS.LanguageServer.Server do build_diagnostics: [], dialyzer_diagnostics: [], needs_build?: false, + full_build_done?: false, build_running?: false, analysis_ready?: false, received_shutdown?: false, @@ -316,6 +317,7 @@ defmodule ElixirLS.LanguageServer.Server do %__MODULE__{build_ref: ref, build_running?: true} = state ) do state = %{state | build_running?: false} + IO.warn("Build end reason: #{inspect(reason)}") # in case the build was interrupted make sure that cwd is reset to project dir case File.cd(state.project_dir) do @@ -1337,7 +1339,7 @@ defmodule ElixirLS.LanguageServer.Server do # Build - defp trigger_build(state = %__MODULE__{project_dir: project_dir}) do + defp trigger_build(state = %__MODULE__{project_dir: project_dir, full_build_done?: full_build_done?}) do cond do not is_binary(project_dir) -> state @@ -1345,7 +1347,8 @@ defmodule ElixirLS.LanguageServer.Server do not state.build_running? -> opts = [ fetch_deps?: Map.get(state.settings || %{}, "fetchDeps", false), - compile?: Map.get(state.settings || %{}, "autoBuild", true) + compile?: Map.get(state.settings || %{}, "autoBuild", true), + force?: not full_build_done? ] {_pid, build_ref} = @@ -1459,7 +1462,7 @@ defmodule ElixirLS.LanguageServer.Server do state.project_dir ) - state + %{state | full_build_done?: if(status == :ok, do: true, else: state.full_build_done?)} end defp handle_dialyzer_result(diagnostics, build_ref, state = %__MODULE__{}) do @@ -1689,7 +1692,7 @@ defmodule ElixirLS.LanguageServer.Server do add_watched_extensions(state.server_instance_id, additional_watched_extensions) - maybe_rebuild(state) + # maybe_rebuild(state) state = create_gitignore(state) if state.mix_project? do @@ -2153,7 +2156,7 @@ defmodule ElixirLS.LanguageServer.Server do rescue e -> message = - "Unable to reload project: #{Exception.message(e)}" + "Unable to reload project: #{Exception.format(:error, e, __STACKTRACE__)}" Logger.error(message) diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 2b52c53bc..6794c34e0 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -491,7 +491,9 @@ defmodule ElixirLS.LanguageServer.Tracer do {version, ""} <- Integer.parse(text) do version else - _ -> nil + other -> + IO.warn("Manifest: #{inspect(other)}") + nil end end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 791857ad5..2dd54cfe6 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1682,7 +1682,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "(CompileError) undefined function does_not_exist" <> _, + "message" => "** (CompileError) lib/has_error.ex:4: undefined function does_not_exist" <> _, "range" => %{"end" => %{"line" => 3}, "start" => %{"line" => 3}}, "severity" => 1 } From 48baa6ae83380150f1cd3426cbe79ebb810dc9bb Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 28 Nov 2023 22:36:22 +0100 Subject: [PATCH 20/20] run formatter --- apps/language_server/lib/language_server/build.ex | 5 ++--- apps/language_server/lib/language_server/server.ex | 5 +++-- .../language_server/test/providers/document_symbols_test.exs | 3 ++- apps/language_server/test/server_test.exs | 4 +++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 4982c630b..b387b5d72 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -67,7 +67,8 @@ defmodule ElixirLS.LanguageServer.Build do case deps_result do :ok -> if Keyword.get(opts, :compile?) do - {status, compile_diagnostics} = run_mix_compile(Keyword.get(opts, :force?, false)) + {status, compile_diagnostics} = + run_mix_compile(Keyword.get(opts, :force?, false)) compile_diagnostics = Diagnostics.normalize(compile_diagnostics, root_path, mixfile) @@ -128,7 +129,6 @@ defmodule ElixirLS.LanguageServer.Build do JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{ "elixir_ls.build_time" => div(us, 1000) }) - IO.warn("Releasing build lock") end) end) @@ -266,7 +266,6 @@ defmodule ElixirLS.LanguageServer.Build do Code.put_compiler_option(:no_warn_undefined, :all) # We can get diagnostics if Mixfile fails to load - IO.warn("Building mixfile #{mixfile}") {mixfile_status, mixfile_diagnostics} = case Kernel.ParallelCompiler.compile([mixfile]) do {:ok, _, warnings} -> diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 5c915deaf..cd7e9db0f 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -317,7 +317,6 @@ defmodule ElixirLS.LanguageServer.Server do %__MODULE__{build_ref: ref, build_running?: true} = state ) do state = %{state | build_running?: false} - IO.warn("Build end reason: #{inspect(reason)}") # in case the build was interrupted make sure that cwd is reset to project dir case File.cd(state.project_dir) do @@ -1339,7 +1338,9 @@ defmodule ElixirLS.LanguageServer.Server do # Build - defp trigger_build(state = %__MODULE__{project_dir: project_dir, full_build_done?: full_build_done?}) do + defp trigger_build( + state = %__MODULE__{project_dir: project_dir, full_build_done?: full_build_done?} + ) do cond do not is_binary(project_dir) -> state diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 0a8d170db..c9c188b73 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -2465,7 +2465,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end """ - assert {:error, :server_error, "Cannot parse source file", false} = DocumentSymbols.symbols(uri, text, true) + assert {:error, :server_error, "Cannot parse source file", false} = + DocumentSymbols.symbols(uri, text, true) end test "returns def and defp as a prefix" do diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 2dd54cfe6..3f94cc956 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1682,7 +1682,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "** (CompileError) lib/has_error.ex:4: undefined function does_not_exist" <> _, + "message" => + "** (CompileError) lib/has_error.ex:4: undefined function does_not_exist" <> + _, "range" => %{"end" => %{"line" => 3}, "start" => %{"line" => 3}}, "severity" => 1 }