Skip to content

Commit 55e09e8

Browse files
committed
fix(engine): improve entity resolution for HEEx components with curly braces
Previous implementation had a bug where if there was a curly braces expression on the line with a HEEx component, its arity was always taken, even if the cursor was on the component. This lead to incorrect resolution (usually to not be able to resolve the component call).
1 parent 9e913f6 commit 55e09e8

File tree

2 files changed

+115
-11
lines changed

2 files changed

+115
-11
lines changed

apps/engine/lib/engine/code_intelligence/heex.ex

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@ defmodule Engine.CodeIntelligence.Heex do
4545
def arity({:sigil_H, meta, [{:<<>>, _, parts}, _]}, position, arity_at_position) do
4646
content = sigil_content(parts)
4747
sigil_start_line = Keyword.get(meta, :line, 1)
48+
sigil_start_column = Keyword.get(meta, :column, 1)
4849
relative_line = position.line - sigil_start_line
50+
# calculate relative column (only meaningful when on first line of sigil content)
51+
relative_column = position.character - sigil_start_column
4952

5053
with {:ok, tokens} <- EEx.tokenize(content),
51-
{:ok, expr} <- find_expr_at(tokens, relative_line),
54+
{:ok, expr} <- find_expr_at(tokens, relative_line, relative_column),
5255
{:ok, ast} <- Code.string_to_quoted(List.to_string(expr)) do
5356
arity_at_position.([ast], position)
5457
else
55-
# Component shorthand like `<.button>` - after normalization has arity 1
58+
# component shorthand like `<.button>` - after normalization has arity 1
5659
:component_shorthand -> 1
5760
_ -> 0
5861
end
@@ -202,33 +205,64 @@ defmodule Engine.CodeIntelligence.Heex do
202205
end)
203206
end
204207

205-
defp find_expr_at(tokens, target_line) do
208+
defp find_expr_at(tokens, target_line, target_column) do
206209
Enum.find_value(tokens, :component_shorthand, fn
207-
{:expr, _marker, expr, %{line: line}} when line == target_line ->
208-
{:ok, expr}
210+
{:expr, _marker, expr, %{line: line, column: col}} when line == target_line ->
211+
# check if cursor is within this expression's column range
212+
expr_length = length(expr)
209213

210-
{:text, text, %{line: start_line}} ->
214+
if target_column >= col and target_column <= col + expr_length do
215+
{:ok, expr}
216+
else
217+
nil
218+
end
219+
220+
{:text, text, %{line: start_line, column: start_col}} ->
211221
text_str = List.to_string(text)
212222
line_in_text = target_line - start_line
213-
find_curly_expr_at_line(text_str, line_in_text)
223+
# calculate column offset within the text
224+
text_column = if line_in_text == 0, do: target_column - start_col, else: target_column
225+
find_curly_expr_at_line(text_str, line_in_text, text_column)
214226

215227
_ ->
216228
nil
217229
end)
218230
end
219231

220-
defp find_curly_expr_at_line(text, line_offset) do
232+
defp find_curly_expr_at_line(text, line_offset, cursor_column) do
221233
lines = String.split(text, "\n")
222234

223235
if line_offset >= 0 and line_offset < length(lines) do
224236
line = Enum.at(lines, line_offset)
225237

226-
case Regex.run(~r/\{([^{}]+)\}/, line) do
227-
[_, expr] -> {:ok, String.to_charlist(expr)}
228-
_ -> nil
238+
# check if cursor on a component shorthand
239+
if cursor_on_component_shorthand?(line, cursor_column) do
240+
:component_shorthand
241+
else
242+
# check if cursor inside a curly expression
243+
find_curly_expr_at_column(line, cursor_column)
229244
end
230245
else
231246
nil
232247
end
233248
end
249+
250+
defp cursor_on_component_shorthand?(line, cursor_column) do
251+
Regex.scan(@component_regex, line, return: :index)
252+
|> Enum.any?(fn [{match_start, match_len} | _] ->
253+
cursor_column >= match_start and cursor_column < match_start + match_len
254+
end)
255+
end
256+
257+
defp find_curly_expr_at_column(line, cursor_column) do
258+
Regex.scan(~r/\{([^{}]+)\}/, line, return: :index)
259+
|> Enum.find_value(fn [{match_start, match_len}, {expr_start, expr_len}] ->
260+
if cursor_column >= match_start and cursor_column < match_start + match_len do
261+
expr = binary_part(line, expr_start, expr_len)
262+
{:ok, String.to_charlist(expr)}
263+
else
264+
nil
265+
end
266+
end)
267+
end
234268
end

apps/engine/test/engine/code_intelligence/entity_test.exs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,76 @@ defmodule Engine.CodeIntelligence.EntityTest do
983983
assert {:ok, {:call, MyLiveView, :button, 1}, _} = resolve(code)
984984
end
985985

986+
test "resolves shorthand component without closing tag with correct arity" do
987+
code = ~q[
988+
defmodule MyLiveView do
989+
use Phoenix.Component
990+
991+
def render(assigns) do
992+
~H"""
993+
<.but|ton label="Click" />
994+
"""
995+
end
996+
997+
def button(assigns), do: nil
998+
end
999+
]
1000+
1001+
assert {:ok, {:call, MyLiveView, :button, 1}, _} = resolve(code)
1002+
end
1003+
1004+
test "resolves shorthand component with curly braces with correct arity" do
1005+
code = ~q[
1006+
defmodule MyLiveView do
1007+
use Phoenix.Component
1008+
1009+
def render(assigns) do
1010+
~H"""
1011+
<.but|ton label={label} />
1012+
"""
1013+
end
1014+
1015+
def button(assigns), do: nil
1016+
end
1017+
]
1018+
1019+
assert {:ok, {:call, MyLiveView, :button, 1}, _} = resolve(code)
1020+
end
1021+
1022+
test "resolves shorthand component with curly braces on the first line of sigil with correct arity" do
1023+
code = ~q[
1024+
defmodule MyLiveView do
1025+
use Phoenix.Component
1026+
1027+
def render(assigns) do
1028+
~H"<.butto|n label={label} />"
1029+
end
1030+
1031+
def button(assigns), do: nil
1032+
end
1033+
]
1034+
1035+
assert {:ok, {:call, MyLiveView, :button, 1}, _} = resolve(code)
1036+
end
1037+
1038+
test "resolves function called inside shorthand component with curly braces with correct arity" do
1039+
code = ~q[
1040+
defmodule MyLiveView do
1041+
use Phoenix.Component
1042+
1043+
def render(assigns) do
1044+
~H"""
1045+
<.button label={LabelGenerator.for_bu|tton("label", in_live_view: true, language: :en)} />
1046+
"""
1047+
end
1048+
1049+
def button(assigns), do: nil
1050+
end
1051+
]
1052+
1053+
assert {:ok, {:call, LabelGenerator, :for_button, 2}, _} = resolve(code)
1054+
end
1055+
9861056
test "resolves EEx expression with arity 1" do
9871057
code = ~q[
9881058
defmodule MyLiveView do

0 commit comments

Comments
 (0)