Skip to content

Commit 6057d36

Browse files
authored
Fix numerous cases of incorrect utf16 positions returned and passed into elixir_sense (#677)
* add utility functions for utf8-utf16 conversions * fix numerous cases of incorrect utf16 positions
1 parent 58f07d8 commit 6057d36

File tree

16 files changed

+1181
-487
lines changed

16 files changed

+1181
-487
lines changed

apps/language_server/lib/language_server/build.ex

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ defmodule ElixirLS.LanguageServer.Build do
281281
defp range(position, nil) when is_integer(position) do
282282
line = position - 1
283283

284+
# we don't care about utf16 positions here as we send 0
284285
%{
285286
"start" => %{"line" => line, "character" => 0},
286287
"end" => %{"line" => line, "character" => 0}
@@ -290,23 +291,24 @@ defmodule ElixirLS.LanguageServer.Build do
290291
defp range(position, source_file) when is_integer(position) do
291292
line = position - 1
292293
text = Enum.at(SourceFile.lines(source_file), line) || ""
293-
start_idx = String.length(text) - String.length(String.trim_leading(text))
294-
length = Enum.max([String.length(String.trim(text)), 1])
295294

296-
%{
297-
"start" => %{"line" => line, "character" => start_idx},
298-
"end" => %{"line" => line, "character" => start_idx + length}
299-
}
300-
end
295+
start_idx = String.length(text) - String.length(String.trim_leading(text)) + 1
296+
length = max(String.length(String.trim(text)), 1)
301297

302-
defp range({start_line, start_col, end_line, end_col}, _) do
303298
%{
304-
"start" => %{"line" => start_line - 1, "character" => start_col},
305-
"end" => %{"line" => end_line - 1, "character" => end_col}
299+
"start" => %{
300+
"line" => line,
301+
"character" => SourceFile.elixir_character_to_lsp(text, start_idx)
302+
},
303+
"end" => %{
304+
"line" => line,
305+
"character" => SourceFile.elixir_character_to_lsp(text, start_idx + length)
306+
}
306307
}
307308
end
308309

309310
defp range(_, nil) do
311+
# we don't care about utf16 positions here as we send 0
310312
%{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}}
311313
end
312314

apps/language_server/lib/language_server/protocol/location.ex

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,30 @@ defmodule ElixirLS.LanguageServer.Protocol.Location do
88
defstruct [:uri, :range]
99

1010
alias ElixirLS.LanguageServer.SourceFile
11-
alias ElixirLS.LanguageServer.Protocol
11+
require ElixirLS.LanguageServer.Protocol, as: Protocol
1212

13-
def new(%ElixirSense.Location{file: file, line: line, column: column}, uri) do
13+
def new(
14+
%ElixirSense.Location{file: file, line: line, column: column},
15+
current_file_uri,
16+
current_file_text
17+
) do
1418
uri =
1519
case file do
16-
nil -> uri
20+
nil -> current_file_uri
1721
_ -> SourceFile.path_to_uri(file)
1822
end
1923

20-
# LSP messages are 0 indexed whilst elixir/erlang is 1 indexed.
21-
# Guard against malformed line or column values.
22-
line = max(line - 1, 0)
23-
column = max(column - 1, 0)
24+
text =
25+
case file do
26+
nil -> current_file_text
27+
file -> File.read!(file)
28+
end
29+
30+
{line, column} = SourceFile.elixir_position_to_lsp(text, {line, column})
2431

2532
%Protocol.Location{
2633
uri: uri,
27-
range: %{
28-
"start" => %{"line" => line, "character" => column},
29-
"end" => %{"line" => line, "character" => column}
30-
}
34+
range: Protocol.range(line, column, line, column)
3135
}
3236
end
3337
end

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do
1717

1818
def build_code_lens(line, title, command, argument) do
1919
%{
20+
# we don't care about utf16 positions here as we send 0
2021
"range" => range(line - 1, 0, line - 1, 0),
2122
"command" => %{
2223
"title" => title,

apps/language_server/lib/language_server/providers/completion.ex

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,20 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
9090
|> SourceFile.lines()
9191
|> Enum.at(line)
9292

93-
text_before_cursor = String.slice(line_text, 0, character)
94-
text_after_cursor = String.slice(line_text, character..-1)
93+
# convert to 1 based utf8 position
94+
line = line + 1
95+
character = SourceFile.lsp_character_to_elixir(line_text, character)
96+
97+
text_before_cursor = String.slice(line_text, 0, character - 1)
98+
text_after_cursor = String.slice(line_text, (character - 1)..-1)
9599

96100
prefix = get_prefix(text_before_cursor)
97101

98102
# TODO: Don't call into here directly
99103
# Can we use ElixirSense.Providers.Suggestion? ElixirSense.suggestions/3
100104
env =
101-
ElixirSense.Core.Parser.parse_string(text, true, true, line + 1)
102-
|> ElixirSense.Core.Metadata.get_env(line + 1)
105+
ElixirSense.Core.Parser.parse_string(text, true, true, line)
106+
|> ElixirSense.Core.Metadata.get_env(line)
103107

104108
scope =
105109
case env.scope do
@@ -135,7 +139,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
135139
}
136140

137141
items =
138-
ElixirSense.suggestions(text, line + 1, character + 1)
142+
ElixirSense.suggestions(text, line, character)
139143
|> maybe_reject_derived_functions(context, options)
140144
|> Enum.map(&from_completion_item(&1, context, options))
141145
|> maybe_add_do(context)

apps/language_server/lib/language_server/providers/definition.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do
33
Go-to-definition provider utilizing Elixir Sense
44
"""
55

6-
alias ElixirLS.LanguageServer.Protocol
6+
alias ElixirLS.LanguageServer.{Protocol, SourceFile}
77

88
def definition(uri, text, line, character) do
9+
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
10+
911
result =
10-
case ElixirSense.definition(text, line + 1, character + 1) do
12+
case ElixirSense.definition(text, line, character) do
1113
nil ->
1214
nil
1315

1416
%ElixirSense.Location{} = location ->
15-
Protocol.Location.new(location, uri)
17+
Protocol.Location.new(location, uri, text)
1618
end
1719

1820
{:ok, result}

apps/language_server/lib/language_server/providers/document_symbols.ex

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do
66
"""
77

88
alias ElixirLS.LanguageServer.Providers.SymbolUtils
9-
alias ElixirLS.LanguageServer.Protocol
9+
alias ElixirLS.LanguageServer.SourceFile
10+
require ElixirLS.LanguageServer.Protocol, as: Protocol
1011

1112
defmodule Info do
1213
defstruct [:type, :name, :location, :children]
@@ -25,22 +26,22 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do
2526
def symbols(uri, text, hierarchical) do
2627
case list_symbols(text) do
2728
{:ok, symbols} ->
28-
{:ok, build_symbols(symbols, uri, hierarchical)}
29+
{:ok, build_symbols(symbols, uri, text, hierarchical)}
2930

3031
{:error, :compilation_error} ->
3132
{:error, :server_error, "[DocumentSymbols] Compilation error while parsing source file"}
3233
end
3334
end
3435

35-
defp build_symbols(symbols, uri, hierarchical)
36+
defp build_symbols(symbols, uri, text, hierarchical)
3637

37-
defp build_symbols(symbols, uri, true) do
38-
Enum.map(symbols, &build_symbol_information_hierarchical(uri, &1))
38+
defp build_symbols(symbols, uri, text, true) do
39+
Enum.map(symbols, &build_symbol_information_hierarchical(uri, text, &1))
3940
end
4041

41-
defp build_symbols(symbols, uri, false) do
42+
defp build_symbols(symbols, uri, text, false) do
4243
symbols
43-
|> Enum.map(&build_symbol_information_flat(uri, &1))
44+
|> Enum.map(&build_symbol_information_flat(uri, text, &1))
4445
|> List.flatten()
4546
end
4647

@@ -283,25 +284,27 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do
283284

284285
defp extract_symbol(_, _), do: nil
285286

286-
defp build_symbol_information_hierarchical(uri, info) when is_list(info),
287-
do: Enum.map(info, &build_symbol_information_hierarchical(uri, &1))
287+
defp build_symbol_information_hierarchical(uri, text, info) when is_list(info),
288+
do: Enum.map(info, &build_symbol_information_hierarchical(uri, text, &1))
289+
290+
defp build_symbol_information_hierarchical(uri, text, %Info{} = info) do
291+
range = location_to_range(info.location, text)
288292

289-
defp build_symbol_information_hierarchical(uri, %Info{} = info) do
290293
%Protocol.DocumentSymbol{
291294
name: info.name,
292295
kind: SymbolUtils.symbol_kind_to_code(info.type),
293-
range: location_to_range(info.location),
294-
selectionRange: location_to_range(info.location),
295-
children: build_symbol_information_hierarchical(uri, info.children)
296+
range: range,
297+
selectionRange: range,
298+
children: build_symbol_information_hierarchical(uri, text, info.children)
296299
}
297300
end
298301

299-
defp build_symbol_information_flat(uri, info, parent_name \\ nil)
302+
defp build_symbol_information_flat(uri, text, info, parent_name \\ nil)
300303

301-
defp build_symbol_information_flat(uri, info, parent_name) when is_list(info),
302-
do: Enum.map(info, &build_symbol_information_flat(uri, &1, parent_name))
304+
defp build_symbol_information_flat(uri, text, info, parent_name) when is_list(info),
305+
do: Enum.map(info, &build_symbol_information_flat(uri, text, &1, parent_name))
303306

304-
defp build_symbol_information_flat(uri, %Info{} = info, parent_name) do
307+
defp build_symbol_information_flat(uri, text, %Info{} = info, parent_name) do
305308
case info.children do
306309
[_ | _] ->
307310
[
@@ -310,11 +313,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do
310313
kind: SymbolUtils.symbol_kind_to_code(info.type),
311314
location: %{
312315
uri: uri,
313-
range: location_to_range(info.location)
316+
range: location_to_range(info.location, text)
314317
},
315318
containerName: parent_name
316319
}
317-
| Enum.map(info.children, &build_symbol_information_flat(uri, &1, info.name))
320+
| Enum.map(info.children, &build_symbol_information_flat(uri, text, &1, info.name))
318321
]
319322

320323
_ ->
@@ -323,18 +326,18 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do
323326
kind: SymbolUtils.symbol_kind_to_code(info.type),
324327
location: %{
325328
uri: uri,
326-
range: location_to_range(info.location)
329+
range: location_to_range(info.location, text)
327330
},
328331
containerName: parent_name
329332
}
330333
end
331334
end
332335

