Skip to content

Commit e8c381b

Browse files
authored
URI - file system path conversion fixes (#447)
* 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 <[email protected]>
1 parent 9fc2188 commit e8c381b

File tree

14 files changed

+572
-231
lines changed

14 files changed

+572
-231
lines changed

.formatter.exs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
[
22
inputs: [
33
"*.exs",
4-
"config/**/*.exs",
5-
"apps/*/{config,lib,test}/**/*.{ex,exs}",
6-
"apps/*/mix.exs"
4+
"config/**/*.exs"
5+
],
6+
subdirectories: [
7+
"apps/elixir_ls_utils",
8+
"apps/elixir_ls_debugger",
9+
"apps/language_server"
710
]
811
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
inputs: [
3+
"*.exs",
4+
"{lib,test,config}/**/*.{ex,exs}"
5+
]
6+
]

apps/elixir_ls_utils/.formatter.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
inputs: [
3+
"*.exs",
4+
"{lib,config}/**/*.{ex,exs}",
5+
"test/*.exs",
6+
"test/support/**/*.ex"
7+
]
8+
]

apps/language_server/.formatter.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
inputs: [
3+
"*.exs",
4+
"{lib,test,config}/**/*.{ex,exs}"
5+
]
6+
]

apps/language_server/lib/language_server/providers/code_lens/test.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do
1616

1717
@run_test_command "elixir.lens.test.run"
1818

19-
def code_lens(uri, text) do
19+
def code_lens(uri = "file:" <> _, text) do
2020
with {:ok, buffer_file_metadata} <- parse_source(text) do
2121
source_lines = SourceFile.lines(text)
2222

@@ -48,6 +48,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do
4848
end
4949
end
5050

