From e8c381b6d029902e20fa46719ef17c99cd7599c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Thu, 14 Jan 2021 19:12:18 +0100 Subject: [PATCH] URI - file system path conversion fixes (#447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixes and improvements to URI - file system path translation tests and implementation basing on https://github.com/microsoft/vscode-uri adds support for UNC paths adds downcasing for Windows drive letters fixes character escaping for paths with URI control characters (?, #, : etc) don't translate non file: URIs to paths * fix invalid URIs int tests file://project/file.ex is a path to /file.ex on project server valid UNIX path URI to /project/file.ex is file:///project/file.ex * reintroduce relative path expand * change dubious logic * fix tests on windows * fix tests * add .formatter.exs in apps * fix formatter tests * fix potential crashes when URI is not in file scheme * fix tests on elixir < 1.10 * fix tests after merge Co-authored-by: Łukasz Samson --- .formatter.exs | 9 +- apps/elixir_ls_debugger/.formatter.exs | 6 + apps/elixir_ls_utils/.formatter.exs | 8 + apps/language_server/.formatter.exs | 6 + .../providers/code_lens/test.ex | 4 +- .../language_server/providers/formatting.ex | 6 +- .../lib/language_server/server.ex | 8 +- .../lib/language_server/source_file.ex | 103 +++++- .../test/providers/code_lens/test_test.exs | 55 +-- .../test/providers/document_symbols_test.exs | 84 ++--- .../test/providers/formatting_test.exs | 313 +++++++++--------- .../language_server/test/source_file_test.exs | 181 ++++++++++ .../test/support/fixture_helpers.ex | 4 + .../test/support/platform_test_helpers.ex | 16 + 14 files changed, 572 insertions(+), 231 deletions(-) create mode 100644 apps/elixir_ls_debugger/.formatter.exs create mode 100644 apps/elixir_ls_utils/.formatter.exs create mode 100644 apps/language_server/.formatter.exs create mode 100644 apps/language_server/test/support/platform_test_helpers.ex diff --git a/.formatter.exs b/.formatter.exs index 612f7a6fd..afa4ad396 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,8 +1,11 @@ [ inputs: [ "*.exs", - "config/**/*.exs", - "apps/*/{config,lib,test}/**/*.{ex,exs}", - "apps/*/mix.exs" + "config/**/*.exs" + ], + subdirectories: [ + "apps/elixir_ls_utils", + "apps/elixir_ls_debugger", + "apps/language_server" ] ] diff --git a/apps/elixir_ls_debugger/.formatter.exs b/apps/elixir_ls_debugger/.formatter.exs new file mode 100644 index 000000000..ab08bdb99 --- /dev/null +++ b/apps/elixir_ls_debugger/.formatter.exs @@ -0,0 +1,6 @@ +[ + inputs: [ + "*.exs", + "{lib,test,config}/**/*.{ex,exs}" + ] +] diff --git a/apps/elixir_ls_utils/.formatter.exs b/apps/elixir_ls_utils/.formatter.exs new file mode 100644 index 000000000..fd414856c --- /dev/null +++ b/apps/elixir_ls_utils/.formatter.exs @@ -0,0 +1,8 @@ +[ + inputs: [ + "*.exs", + "{lib,config}/**/*.{ex,exs}", + "test/*.exs", + "test/support/**/*.ex" + ] +] diff --git a/apps/language_server/.formatter.exs b/apps/language_server/.formatter.exs new file mode 100644 index 000000000..ab08bdb99 --- /dev/null +++ b/apps/language_server/.formatter.exs @@ -0,0 +1,6 @@ +[ + inputs: [ + "*.exs", + "{lib,test,config}/**/*.{ex,exs}" + ] +] 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 878cd0a3f..8155e07b3 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 @@ -16,7 +16,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do @run_test_command "elixir.lens.test.run" - def code_lens(uri, text) do + def code_lens(uri = "file:" <> _, text) do with {:ok, buffer_file_metadata} <- parse_source(text) do source_lines = SourceFile.lines(text) @@ -48,6 +48,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do end end + def code_lens(_uri, _text), do: {:ok, []} + defp get_test_lenses(test_blocks, file_path) do args = fn block -> %{ diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index 056a3bcaf..807823c3f 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -36,13 +36,15 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do # If in an umbrella project, the cwd might be set to a sub-app if it's being compiled. This is # fine if the file we're trying to format is in that app. Otherwise, we return an error. - defp can_format?(file_uri, project_dir) do + defp can_format?(file_uri = "file:" <> _, project_dir) do file_path = file_uri |> SourceFile.path_from_uri() |> Path.absname() - not String.starts_with?(file_path, project_dir) or + String.starts_with?(file_path, Path.absname(project_dir)) or String.starts_with?(file_path, File.cwd!()) end + defp can_format?(_uri, _project_dir), do: false + def should_format?(file_uri, project_dir, inputs) when is_list(inputs) do file_path = file_uri |> SourceFile.path_from_uri() |> Path.absname() diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index f7aeaad47..83e0390c7 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -118,7 +118,7 @@ defmodule ElixirLS.LanguageServer.Server do end @impl GenServer - def handle_call({:suggest_contracts, uri}, from, state) do + def handle_call({:suggest_contracts, uri = "file:" <> _}, from, state) do case state do %{analysis_ready?: true, source_files: %{^uri => %{dirty?: false}}} -> {:reply, Dialyzer.suggest_contracts([SourceFile.path_from_uri(uri)]), state} @@ -130,6 +130,10 @@ defmodule ElixirLS.LanguageServer.Server do end end + def handle_call({:suggest_contracts, _uri}, _from, state) do + {:reply, [], state} + end + @impl GenServer def handle_cast({:build_finished, {status, diagnostics}}, state) when status in [:ok, :noop, :error] and is_list(diagnostics) do @@ -400,7 +404,7 @@ defmodule ElixirLS.LanguageServer.Server do # deleted file still open in editor, keep dirty flag acc - %{"uri" => uri}, acc -> + %{"uri" => uri = "file:" <> _}, acc -> # file created/updated - set dirty flag to false if file contents are equal case acc[uri] do %SourceFile{text: source_file_text, dirty?: true} = source_file -> diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 8c4778538..dc0df0d3b 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -62,28 +62,101 @@ defmodule ElixirLS.LanguageServer.SourceFile do @doc """ Returns path from URI in a way that handles windows file:///c%3A/... URLs correctly """ - def path_from_uri(uri) do - uri_path = URI.decode(URI.parse(uri).path) + def path_from_uri(%URI{scheme: "file", path: path, authority: authority}) do + uri_path = + cond do + path == nil -> + # treat no path as root path + "/" + + authority not in ["", nil] and path not in ["", nil] -> + # UNC path + "//#{URI.decode(authority)}#{URI.decode(path)}" + + true -> + decoded_path = URI.decode(path) + + if match?({:win32, _}, :os.type()) and + String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do + # Windows drive letter path + # drop leading `/` and downcase drive letter + <<_, letter, path_rest::binary>> = decoded_path + <> + else + decoded_path + end + end case :os.type() do - {:win32, _} -> String.trim_leading(uri_path, "/") - _ -> uri_path + {:win32, _} -> + # convert path separators from URI to Windows + String.replace(uri_path, ~r/\//, "\\") + + _ -> + uri_path end end + def path_from_uri(%URI{scheme: scheme}) do + raise ArgumentError, message: "unexpected URI scheme #{inspect(scheme)}" + end + + def path_from_uri(uri) do + uri |> URI.parse() |> path_from_uri + end + def path_to_uri(path) do - uri_path = - path - |> Path.expand() - |> URI.encode() - |> String.replace(":", "%3A") + path = Path.expand(path) - case :os.type() do - {:win32, _} -> "file:///" <> uri_path - _ -> "file://" <> uri_path - end + path = + case :os.type() do + {:win32, _} -> + # convert path separators from Windows to URI + String.replace(path, ~r/\\/, "/") + + _ -> + path + end + + {authority, path} = + case path do + "//" <> rest -> + # UNC path - extract authority + case String.split(rest, "/", parts: 2) do + [_] -> + # no path part, use root path + {rest, "/"} + + [a, ""] -> + # empty path part, use root path + {a, "/"} + + [a, p] -> + {a, "/" <> p} + end + + "/" <> _rest -> + {"", path} + + other -> + # treat as relative to root path + {"", "/" <> other} + end + + %URI{ + scheme: "file", + authority: authority |> URI.encode(), + # file system paths allow reserved URI characters that need to be escaped + # the exact rules are complicated but for simplicity we escape all reserved except `/` + # that's what https://github.com/microsoft/vscode-uri does + path: path |> URI.encode(&(&1 == ?/ or URI.char_unreserved?(&1))) + } + |> URI.to_string() end + defp downcase(char) when char >= ?A and char <= ?Z, do: char + 32 + defp downcase(char), do: char + def full_range(source_file) do lines = lines(source_file) last_line = List.last(lines) @@ -244,7 +317,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do end @spec formatter_opts(String.t()) :: {:ok, keyword()} | :error - def formatter_opts(uri) do + def formatter_opts(uri = "file:" <> _) do path = path_from_uri(uri) try do @@ -263,6 +336,8 @@ defmodule ElixirLS.LanguageServer.SourceFile do end end + def formatter_opts(_), do: :error + defp format_code(code, opts) do try do {:ok, Code.format_string!(code, opts)} diff --git a/apps/language_server/test/providers/code_lens/test_test.exs b/apps/language_server/test/providers/code_lens/test_test.exs index f5f30807e..c334850d5 100644 --- a/apps/language_server/test/providers/code_lens/test_test.exs +++ b/apps/language_server/test/providers/code_lens/test_test.exs @@ -1,6 +1,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do use ExUnit.Case + import ElixirLS.LanguageServer.Test.PlatformTestHelpers alias ElixirLS.LanguageServer.Providers.CodeLens setup context do @@ -16,7 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do end test "returns all module code lenses" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -32,13 +33,17 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do assert lenses == [ - build_code_lens(0, :module, "/file.ex", %{"module" => MyModule}), - build_code_lens(4, :module, "/file.ex", %{"module" => MyModule2}) + build_code_lens(0, :module, maybe_convert_path_separators("/project/file.ex"), %{ + "module" => MyModule + }), + build_code_lens(4, :module, maybe_convert_path_separators("/project/file.ex"), %{ + "module" => MyModule2 + }) ] end test "returns all nested module code lenses" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -54,13 +59,17 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do assert lenses == [ - build_code_lens(0, :module, "/file.ex", %{"module" => MyModule}), - build_code_lens(3, :module, "/file.ex", %{"module" => MyModule.MyModule2}) + build_code_lens(0, :module, maybe_convert_path_separators("/project/file.ex"), %{ + "module" => MyModule + }), + build_code_lens(3, :module, maybe_convert_path_separators("/project/file.ex"), %{ + "module" => MyModule.MyModule2 + }) ] end test "does not return lenses for modules that don't import ExUnit.case" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -73,7 +82,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do end test "returns lenses for all describe blocks" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -91,17 +100,21 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do assert Enum.member?( lenses, - build_code_lens(3, :describe, "/file.ex", %{"describe" => "describe1"}) + build_code_lens(3, :describe, maybe_convert_path_separators("/project/file.ex"), %{ + "describe" => "describe1" + }) ) assert Enum.member?( lenses, - build_code_lens(6, :describe, "/file.ex", %{"describe" => "describe2"}) + build_code_lens(6, :describe, maybe_convert_path_separators("/project/file.ex"), %{ + "describe" => "describe2" + }) ) end test "returns lenses for all test blocks" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -119,17 +132,21 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do assert Enum.member?( lenses, - build_code_lens(3, :test, "/file.ex", %{"testName" => "test1"}) + build_code_lens(3, :test, maybe_convert_path_separators("/project/file.ex"), %{ + "testName" => "test1" + }) ) assert Enum.member?( lenses, - build_code_lens(6, :test, "/file.ex", %{"testName" => "test2"}) + build_code_lens(6, :test, maybe_convert_path_separators("/project/file.ex"), %{ + "testName" => "test2" + }) ) end test "given test blocks inside describe blocks, should return code lenses with the test and describe name" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -146,7 +163,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do assert Enum.member?( lenses, - build_code_lens(4, :test, "/file.ex", %{ + build_code_lens(4, :test, maybe_convert_path_separators("/project/file.ex"), %{ "testName" => "test1", "describe" => "describe1" }) @@ -281,26 +298,26 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do end test "returns module lens on the module declaration line", %{text: text} do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" {:ok, lenses} = CodeLens.Test.code_lens(uri, text) assert Enum.member?( lenses, - build_code_lens(0, :module, "/file.ex", %{ + build_code_lens(0, :module, maybe_convert_path_separators("/project/file.ex"), %{ "module" => ElixirLS.LanguageServer.DiagnosticsTest }) ) end test "returns test lenses with describe info", %{text: text} do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" {:ok, lenses} = CodeLens.Test.code_lens(uri, text) assert Enum.member?( lenses, - build_code_lens(5, :test, "/file.ex", %{ + build_code_lens(5, :test, maybe_convert_path_separators("/project/file.ex"), %{ "testName" => "extract the stacktrace from the message and format it", "describe" => "normalize/2" }) diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 69ccff5ca..e2ee78ecc 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -5,7 +5,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do alias ElixirLS.LanguageServer.Protocol test "returns hierarchical symbol information" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do @my_mod_var "module variable" @@ -167,7 +167,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "returns flat symbol information" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do @my_mod_var "module variable" @@ -304,7 +304,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles nested module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do defmodule SubModule do @@ -360,7 +360,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles nested module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do defmodule SubModule do @@ -407,7 +407,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles multiple module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do def some_function(), do: :ok @@ -477,7 +477,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles multiple module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do def some_function(), do: :ok @@ -535,7 +535,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles elixir atom module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule :'Elixir.MyModule' do def my_fn(), do: :ok @@ -566,7 +566,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles elixir atom module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule :'Elixir.MyModule' do def my_fn(), do: :ok @@ -594,7 +594,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles unquoted module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule unquote(var) do def my_fn(), do: :ok @@ -625,7 +625,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles unquoted module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule unquote(var) do def my_fn(), do: :ok @@ -653,7 +653,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles erlang atom module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule :my_module do def my_fn(), do: :ok @@ -684,7 +684,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles erlang atom module definitions" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule :my_module do def my_fn(), do: :ok @@ -712,7 +712,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles nested module definitions with __MODULE__" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule __MODULE__ do defmodule __MODULE__.SubModule do @@ -756,7 +756,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles nested module definitions with __MODULE__" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule __MODULE__ do defmodule __MODULE__.SubModule do @@ -794,7 +794,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles protocols and implementations" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defprotocol MyProtocol do @@ -871,7 +871,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles protocols and implementations" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defprotocol MyProtocol do @@ -939,7 +939,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles module definitions with struct" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -992,7 +992,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles module definitions with struct" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1037,7 +1037,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles module definitions with exception" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyError do @@ -1080,7 +1080,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles module definitions with exception" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyError do @@ -1117,7 +1117,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles module definitions with typespecs" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1204,7 +1204,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles module definitions with typespecs" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1278,7 +1278,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles module definitions with callbacks" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1367,7 +1367,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles module definitions with callbacks" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1443,7 +1443,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles funs with specs" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do @spec my_fn(integer) :: atom @@ -1485,7 +1485,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles funs with specs" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do @spec my_fn(integer) :: atom @@ -1522,7 +1522,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] skips docs attributes" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1545,7 +1545,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] skips docs attributes" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1568,7 +1568,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles various builtin attributes" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1776,7 +1776,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles various builtin attributes" do - uri = "file://project/file.ex" + uri = "file:///project/file.ex" text = """ defmodule MyModule do @@ -1949,7 +1949,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles exunit tests" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = ~S[ defmodule MyModuleTest do use ExUnit.Case @@ -1990,7 +1990,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles exunit tests" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = ~S[ defmodule MyModuleTest do use ExUnit.Case @@ -2019,7 +2019,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles exunit descibe tests" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = ~S[ defmodule MyModuleTest do use ExUnit.Case @@ -2076,7 +2076,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles exunit descibe tests" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = ~S[ defmodule MyModuleTest do use ExUnit.Case @@ -2115,7 +2115,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles exunit callbacks" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ defmodule MyModuleTest do @@ -2174,7 +2174,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles exunit callbacks" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ defmodule MyModuleTest do @@ -2226,7 +2226,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles config" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ use Mix.Config @@ -2276,7 +2276,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[flat] handles config" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ use Mix.Config @@ -2326,7 +2326,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles a file with a top-level module without a name" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ defmodule do @@ -2370,7 +2370,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "[nested] handles a file with a top-level protocol module without a name" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ defprotocol do @@ -2397,7 +2397,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "handles a file with compilation errors by returning an empty list" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ defmodule A do @@ -2412,7 +2412,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end test "returns def and defp as a prefix" do - uri = "file://project/test.exs" + uri = "file:///project/test.exs" text = """ defmodule A do diff --git a/apps/language_server/test/providers/formatting_test.exs b/apps/language_server/test/providers/formatting_test.exs index 0f6c713e7..5d85a27ab 100644 --- a/apps/language_server/test/providers/formatting_test.exs +++ b/apps/language_server/test/providers/formatting_test.exs @@ -1,185 +1,202 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do - use ExUnit.Case + use ElixirLS.Utils.MixTest.Case, async: false + import ElixirLS.LanguageServer.Test.PlatformTestHelpers alias ElixirLS.LanguageServer.Providers.Formatting alias ElixirLS.LanguageServer.SourceFile + alias ElixirLS.LanguageServer.Test.FixtureHelpers test "Formats a file" do - uri = "file://project/file.ex" + in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> + path = "lib/file.ex" + uri = SourceFile.path_to_uri(path) - text = """ - defmodule MyModule do - require Logger + text = """ + defmodule MyModule do + require Logger - def dummy_function() do - Logger.info "dummy" + def dummy_function() do + Logger.info "dummy" + end end - end - """ - - source_file = %SourceFile{ - text: text, - version: 1, - dirty?: true - } - - project_dir = "/project" - - assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) - - assert changes == [ - %{ - "newText" => ")", - "range" => %{ - "end" => %{"character" => 23, "line" => 4}, - "start" => %{"character" => 23, "line" => 4} - } - }, - %{ - "newText" => "(", - "range" => %{ - "end" => %{"character" => 16, "line" => 4}, - "start" => %{"character" => 15, "line" => 4} + """ + + source_file = %SourceFile{ + text: text, + version: 1, + dirty?: true + } + + project_dir = maybe_convert_path_separators(FixtureHelpers.get_path("formatter")) + + assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) + + assert changes == [ + %{ + "newText" => ")", + "range" => %{ + "end" => %{"character" => 23, "line" => 4}, + "start" => %{"character" => 23, "line" => 4} + } + }, + %{ + "newText" => "(", + "range" => %{ + "end" => %{"character" => 16, "line" => 4}, + "start" => %{"character" => 15, "line" => 4} + } } - } - ] + ] - assert Enum.all?(changes, fn change -> - assert_position_type(change["range"]["end"]) and - assert_position_type(change["range"]["start"]) - end) + assert Enum.all?(changes, fn change -> + assert_position_type(change["range"]["end"]) and + assert_position_type(change["range"]["start"]) + end) + end) end defp assert_position_type(%{"character" => ch, "line" => line}), do: is_integer(ch) and is_integer(line) test "returns an error when formatting a file with a syntax error" do - uri = "file://project/file.ex" + in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> + path = "lib/file.ex" + uri = SourceFile.path_to_uri(path) - text = """ - defmodule MyModule do - require Logger + text = """ + defmodule MyModule do + require Logger - def dummy_function() do - Logger.info("dummy + def dummy_function() do + Logger.info("dummy + end end - end - """ + """ - source_file = %SourceFile{ - text: text, - version: 1, - dirty?: true - } + source_file = %SourceFile{ + text: text, + version: 1, + dirty?: true + } - project_dir = "/project" + project_dir = maybe_convert_path_separators(FixtureHelpers.get_path("formatter")) - assert {:error, :internal_error, msg} = Formatting.format(source_file, uri, project_dir) - assert String.contains?(msg, "Unable to format") + assert {:error, :internal_error, msg} = Formatting.format(source_file, uri, project_dir) + assert String.contains?(msg, "Unable to format") + end) end test "Proper utf-16 format: emoji 😀" do - uri = "file://project/file.ex" - - text = """ - IO.puts "😀" - """ - - source_file = %SourceFile{ - text: text, - version: 1, - dirty?: true - } - - project_dir = "/project" - - assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) - - assert changes == [ - %{ - "newText" => ")", - "range" => %{ - "end" => %{"character" => 12, "line" => 0}, - "start" => %{"character" => 12, "line" => 0} - } - }, - %{ - "newText" => "(", - "range" => %{ - "end" => %{"character" => 8, "line" => 0}, - "start" => %{"character" => 7, "line" => 0} + in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> + path = "lib/file.ex" + uri = SourceFile.path_to_uri(path) + + text = """ + IO.puts "😀" + """ + + source_file = %SourceFile{ + text: text, + version: 1, + dirty?: true + } + + project_dir = maybe_convert_path_separators("/project") + + assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) + + assert changes == [ + %{ + "newText" => ")", + "range" => %{ + "end" => %{"character" => 12, "line" => 0}, + "start" => %{"character" => 12, "line" => 0} + } + }, + %{ + "newText" => "(", + "range" => %{ + "end" => %{"character" => 8, "line" => 0}, + "start" => %{"character" => 7, "line" => 0} + } } - } - ] + ] + end) end test "Proper utf-16 format: emoji 🏳️‍🌈" do - uri = "file://project/file.ex" - - text = """ - IO.puts "🏳️‍🌈" - """ - - source_file = %SourceFile{ - text: text, - version: 1, - dirty?: true - } - - project_dir = "/project" - - assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) - - assert changes == [ - %{ - "newText" => ")", - "range" => %{ - "end" => %{"character" => 16, "line" => 0}, - "start" => %{"character" => 16, "line" => 0} - } - }, - %{ - "newText" => "(", - "range" => %{ - "end" => %{"character" => 8, "line" => 0}, - "start" => %{"character" => 7, "line" => 0} + in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> + path = "lib/file.ex" + uri = SourceFile.path_to_uri(path) + + text = """ + IO.puts "🏳️‍🌈" + """ + + source_file = %SourceFile{ + text: text, + version: 1, + dirty?: true + } + + project_dir = maybe_convert_path_separators(FixtureHelpers.get_path("formatter")) + + assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) + + assert changes == [ + %{ + "newText" => ")", + "range" => %{ + "end" => %{"character" => 16, "line" => 0}, + "start" => %{"character" => 16, "line" => 0} + } + }, + %{ + "newText" => "(", + "range" => %{ + "end" => %{"character" => 8, "line" => 0}, + "start" => %{"character" => 7, "line" => 0} + } } - } - ] + ] + end) end test "Proper utf-16 format: zalgo" do - uri = "file://project/file.ex" - - text = """ - IO.puts "ẕ̸͇̞̲͇͕̹̙̄͆̇͂̏̊͒̒̈́́̕͘͠͝à̵̢̛̟̞͚̟͖̻̹̮̘͚̻͍̇͂̂̅́̎̉͗́́̃̒l̴̻̳͉̖̗͖̰̠̗̃̈́̓̓̍̅͝͝͝g̷̢͚̠̜̿̊́̋͗̔ȍ̶̹̙̅̽̌̒͌͋̓̈́͑̏͑͊͛͘ ̸̨͙̦̫̪͓̠̺̫̖͙̫̏͂̒̽́̿̂̊́͂͋͜͠͝͝ṭ̴̜͎̮͉̙͍͔̜̾͋͒̓̏̉̄͘͠͝ͅę̷̡̭̹̰̺̩̠͓͌̃̕͜͝ͅͅx̵̧͍̦͈͍̝͖͙̘͎̥͕̾̾̍̀̿̔̄̑̈͝t̸̛͇̀̕" - """ - - source_file = %SourceFile{ - text: text, - version: 1, - dirty?: true - } - - project_dir = "/project" - - assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) - - assert changes == [ - %{ - "newText" => ")", - "range" => %{ - "end" => %{"character" => 213, "line" => 0}, - "start" => %{"character" => 213, "line" => 0} - } - }, - %{ - "newText" => "(", - "range" => %{ - "end" => %{"character" => 8, "line" => 0}, - "start" => %{"character" => 7, "line" => 0} + in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> + path = "lib/file.ex" + uri = SourceFile.path_to_uri(path) + + text = """ + IO.puts "ẕ̸͇̞̲͇͕̹̙̄͆̇͂̏̊͒̒̈́́̕͘͠͝à̵̢̛̟̞͚̟͖̻̹̮̘͚̻͍̇͂̂̅́̎̉͗́́̃̒l̴̻̳͉̖̗͖̰̠̗̃̈́̓̓̍̅͝͝͝g̷̢͚̠̜̿̊́̋͗̔ȍ̶̹̙̅̽̌̒͌͋̓̈́͑̏͑͊͛͘ ̸̨͙̦̫̪͓̠̺̫̖͙̫̏͂̒̽́̿̂̊́͂͋͜͠͝͝ṭ̴̜͎̮͉̙͍͔̜̾͋͒̓̏̉̄͘͠͝ͅę̷̡̭̹̰̺̩̠͓͌̃̕͜͝ͅͅx̵̧͍̦͈͍̝͖͙̘͎̥͕̾̾̍̀̿̔̄̑̈͝t̸̛͇̀̕" + """ + + source_file = %SourceFile{ + text: text, + version: 1, + dirty?: true + } + + project_dir = maybe_convert_path_separators(FixtureHelpers.get_path("formatter")) + + assert {:ok, changes} = Formatting.format(source_file, uri, project_dir) + + assert changes == [ + %{ + "newText" => ")", + "range" => %{ + "end" => %{"character" => 213, "line" => 0}, + "start" => %{"character" => 213, "line" => 0} + } + }, + %{ + "newText" => "(", + "range" => %{ + "end" => %{"character" => 8, "line" => 0}, + "start" => %{"character" => 7, "line" => 0} + } } - } - ] + ] + end) end test "honors :inputs when deciding to format" do diff --git a/apps/language_server/test/source_file_test.exs b/apps/language_server/test/source_file_test.exs index 04062fc8a..88c59bd66 100644 --- a/apps/language_server/test/source_file_test.exs +++ b/apps/language_server/test/source_file_test.exs @@ -1,6 +1,7 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do use ExUnit.Case, async: true use ExUnitProperties + import ElixirLS.LanguageServer.Test.PlatformTestHelpers alias ElixirLS.LanguageServer.SourceFile @@ -642,4 +643,184 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do end end end + + # tests basing on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts + describe "path_from_uri" do + test "unix" do + path = SourceFile.path_from_uri("file:///some/path") + + if is_windows() do + assert path == "\\some\\path" + else + assert path == "/some/path" + end + + path = SourceFile.path_from_uri("file:///some/path/") + + if is_windows() do + assert path == "\\some\\path\\" + else + assert path == "/some/path/" + end + + path = SourceFile.path_from_uri("file:///nodes%2B%23.ex") + + if is_windows() do + assert path == "\\nodes+#.ex" + else + assert path == "/nodes+#.ex" + end + end + + test "UNC" do + path = SourceFile.path_from_uri("file://shares/files/c%23/p.cs") + + if is_windows() do + assert path == "\\\\shares\\files\\c#\\p.cs" + else + assert path == "//shares/files/c#/p.cs" + end + + path = SourceFile.path_from_uri("file://monacotools1/certificates/SSL/") + + if is_windows() do + assert path == "\\\\monacotools1\\certificates\\SSL\\" + else + assert path == "//monacotools1/certificates/SSL/" + end + + path = SourceFile.path_from_uri("file://monacotools1/") + + if is_windows() do + assert path == "\\\\monacotools1\\" + else + assert path == "//monacotools1/" + end + end + + test "no `path` in URI" do + path = SourceFile.path_from_uri("file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp") + + if is_windows() do + assert path == "\\" + else + assert path == "/" + end + end + + test "windows drive letter" do + path = SourceFile.path_from_uri("file:///c:/test/me") + + if is_windows() do + assert path == "c:\\test\\me" + else + assert path == "/c:/test/me" + end + + path = SourceFile.path_from_uri("file:///c%3A/test/me") + + if is_windows() do + assert path == "c:\\test\\me" + else + assert path == "/c:/test/me" + end + + path = SourceFile.path_from_uri("file:///C:/test/me/") + + if is_windows() do + assert path == "c:\\test\\me\\" + else + assert path == "/C:/test/me/" + end + + path = SourceFile.path_from_uri("file:///_:/path") + + if is_windows() do + assert path == "\\_:\\path" + else + assert path == "/_:/path" + end + + path = + SourceFile.path_from_uri( + "file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins" + ) + + if is_windows() do + assert path == "c:\\Source\\Zürich or Zurich (ˈzjʊərɪk,\\Code\\resources\\app\\plugins" + else + assert path == "/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins" + end + end + + test "wrong schema" do + assert_raise ArgumentError, fn -> + SourceFile.path_from_uri("untitled:Untitled-1") + end + + assert_raise ArgumentError, fn -> + SourceFile.path_from_uri("unsaved://343C3EE7-D575-486D-9D33-93AFFAF773BD") + end + end + end + + # tests basing on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts + describe "path_to_uri" do + test "unix path" do + unless is_windows() do + assert "file:///nodes%2B%23.ex" == SourceFile.path_to_uri("/nodes+#.ex") + assert "file:///coding/c%23/project1" == SourceFile.path_to_uri("/coding/c#/project1") + + assert "file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js" == + SourceFile.path_to_uri( + "/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js" + ) + + assert "file:///foo/%25A0.txt" == SourceFile.path_to_uri("/foo/%A0.txt") + assert "file:///foo/%252e.txt" == SourceFile.path_to_uri("/foo/%2e.txt") + end + end + + test "windows path" do + if is_windows() do + assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:/win/path") + assert "file:///c%3A/win/path" == SourceFile.path_to_uri("C:/win/path") + assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:/win/path/") + assert "file:///c%3A/win/path" == SourceFile.path_to_uri("/c:/win/path") + + assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:\\win\\path") + assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:\\win/path") + + assert "file:///c%3A/test%20with%20%25/path" == + SourceFile.path_to_uri("c:\\test with %\\path") + + assert "file:///c%3A/test%20with%20%2525/c%23code" == + SourceFile.path_to_uri("c:\\test with %25\\c#code") + end + end + + test "relative path" do + cwd = File.cwd!() + + uri = SourceFile.path_to_uri("a.file") + + assert SourceFile.path_from_uri(uri) == + maybe_convert_path_separators(Path.join(cwd, "a.file")) + + uri = SourceFile.path_to_uri("./foo/bar") + + assert SourceFile.path_from_uri(uri) == + maybe_convert_path_separators(Path.join(cwd, "foo/bar")) + end + + test "UNC path" do + if is_windows() do + assert "file://sh%C3%A4res/path/c%23/plugin.json" == + SourceFile.path_to_uri("\\\\shäres\\path\\c#\\plugin.json") + + assert "file://localhost/c%24/GitDevelopment/express" == + SourceFile.path_to_uri("\\\\localhost\\c$\\GitDevelopment\\express") + end + end + end end diff --git a/apps/language_server/test/support/fixture_helpers.ex b/apps/language_server/test/support/fixture_helpers.ex index 8f4881dd8..c166a1aad 100644 --- a/apps/language_server/test/support/fixture_helpers.ex +++ b/apps/language_server/test/support/fixture_helpers.ex @@ -1,4 +1,8 @@ defmodule ElixirLS.LanguageServer.Test.FixtureHelpers do + def get_path() do + Path.join([__DIR__, "fixtures"]) |> Path.expand() + end + def get_path(file) do Path.join([__DIR__, "fixtures", file]) |> Path.expand() end diff --git a/apps/language_server/test/support/platform_test_helpers.ex b/apps/language_server/test/support/platform_test_helpers.ex new file mode 100644 index 000000000..c7ef1d5ea --- /dev/null +++ b/apps/language_server/test/support/platform_test_helpers.ex @@ -0,0 +1,16 @@ +defmodule ElixirLS.LanguageServer.Test.PlatformTestHelpers do + def maybe_convert_path_separators(path) do + if is_windows() do + String.replace(path, ~r/\//, "\\") + else + path + end + end + + def is_windows() do + case :os.type() do + {:win32, _} -> true + _ -> false + end + end +end