333-
defp location_to_range(location) do
334-
%{
335-
start: %{line: location[:line] - 1, character: location[:column] - 1},
336-
end: %{line: location[:line] - 1, character: location[:column] - 1}
337-
}
336+
defp location_to_range(location, text) do
337+
{line, character} =
338+
SourceFile.elixir_position_to_lsp(text, {location[:line], location[:column]})
339+
340+
Protocol.range(line, character, line, character)
338341
end
339342

340343
defp extract_module_name(protocol: protocol, implementations: implementations) do

apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do
7878
"label" => "Add @spec to #{mod}.#{fun}/#{arity}",
7979
"edit" => %{
8080
"changes" => %{
81+
# we don't care about utf16 positions here as we send 0
8182
uri => [%{"range" => range(line - 1, 0, line - 1, 0), "newText" => formatted}]
8283
}
8384
}

apps/language_server/lib/language_server/providers/hover.ex

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule ElixirLS.LanguageServer.Providers.Hover do
22
alias ElixirLS.LanguageServer.SourceFile
3+
import ElixirLS.LanguageServer.Protocol
34

45
@moduledoc """
56
Hover provider utilizing Elixir Sense
@@ -17,14 +18,16 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
1718
|> Enum.map(fn x -> "lib/#{x}/lib" end)
1819

1920
def hover(text, line, character, project_dir) do
21+
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
22+
2023
response =
21-
case ElixirSense.docs(text, line + 1, character + 1) do
24+
case ElixirSense.docs(text, line, character) do
2225
%{subject: ""} ->
2326
nil
2427

2528
%{subject: subject, docs: docs} ->
26-
line_text = Enum.at(SourceFile.lines(text), line)
27-
range = highlight_range(line_text, line, character, subject)
29+
line_text = Enum.at(SourceFile.lines(text), line - 1)
30+
range = highlight_range(line_text, line - 1, character - 1, subject)
2831

2932
%{"contents" => contents(docs, subject, project_dir), "range" => range}
3033
end
@@ -45,10 +48,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
4548

4649
Enum.find_value(regex_ranges, fn
4750
[{start, length}] when start <= character and character <= start + length ->
48-
%{
49-
"start" => %{"line" => line, "character" => start},
50-
"end" => %{"line" => line, "character" => start + length}
51-
}
51+
range(
52+
line,
53+
SourceFile.elixir_character_to_lsp(line_text, start + 1),
54+
line,
55+
SourceFile.elixir_character_to_lsp(line_text, start + 1 + length)
56+
)
5257

5358
_ ->
5459
nil

apps/language_server/lib/language_server/providers/implementation.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ defmodule ElixirLS.LanguageServer.Providers.Implementation do
33
Go-to-implementation provider utilizing Elixir Sense
44
"""
55

