Skip to content

Commit 6fb7164

Browse files
miXwuiaxelson
andauthored
Format ranges using UTF-16 string representation (#200)
* Format ranges using UTF-16 string representation * fix formatting Co-authored-by: Jason Axelson <jason.axelson@gmail.com>
1 parent 5fbf589 commit 6fb7164

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do
7979
if char == "\n" do
8080
{line + 1, 0}
8181
else
82-
{line, col + 1}
82+
# LSP contentChanges positions are based on UTF-16 string representation
83+
# https://microsoft.github.io/language-server-protocol/specification#textDocuments
84+
{line, col + byte_size(:unicode.characters_to_binary(char, :utf8, :utf16)) / 2}
8385
end
8486
end)
8587
end

apps/language_server/lib/language_server/source_file.ex

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,14 @@ defmodule ElixirLS.LanguageServer.SourceFile do
8383
[[line, ?\n] | acc]
8484

8585
idx == start_line ->
86-
[String.slice(line, 0, start_character) | acc]
86+
# LSP contentChanges positions are based on UTF-16 string representation
87+
# https://microsoft.github.io/language-server-protocol/specification#textDocuments
88+
beginning_utf8 =
89+
:unicode.characters_to_binary(line, :utf8, :utf16)
90+
|> binary_part(0, start_character * 2)
91+
|> :unicode.characters_to_binary(:utf16, :utf8)
92+
93+
[beginning_utf8 | acc]
8794

8895
idx > start_line ->
8996
acc
@@ -99,7 +106,18 @@ defmodule ElixirLS.LanguageServer.SourceFile do
99106
acc
100107

101108
idx == end_line ->
102-
[[String.slice(line, end_character..-1), ?\n] | acc]
109+
# LSP contentChanges positions are based on UTF-16 string representation
110+
# https://microsoft.github.io/language-server-protocol/specification#textDocuments
111+
ending_utf8 =
112+
:unicode.characters_to_binary(line, :utf8, :utf16)
113+
|> (&binary_part(
114+
&1,
115+
end_character * 2,
116+
byte_size(&1) - end_character * 2
117+
)).()
118+
|> :unicode.characters_to_binary(:utf16, :utf8)
119+
120+
[[ending_utf8, ?\n] | acc]
103121

104122
idx > end_line ->
105123
[[line, ?\n] | acc]

apps/language_server/test/providers/formatting_test.exs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,109 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do
6767
assert {:error, :internal_error, msg} = Formatting.format(source_file, uri, project_dir)
6868
assert String.contains?(msg, "Unable to format")
6969
end
70+
71+
test "Proper utf-16 format: emoji 😀" do
72+
uri = "file://project/file.ex"
73+
74+
text = """
75+
IO.puts "😀"
76+
"""
77+
78+
source_file = %ElixirLS.LanguageServer.SourceFile{
79+
text: text,
80+
version: 1,
81+
dirty?: true
82+
}
83+
84+
project_dir = "/project"
85+
86+
assert {:ok, changes} = Formatting.format(source_file, uri, project_dir)
87+
88+
assert changes == [
89+
%{
90+
"newText" => ")",
91+
"range" => %{
92+
"end" => %{"character" => 12, "line" => 0},
93+
"start" => %{"character" => 12, "line" => 0}
94+
}
95+
},
96+
%{
97+
"newText" => "(",
98+
"range" => %{
99+
"end" => %{"character" => 8, "line" => 0},
100+
"start" => %{"character" => 7, "line" => 0}
101+
}
102+
}
103+
]
104+
end
105+
106+
test "Proper utf-16 format: emoji 🏳️‍🌈" do
107+
uri = "file://project/file.ex"
108+
109+
text = """
110+
IO.puts "🏳️‍🌈"
111+
"""
112+
113+
source_file = %ElixirLS.LanguageServer.SourceFile{
114+
text: text,
115+
version: 1,
116+
dirty?: true
117+
}
118+
119+
project_dir = "/project"
120+
121+
assert {:ok, changes} = Formatting.format(source_file, uri, project_dir)
122+
123+
assert changes == [
124+
%{
125+
"newText" => ")",
126+
"range" => %{
127+
"end" => %{"character" => 16, "line" => 0},
128+
"start" => %{"character" => 16, "line" => 0}
129+
}
130+
},
131+
%{
132+
"newText" => "(",
133+
"range" => %{
134+
"end" => %{"character" => 8, "line" => 0},
135+
"start" => %{"character" => 7, "line" => 0}
136+
}
137+
}
138+
]
139+
end
140+
141+
test "Proper utf-16 format: zalgo" do
142+
uri = "file://project/file.ex"
143+
144+
text = """
145+
IO.puts "ẕ̸͇̞̲͇͕̹̙̄͆̇͂̏̊͒̒̈́́̕͘͠͝à̵̢̛̟̞͚̟͖̻̹̮̘͚̻͍̇͂̂̅́̎̉͗́́̃̒l̴̻̳͉̖̗͖̰̠̗̃̈́̓̓̍̅͝͝͝g̷̢͚̠̜̿̊́̋͗̔ȍ̶̹̙̅̽̌̒͌͋̓̈́͑̏͑͊͛͘ ̸̨͙̦̫̪͓̠̺̫̖͙̫̏͂̒̽́̿̂̊́͂͋͜͠͝͝ṭ̴̜͎̮͉̙͍͔̜̾͋͒̓̏̉̄͘͠͝ͅę̷̡̭̹̰̺̩̠͓͌̃̕͜͝ͅͅx̵̧͍̦͈͍̝͖͙̘͎̥͕̾̾̍̀̿̔̄̑̈͝t̸̛͇̀̕"
146+
"""
147+
148+
source_file = %ElixirLS.LanguageServer.SourceFile{
149+
text: text,
150+
version: 1,
151+
dirty?: true
152+
}
153+
154+
project_dir = "/project"
155+
156+
assert {:ok, changes} = Formatting.format(source_file, uri, project_dir)
157+
158+
assert changes == [
159+
%{
160+
"newText" => ")",
161+
"range" => %{
162+
"end" => %{"character" => 213, "line" => 0},
163+
"start" => %{"character" => 213, "line" => 0}
164+
}
165+
},
166+
%{
167+
"newText" => "(",
168+
"range" => %{
169+
"end" => %{"character" => 8, "line" => 0},
170+
"start" => %{"character" => 7, "line" => 0}
171+
}
172+
}
173+
]
174+
end
70175
end

0 commit comments

Comments
 (0)