Skip to content

Commit e7c7a54

Browse files
committed
improve reserved word completions
attempt to correctly handle indentation with after, else, catch, rescue handle atom forms of keywords make sure do is suggested even if there are whitespaces after
1 parent 372ef9b commit e7c7a54

File tree

2 files changed

+118
-54
lines changed

2 files changed

+118
-54
lines changed

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

Lines changed: 117 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
1010
alias ElixirLS.LanguageServer.Protocol.TextEdit
1111
alias ElixirLS.LanguageServer.SourceFile
1212
import ElixirLS.LanguageServer.Protocol, only: [range: 4]
13+
alias ElixirSense.Providers.Suggestion.Matcher
1314

1415
@enforce_keys [:label, :kind, :insert_text, :priority, :tags]
1516
defstruct [
@@ -18,14 +19,16 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
1819
:detail,
1920
:documentation,
2021
:insert_text,
22+
:insert_text_mode,
2123
:filter_text,
2224
# Lower priority is shown higher in the result list
2325
:priority,
2426
:label_details,
2527
:tags,
2628
:command,
2729
{:preselect, false},
28-
:additional_text_edit
30+
:additional_text_edit,
31+
:text_edit
2932
]
3033

3134
@func_snippets %{
@@ -90,10 +93,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
9093
end
9194

9295
def completion(text, line, character, options) do
93-
line_text =
94-
text
95-
|> SourceFile.lines()
96-
|> Enum.at(line)
96+
lines = SourceFile.lines(text)
97+
line_text = Enum.at(lines, line)
9798

9899
# convert to 1 based utf8 position
99100
line = line + 1
@@ -132,6 +133,18 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
132133
nil
133134
end
134135

136+
do_block_indent =
137+
lines
138+
|> Enum.slice(0..(line - 1))
139+
|> Enum.reverse()
140+
|> Enum.find_value(0, fn line_text ->
141+
if Regex.match?(~r/(?<=\s|^)do\s*(#.*)?$/, line_text) do
142+
String.length(line_text) - String.length(String.trim_leading(line_text))
143+
end
144+
end)
145+
146+
line_indent = String.length(line_text) - String.length(String.trim_leading(line_text))
147+
135148
context = %{
136149
text_before_cursor: text_before_cursor,
137150
text_after_cursor: text_after_cursor,
@@ -141,7 +154,11 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
141154
pipe_before?: Regex.match?(~r/\|>\s*#{prefix}$/, text_before_cursor),
142155
capture_before?: Regex.match?(~r/&#{prefix}$/, text_before_cursor),
143156
scope: scope,
144-
module: env.module
157+
module: env.module,
158+
line: line,
159+
character: character,
160+
do_block_indent: do_block_indent,
161+
line_indent: line_indent
145162
}
146163

147164
position_to_insert_alias =
@@ -160,7 +177,6 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
160177
|> maybe_reject_derived_functions(context, options)
161178
|> Enum.map(&from_completion_item(&1, context, options))
162179
|> maybe_add_do(context)
163-
|> maybe_add_end(context)
164180
|> maybe_add_keywords(context)
165181
|> Enum.reject(&is_nil/1)
166182
|> sort_items()
@@ -204,33 +220,23 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
204220
end
205221

206222
defp maybe_add_do(completion_items, context) do
207-
if String.ends_with?(context.text_before_cursor, " do") && context.text_after_cursor == "" do
223+
hint =
224+
case Regex.scan(~r/(?<=\s|^)[a-z]+$/, context.text_before_cursor) do
225+
[] -> ""
226+
[[match]] -> match
227+
end
228+
229+
if hint in ["d", "do"] do
208230
item = %__MODULE__{
209231
label: "do",
210232
kind: :keyword,
211-
detail: "keyword",
212-
insert_text: "do\n $0\nend",
233+
detail: "reserved word",
234+
insert_text:
235+
if(String.trim(context.text_after_cursor) == "", do: "do\n $0\nend", else: "do: "),
213236
tags: [],
214237
priority: 0,
215238
# force selection over other longer not exact completions
216-
preselect: true
217-
}
218-
219-
[item | completion_items]
220-
else
221-
completion_items
222-
end
223-
end
224-
225-
defp maybe_add_end(completion_items, context) do
226-
if String.ends_with?(context.text_before_cursor, "end") && context.text_after_cursor == "" do
227-
item = %__MODULE__{
228-
label: "end",
229-
kind: :keyword,
230-
detail: "keyword",
231-
insert_text: "end",
232-
tags: [],
233-
priority: 0
239+
preselect: hint == "do"
234240
}
235241

236242
[item | completion_items]
@@ -239,40 +245,84 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
239245
end
240246
end
241247

242-
defp maybe_add_keywords(completion_items, %{text_after_cursor: ""} = context) do
243-
kw = Map.get(context, :text_before_cursor) |> String.trim_leading() |> get_keyword()
248+
defp maybe_add_keywords(completion_items, context) do
249+
hint =
250+
case Regex.scan(~r/(?<=\s|^)[a-z]+$/, context.text_before_cursor) do
251+
[] -> ""
252+
[[match]] -> match
253+
end
244254

245-
if kw != "" do
246-
item = %__MODULE__{
247-
label: kw,
248-
kind: :keyword,
249-
detail: "keyword",
250-
insert_text: kw,
251-
tags: [],
252-
priority: 0
253-
}
255+
if hint != "" do
256+
keyword_items =
257+
for keyword <- ~w(true false nil when end rescue catch else after),
258+
Matcher.match?(keyword, hint) do
259+
{insert_text, text_edit} =
260+
cond do
261+
keyword in ~w(rescue catch else after) ->
262+
if String.trim(context.text_after_cursor) == "" do
263+
{nil,
264+
%{
265+
"range" => %{
266+
"start" => %{
267+
"line" => context.line - 1,
268+
"character" =>
269+
context.character - String.length(hint) - 1 -
270+
(context.line_indent - context.do_block_indent)
271+
},
272+
"end" => %{
273+
"line" => context.line - 1,
274+
"character" => context.character - 1
275+
}
276+
},
277+
"newText" => "#{keyword}\n "
278+
}}
279+
else
280+
{"#{keyword}: ", nil}
281+
end
282+
283+
keyword == "when" ->
284+
{"when ", nil}
285+
286+
keyword == "end" ->
287+
{nil,
288+
%{
289+
"range" => %{
290+
"start" => %{
291+
"line" => context.line - 1,
292+
"character" =>
293+
context.character - String.length(hint) - 1 -
294+
(context.line_indent - context.do_block_indent)
295+
},
296+
"end" => %{"line" => context.line - 1, "character" => context.character - 1}
297+
},
298+
"newText" => "end\n"
299+
}}
300+
301+
true ->
302+
{keyword, nil}
303+
end
304+
305+
%__MODULE__{
306+
label: keyword,
307+
kind: :keyword,
308+
detail: "reserved word",
309+
insert_text: insert_text,
310+
text_edit: text_edit,
311+
tags: [],
312+
priority: 0,
313+
insert_text_mode: 2,
314+
preselect: hint == keyword
315+
}
316+
end
254317

255-
[item | completion_items]
318+
keyword_items ++ completion_items
256319
else
257320
completion_items
258321
end
259322
end
260323

261-
defp maybe_add_keywords(completion_items, _context) do
262-
completion_items
263-
end
264-
265324
## Helpers
266325

267-
defp get_keyword(t) do
268-
cond do
269-
Enum.member?(["t", "tr", "tru", "true"], t) -> "true"
270-
Enum.member?(["f", "fa", "fal", "fals", "false"], t) -> "false"
271-
Enum.member?(["n", "ni", "nil"], t) -> "nil"
272-
true -> ""
273-
end
274-
end
275-
276326
defp is_incomplete(items) do
277327
if Enum.empty?(items) do
278328
false
@@ -1254,6 +1304,20 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
12541304
json
12551305
end
12561306

1307+
json =
1308+
if item.insert_text_mode do
1309+
Map.put(json, "insertTextMode", item.insert_text_mode)
1310+
else
1311+
json
1312+
end
1313+
1314+
json =
1315+
if item.text_edit do
1316+
Map.put(json, "textEdit", item.text_edit)
1317+
else
1318+
json
1319+
end
1320+
12571321
# deprecated as of Language Server Protocol Specification - 3.15
12581322
json =
12591323
if Keyword.get(options, :deprecated_supported, false) do

apps/language_server/test/providers/completion_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
710710
[item] = items |> Enum.filter(&(&1["insertText"] == "true"))
711711

712712
assert %{
713-
"detail" => "keyword",
713+
"detail" => "reserved word",
714714
"documentation" => %{:kind => "markdown", "value" => ""},
715715
"insertText" => "true",
716716
"insertTextFormat" => 2,

0 commit comments

Comments
 (0)