6-
alias ElixirLS.LanguageServer.Protocol
6+
alias ElixirLS.LanguageServer.{Protocol, SourceFile}
77

88
def implementation(uri, text, line, character) do
9-
locations = ElixirSense.implementations(text, line + 1, character + 1)
10-
results = for location <- locations, do: Protocol.Location.new(location, uri)
9+
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
10+
locations = ElixirSense.implementations(text, line, character)
11+
results = for location <- locations, do: Protocol.Location.new(location, uri, text)
1112

1213
{:ok, results}
1314
end

apps/language_server/lib/language_server/providers/on_type_formatting.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer.Providers.OnTypeFormatting do
1010
alias ElixirLS.LanguageServer.SourceFile
1111
import ElixirLS.LanguageServer.Protocol
1212

13-
def format(%SourceFile{} = source_file, line, character, "\n", _options) do
13+
def format(%SourceFile{} = source_file, line, character, "\n", _options) when line >= 1 do
14+
# we don't care about utf16 positions here as we only pass character back to client
1415
lines = SourceFile.lines(source_file)
1516
prev_line = Enum.at(lines, line - 1)
1617

@@ -69,6 +70,7 @@ defmodule ElixirLS.LanguageServer.Providers.OnTypeFormatting do
6970
# In VS Code, currently, the cursor jumps strangely if the current line is blank and we try to
7071
# insert a newline at the current position, so unfortunately, we have to check for that.
7172
defp insert_end_edit(indentation, line, character, insert_on_next_line?) do
73+
# we don't care about utf16 positions here as we either use 0 or what the client sent
7274
if insert_on_next_line? do
7375
{range(line + 1, 0, line + 1, 0), "#{indentation}end\n"}
7476
else

0 commit comments

Comments
 (0)