From 060144f9c0cfca2933db490f3b352516b68bace5 Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Sat, 1 Jun 2024 12:46:10 -0400 Subject: [PATCH 1/3] Implement `Position.compare/2` --- .../lib/lexical/document/position.ex | 27 +++++++++++ .../test/lexical/document/position_test.exs | 45 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 projects/lexical_shared/test/lexical/document/position_test.exs diff --git a/projects/lexical_shared/lib/lexical/document/position.ex b/projects/lexical_shared/lib/lexical/document/position.ex index 53b2baf58..9738c10ef 100644 --- a/projects/lexical_shared/lib/lexical/document/position.ex +++ b/projects/lexical_shared/lib/lexical/document/position.ex @@ -70,6 +70,33 @@ defmodule Lexical.Document.Position do } end end + + @doc """ + Compares two positions. + + Returns `:gt`, `:lt`, or `:eq` depending on the location of the first + position relative to the second. + """ + @spec compare(t | {line, character}, t | {line, character}) :: :lt | :eq | :gt + def compare(%__MODULE__{} = pos1, %__MODULE__{} = pos2) do + compare({pos1.line, pos1.character}, {pos2.line, pos2.character}) + end + + def compare(%__MODULE__{} = pos1, pos2) do + compare({pos1.line, pos1.character}, pos2) + end + + def compare(pos1, %__MODULE__{} = pos2) do + compare(pos1, {pos2.line, pos2.character}) + end + + def compare({_, _} = first, {_, _} = second) do + cond do + first < second -> :lt + first > second -> :gt + true -> :eq + end + end end defimpl Inspect, for: Lexical.Document.Position do diff --git a/projects/lexical_shared/test/lexical/document/position_test.exs b/projects/lexical_shared/test/lexical/document/position_test.exs new file mode 100644 index 000000000..41561aff6 --- /dev/null +++ b/projects/lexical_shared/test/lexical/document/position_test.exs @@ -0,0 +1,45 @@ +defmodule Lexical.Document.PositionTest do + alias Lexical.Document.Line + alias Lexical.Document.Lines + alias Lexical.Document.Position + + import Line + + use ExUnit.Case, async: true + + describe "compare/2" do + test "positions on the same line" do + assert :eq = Position.compare(position(1, 10), position(1, 10)) + assert :gt = Position.compare(position(1, 11), position(1, 10)) + assert :lt = Position.compare(position(1, 9), position(1, 10)) + end + + test "position on earlier line" do + assert :lt = Position.compare(position(1, 10), position(2, 10)) + assert :lt = Position.compare(position(1, 11), position(2, 10)) + assert :lt = Position.compare(position(1, 9), position(2, 10)) + end + + test "position on later line" do + assert :gt = Position.compare(position(2, 10), position(1, 10)) + assert :gt = Position.compare(position(2, 11), position(1, 10)) + assert :gt = Position.compare(position(2, 9), position(1, 10)) + end + end + + defp position(line, character) do + stub_line = line(text: "", ending: "\n", line_number: line, ascii?: true) + + lines = + line + |> empty_lines() + |> put_in([Access.key(:lines), Access.elem(line - 1)], stub_line) + + Position.new(lines, line, character) + end + + defp empty_lines(length) do + tuple = List.to_tuple(for(x <- 1..length, do: x)) + %Lines{lines: tuple, starting_index: 1} + end +end From 1575ecaf5be30c2f73b64483e0f5553bdafc2087 Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Mon, 24 Jun 2024 11:26:12 -0400 Subject: [PATCH 2/3] Rewrite position comparisons in terms of `Position.compare/2` --- apps/common/lib/lexical/ast.ex | 42 ++++--------------- .../code_intelligence/variable.ex | 11 +---- 2 files changed, 10 insertions(+), 43 deletions(-) diff --git a/apps/common/lib/lexical/ast.ex b/apps/common/lib/lexical/ast.ex index a7d506dab..a8e4e2c00 100644 --- a/apps/common/lib/lexical/ast.ex +++ b/apps/common/lib/lexical/ast.ex @@ -317,21 +317,9 @@ defmodule Lexical.Ast do def contains_position?(ast, %Position{} = position) do case Sourceror.get_range(ast) do %{start: start_pos, end: end_pos} -> - on_same_line? = start_pos[:line] == end_pos[:line] and position.line == start_pos[:line] - - cond do - on_same_line? -> - position.character >= start_pos[:column] and position.character <= end_pos[:column] - - position.line == start_pos[:line] -> - position.character >= start_pos[:column] - - position.line == end_pos[:line] -> - position.character <= end_pos[:column] - - true -> - position.line > start_pos[:line] and position.line < end_pos[:line] - end + start_pos = {start_pos[:line], start_pos[:column]} + end_pos = {end_pos[:line], end_pos[:column]} + within?(position, start_pos, end_pos) nil -> false @@ -514,7 +502,7 @@ defmodule Lexical.Ast do fn %Zipper{node: node} = zipper, {last_position, acc} -> current_position = node_position(node, last_position) - if within_range?(current_position, range) do + if within?(current_position, range.start, range.end) do {zipper, new_acc} = fun.(zipper, acc) {:cont, zipper, {current_position, new_acc}} @@ -528,27 +516,15 @@ defmodule Lexical.Ast do end end - defp within_range?({current_line, current_column}, %Range{} = range) do - start_pos = %Position{} = range.start - end_pos = %Position{} = range.end - - cond do - current_line == start_pos.line -> - current_column >= start_pos.character - - current_line == end_pos.line -> - current_column <= end_pos.character - - true -> - current_line >= start_pos.line and current_line <= end_pos.line - end + defp within?(pos, start_pos, end_pos) do + Position.compare(pos, start_pos) in [:gt, :eq] and + Position.compare(pos, end_pos) in [:lt, :eq] end defp at_or_after?(node, %Position{} = position) do - line = get_line(node, 0) - column = get_column(node, 0) + node_position = node_position(node, {0, 0}) - line > position.line or (line == position.line and column >= position.character) + Position.compare(node_position, position) in [:gt, :eq] end defp one_line_range(%Document{} = document, line_number) do diff --git a/apps/remote_control/lib/lexical/remote_control/code_intelligence/variable.ex b/apps/remote_control/lib/lexical/remote_control/code_intelligence/variable.ex index 2a17e6851..241684574 100644 --- a/apps/remote_control/lib/lexical/remote_control/code_intelligence/variable.ex +++ b/apps/remote_control/lib/lexical/remote_control/code_intelligence/variable.ex @@ -82,8 +82,6 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Variable do definition_children = Map.get(block_id_to_children, definition_entry.block_id, []) - definition_start = definition_entry.range.start - # The algorithm here is to first clean up the entries so they either are definitions or references to a # variable with the given name. We sort them by their occurrence in the file, working backwards on a line, so # definitions earlier in the line shadow definitions later in the line. @@ -107,14 +105,7 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Variable do {entries, _, _} = entries |> Enum.filter(fn %Entry{} = entry -> - entry_start = entry.range.start - - after_definition? = - if entry_start.line == definition_start.line do - entry_start.character > definition_entry.range.end.character - else - entry_start.line > definition_start.line - end + after_definition? = Position.compare(entry.range.start, definition_entry.range.end) == :gt variable_type? = entry.type == :variable correct_subject? = entry.subject == definition_entry.subject From c6fb6f49d05ca215b272d0a649ccabc323cc5545 Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Sun, 2 Jun 2024 09:32:36 -0400 Subject: [PATCH 3/3] Guard for integers when comparing position tuples --- projects/lexical_shared/lib/lexical/document/position.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/lexical_shared/lib/lexical/document/position.ex b/projects/lexical_shared/lib/lexical/document/position.ex index 9738c10ef..86c9990ee 100644 --- a/projects/lexical_shared/lib/lexical/document/position.ex +++ b/projects/lexical_shared/lib/lexical/document/position.ex @@ -82,15 +82,16 @@ defmodule Lexical.Document.Position do compare({pos1.line, pos1.character}, {pos2.line, pos2.character}) end - def compare(%__MODULE__{} = pos1, pos2) do + def compare(%__MODULE__{} = pos1, {_, _} = pos2) do compare({pos1.line, pos1.character}, pos2) end - def compare(pos1, %__MODULE__{} = pos2) do + def compare({_, _} = pos1, %__MODULE__{} = pos2) do compare(pos1, {pos2.line, pos2.character}) end - def compare({_, _} = first, {_, _} = second) do + def compare({l1, c1} = first, {l2, c2} = second) + when is_integer(l1) and is_integer(c1) and is_integer(l2) and is_integer(c2) do cond do first < second -> :lt first > second -> :gt