diff --git a/apps/expert/lib/expert/code_intelligence/completion/builder.ex b/apps/expert/lib/expert/code_intelligence/completion/builder.ex index 28c4de94..47308e4f 100644 --- a/apps/expert/lib/expert/code_intelligence/completion/builder.ex +++ b/apps/expert/lib/expert/code_intelligence/completion/builder.ex @@ -108,7 +108,7 @@ defmodule Expert.CodeIntelligence.Completion.Builder do defp prefix_range(%Env{} = env) do end_char = env.position.character - start_char = end_char - prefix_length(env) + start_char = max(end_char - prefix_length(env), 1) {start_char, end_char} end diff --git a/apps/expert/test/expert/code_intelligence/completion/builder_test.exs b/apps/expert/test/expert/code_intelligence/completion/builder_test.exs index 66552733..529a7ddd 100644 --- a/apps/expert/test/expert/code_intelligence/completion/builder_test.exs +++ b/apps/expert/test/expert/code_intelligence/completion/builder_test.exs @@ -2,6 +2,9 @@ defmodule Expert.CodeIntelligence.Completion.BuilderTest do alias Expert.CodeIntelligence.Completion.SortScope alias Forge.Ast alias Forge.Ast.Env + alias Forge.Document + alias Forge.Document.Position + alias Forge.Protocol.Convertible alias GenLSP.Structures.CompletionItem use ExUnit.Case, async: true @@ -82,4 +85,46 @@ defmodule Expert.CodeIntelligence.Completion.BuilderTest do |> snippet("", label: "") end end + + describe "non-ascii line range clamp" do + test "plain_text clamps start_char >= 1 and serializes on non-ASCII line" do + doc = Document.new("file:///builder_test.ex", "⚠️ hello", 0) + pos = Position.new(doc, 1, 1) + + env = %Env{document: doc, position: pos, prefix: "a"} + + item = plain_text(env, "X", label: "X") + + assert {:ok, lsp_text_edit_or_list} = Convertible.to_lsp(item.text_edit) + + lsp_edits = List.wrap(lsp_text_edit_or_list) + + refute Enum.empty?(lsp_edits) + + for %{range: %{start: %{character: start_ch}, end: %{character: end_ch}}} <- lsp_edits do + assert start_ch == 0 + assert end_ch == 0 + end + end + + test "snippet clamps start_char >= 1 and serializes on non-ASCII line" do + doc = Document.new("file:///builder_test.ex", "⚠️ hello", 0) + pos = Position.new(doc, 1, 1) + + env = %Env{document: doc, position: pos, prefix: "a"} + + item = snippet(env, "X$0", label: "X") + + assert {:ok, lsp_text_edit_or_list} = Convertible.to_lsp(item.text_edit) + + lsp_edits = List.wrap(lsp_text_edit_or_list) + + refute Enum.empty?(lsp_edits) + + for %{range: %{start: %{character: start_ch}, end: %{character: end_ch}}} <- lsp_edits do + assert start_ch == 0 + assert end_ch == 0 + end + end + end end