51+
def code_lens(_uri, _text), do: {:ok, []}
52+
5153
defp get_test_lenses(test_blocks, file_path) do
5254
args = fn block ->
5355
%{

apps/language_server/lib/language_server/providers/formatting.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do
3636

3737
# If in an umbrella project, the cwd might be set to a sub-app if it's being compiled. This is
3838
# fine if the file we're trying to format is in that app. Otherwise, we return an error.
39-
defp can_format?(file_uri, project_dir) do
39+
defp can_format?(file_uri = "file:" <> _, project_dir) do
4040
file_path = file_uri |> SourceFile.path_from_uri() |> Path.absname()
4141

42-
not String.starts_with?(file_path, project_dir) or
42+
String.starts_with?(file_path, Path.absname(project_dir)) or
4343
String.starts_with?(file_path, File.cwd!())
4444
end
4545

46+
defp can_format?(_uri, _project_dir), do: false
47+
4648
def should_format?(file_uri, project_dir, inputs) when is_list(inputs) do
4749
file_path = file_uri |> SourceFile.path_from_uri() |> Path.absname()
4850

apps/language_server/lib/language_server/server.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ defmodule ElixirLS.LanguageServer.Server do
118118
end
119119

120120
@impl GenServer
121-
def handle_call({:suggest_contracts, uri}, from, state) do
121+
def handle_call({:suggest_contracts, uri = "file:" <> _}, from, state) do
122122
case state do
123123
%{analysis_ready?: true, source_files: %{^uri => %{dirty?: false}}} ->
124124
{:reply, Dialyzer.suggest_contracts([SourceFile.path_from_uri(uri)]), state}
@@ -130,6 +130,10 @@ defmodule ElixirLS.LanguageServer.Server do
130130
end
131131
end
132132

133+
def handle_call({:suggest_contracts, _uri}, _from, state) do
134+
{:reply, [], state}
135+
end
136+
133137
@impl GenServer
134138
def handle_cast({:build_finished, {status, diagnostics}}, state)
135139
when status in [:ok, :noop, :error] and is_list(diagnostics) do
@@ -400,7 +404,7 @@ defmodule ElixirLS.LanguageServer.Server do
400404
# deleted file still open in editor, keep dirty flag
401405
acc
402406

403-
%{"uri" => uri}, acc ->
407+
%{"uri" => uri = "file:" <> _}, acc ->
404408
# file created/updated - set dirty flag to false if file contents are equal
405409
case acc[uri] do
406410
%SourceFile{text: source_file_text, dirty?: true} = source_file ->

apps/language_server/lib/language_server/source_file.ex

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,28 +62,101 @@ defmodule ElixirLS.LanguageServer.SourceFile do
6262
@doc """
6363
Returns path from URI in a way that handles windows file:///c%3A/... URLs correctly
6464
"""
65-
def path_from_uri(uri) do
66-
uri_path = URI.decode(URI.parse(uri).path)
65+
def path_from_uri(%URI{scheme: "file", path: path, authority: authority}) do
66+
uri_path =
67+
cond do
68+
path == nil ->
69+
# treat no path as root path
70+
"/"
71+
72+
authority not in ["", nil] and path not in ["", nil] ->
73+
# UNC path
74+
"//#{URI.decode(authority)}#{URI.decode(path)}"
75+
76+
true ->
77+
decoded_path = URI.decode(path)
78+
79+
if match?({:win32, _}, :os.type()) and
80+
String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do
81+
# Windows drive letter path
82+
# drop leading `/` and downcase drive letter
83+
<<_, letter, path_rest::binary>> = decoded_path
84+
<<downcase(letter), path_rest::binary>>
85+
else
86+
decoded_path
87+
end
88+
end
6789

6890
case :os.type() do
69-
{:win32, _} -> String.trim_leading(uri_path, "/")
70-
_ -> uri_path
91+
{:win32, _} ->
92+
# convert path separators from URI to Windows
93+
String.replace(uri_path, ~r/\//, "\\")
94+
95+
_ ->
96+
uri_path
7197
end
7298
end
7399

100+
def path_from_uri(%URI{scheme: scheme}) do
101+
raise ArgumentError, message: "unexpected URI scheme #{inspect(scheme)}"
102+
end
103+
104+
def path_from_uri(uri) do
105+
uri |> URI.parse() |> path_from_uri
106+
end
107+
74108
def path_to_uri(path) do
75-
uri_path =
76-
path
77-
|> Path.expand()
78-
|> URI.encode()
79-
|> String.replace(":", "%3A")
109+
path = Path.expand(path)
80110

81-
case :os.type() do
82-
{:win32, _} -> "file:///" <> uri_path
83-
_ -> "file://" <> uri_path
84-
end
111+
path =
112+
case :os.type() do
113+
{:win32, _} ->
114+
# convert path separators from Windows to URI
115+
String.replace(path, ~r/\\/, "/")
116+
117+
_ ->
118+
path
119+
end
120+
121+
{authority, path} =
122+
case path do
123+
"//" <> rest ->
124+
# UNC path - extract authority
125+
case String.split(rest, "/", parts: 2) do
126+
[_] ->
127+
# no path part, use root path
128+
{rest, "/"}
129+
130+
[a, ""] ->
131+
# empty path part, use root path
132+
{a, "/"}
133+
134+
[a, p] ->
135+
{a, "/" <> p}
136+
end
137+
138+
"/" <> _rest ->
139+
{"", path}
140+
141+
other ->
142+
# treat as relative to root path
143+
{"", "/" <> other}
144+
end
145+
146+
%URI{
147+
scheme: "file",
148+
authority: authority |> URI.encode(),
149+
# file system paths allow reserved URI characters that need to be escaped
150+
# the exact rules are complicated but for simplicity we escape all reserved except `/`
151+
# that's what https://github.com/microsoft/vscode-uri does
152+
path: path |> URI.encode(&(&1 == ?/ or URI.char_unreserved?(&1)))
153+
}
154+
|> URI.to_string()
85155
end
86156

157+
defp downcase(char) when char >= ?A and char <= ?Z, do: char + 32
158+
defp downcase(char), do: char
159+
87160
def full_range(source_file) do
88161
lines = lines(source_file)
89162
last_line = List.last(lines)
@@ -244,7 +317,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do
244317
end
245318

246319
@spec formatter_opts(String.t()) :: {:ok, keyword()} | :error
247-
def formatter_opts(uri) do
320+
def formatter_opts(uri = "file:" <> _) do
248321
path = path_from_uri(uri)
249322

250323
try do
@@ -263,6 +336,8 @@ defmodule ElixirLS.LanguageServer.SourceFile do
263336
end
264337
end
265338

339+
def formatter_opts(_), do: :error
340+
266341
defp format_code(code, opts) do
267342
try do
268343
{:ok, Code.format_string!(code, opts)}

apps/language_server/test/providers/code_lens/test_test.exs

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
22
use ExUnit.Case
33

4+
import ElixirLS.LanguageServer.Test.PlatformTestHelpers
45
alias ElixirLS.LanguageServer.Providers.CodeLens
56

67
setup context do
@@ -16,7 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
1617
end
1718

1819
test "returns all module code lenses" do
19-
uri = "file://project/file.ex"
20+
uri = "file:///project/file.ex"
2021

2122
text = """
2223
defmodule MyModule do
@@ -32,13 +33,17 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
3233

3334
assert lenses ==
3435
[
35-
build_code_lens(0, :module, "/file.ex", %{"module" => MyModule}),
36-
build_code_lens(4, :module, "/file.ex", %{"module" => MyModule2})
36+
build_code_lens(0, :module, maybe_convert_path_separators("/project/file.ex"), %{
37+
"module" => MyModule
38+
}),
39+
build_code_lens(4, :module, maybe_convert_path_separators("/project/file.ex"), %{
40+
"module" => MyModule2
41+
})
3742
]
3843
end
3944

4045
test "returns all nested module code lenses" do
41-
uri = "file://project/file.ex"
46+
uri = "file:///project/file.ex"
4247

4348
text = """
4449
defmodule MyModule do
@@ -54,13 +59,17 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
5459

5560
assert lenses ==
5661
[
57-
build_code_lens(0, :module, "/file.ex", %{"module" => MyModule}),
58-
build_code_lens(3, :module, "/file.ex", %{"module" => MyModule.MyModule2})
62+
build_code_lens(0, :module, maybe_convert_path_separators("/project/file.ex"), %{
63+
"module" => MyModule
64+
}),
65+
build_code_lens(3, :module, maybe_convert_path_separators("/project/file.ex"), %{
66+
"module" => MyModule.MyModule2
67+
})
5968
]
6069
end
6170

6271
test "does not return lenses for modules that don't import ExUnit.case" do
63-
uri = "file://project/file.ex"
72+
uri = "file:///project/file.ex"
6473

6574
text = """
6675
defmodule MyModule do
@@ -73,7 +82,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
7382
end
7483

7584
test "returns lenses for all describe blocks" do
76-
uri = "file://project/file.ex"
85+
uri = "file:///project/file.ex"
7786

7887
text = """
7988
defmodule MyModule do
@@ -91,17 +100,21 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
91100

92101
assert Enum.member?(
93102
lenses,
94-
build_code_lens(3, :describe, "/file.ex", %{"describe" => "describe1"})
103+
build_code_lens(3, :describe, maybe_convert_path_separators("/project/file.ex"), %{
104+
"describe" => "describe1"
105+
})
95106
)
96107

97108
assert Enum.member?(
98109
lenses,
99-
build_code_lens(6, :describe, "/file.ex", %{"describe" => "describe2"})
110+
build_code_lens(6, :describe, maybe_convert_path_separators("/project/file.ex"), %{
111+
"describe" => "describe2"
112+
})
100113
)
101114
end
102115

103116
test "returns lenses for all test blocks" do
104-
uri = "file://project/file.ex"
117+
uri = "file:///project/file.ex"
105118

106119
text = """
107120
defmodule MyModule do
@@ -119,17 +132,21 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
119132

120133
assert Enum.member?(
121134
lenses,
122-
build_code_lens(3, :test, "/file.ex", %{"testName" => "test1"})
135+
build_code_lens(3, :test, maybe_convert_path_separators("/project/file.ex"), %{
136+
"testName" => "test1"
137+
})
123138
)
124139

125140
assert Enum.member?(
126141
lenses,
127-
build_code_lens(6, :test, "/file.ex", %{"testName" => "test2"})
142+
build_code_lens(6, :test, maybe_convert_path_separators("/project/file.ex"), %{
143+
"testName" => "test2"
144+
})
128145
)
129146
end
130147

131148
test "given test blocks inside describe blocks, should return code lenses with the test and describe name" do
132-
uri = "file://project/file.ex"
149+
uri = "file:///project/file.ex"
133150

134151
text = """
135152
defmodule MyModule do
@@ -146,7 +163,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
146163

147164
assert Enum.member?(
148165
lenses,
149-
build_code_lens(4, :test, "/file.ex", %{
166+
build_code_lens(4, :test, maybe_convert_path_separators("/project/file.ex"), %{
150167
"testName" => "test1",
151168
"describe" => "describe1"
152169
})
@@ -281,26 +298,26 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do
281298
end
282299

283300
test "returns module lens on the module declaration line", %{text: text} do
284-
uri = "file://project/file.ex"
301+
uri = "file:///project/file.ex"
285302

286303
{:ok, lenses} = CodeLens.Test.code_lens(uri, text)
287304

288305
assert Enum.member?(
289306
lenses,
290-
build_code_lens(0, :module, "/file.ex", %{
307+
build_code_lens(0, :module, maybe_convert_path_separators("/project/file.ex"), %{
291308
"module" => ElixirLS.LanguageServer.DiagnosticsTest
292309
})
293310
)
294311
end
295312

296313
test "returns test lenses with describe info", %{text: text} do
297-
uri = "file://project/file.ex"
314+
uri = "file:///project/file.ex"
298315

299316
{:ok, lenses} = CodeLens.Test.code_lens(uri, text)
300317

301318
assert Enum.member?(
302319
lenses,
303-
build_code_lens(5, :test, "/file.ex", %{
320+
build_code_lens(5, :test, maybe_convert_path_separators("/project/file.ex"), %{
304321
"testName" => "extract the stacktrace from the message and format it",
305322
"describe" => "normalize/2"
306323
})

0 commit comments

Comments
 (0)