Skip to content

Commit 8d3cceb

Browse files
committed
Simplify and optimize fragment multi-line handling
Only convert lines to charlists when necessary and avoid doing multiple passes on the data.
1 parent 0b668af commit 8d3cceb

File tree

2 files changed

+103
-185
lines changed

2 files changed

+103
-185
lines changed

lib/elixir/lib/code/fragment.ex

Lines changed: 101 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -153,35 +153,10 @@ defmodule Code.Fragment do
153153
| {:dot, inside_dot, charlist}
154154
def cursor_context(fragment, opts \\ [])
155155

156-
def cursor_context(binary, opts) when is_binary(binary) and is_list(opts) do
157-
# CRLF not relevant here - we discard everything before last `\n`
158-
binary = last_line(binary)
159-
# case :binary.matches(binary, "\n") do
160-
# [] ->
161-
# binary
162-
163-
# matches ->
164-
# {position, _} = List.last(matches)
165-
# binary_part(binary, position + 1, byte_size(binary) - position - 1)
166-
# end
167-
168-
binary
169-
|> String.to_charlist()
170-
|> :lists.reverse()
171-
|> codepoint_cursor_context(opts)
172-
|> elem(0)
173-
end
174-
175-
def cursor_context(charlist, opts) when is_list(charlist) and is_list(opts) do
176-
# CRLF not relevant here - we discard everything before last `\n`
177-
# charlist =
178-
# case charlist |> Enum.chunk_by(&(&1 == ?\n)) |> List.last([]) do
179-
# [?\n | _] -> []
180-
# rest -> rest
181-
# end
182-
183-
charlist
184-
|> last_line
156+
def cursor_context(fragment, opts)
157+
when (is_binary(fragment) or is_list(fragment)) and is_list(opts) do
158+
fragment
159+
|> last_line()
185160
|> :lists.reverse()
186161
|> codepoint_cursor_context(opts)
187162
|> elem(0)
@@ -594,18 +569,19 @@ defmodule Code.Fragment do
594569
| {:dot, inside_dot, charlist}
595570
def surround_context(fragment, position, options \\ [])
596571

597-
def surround_context(string, {line, column}, opts) when is_binary(string) or is_list(string) do
598-
{binary, {lines_before_reversed_lengths, cursor_line_length, lines_after_lengths}} =
599-
join_lines(string, line, column)
572+
def surround_context(string, {line, column}, opts)
573+
when (is_binary(string) or is_list(string)) and is_list(opts) do
574+
{charlist, lines_before_lengths, lines_current_and_after_lengths} =
575+
surround_line(string, line, column)
600576

601-
prepended_columns = Enum.sum(lines_before_reversed_lengths)
577+
prepended_columns = Enum.sum(lines_before_lengths)
602578

603-
binary
604-
|> String.to_charlist()
579+
charlist
605580
|> position_surround_context(line, column + prepended_columns, opts)
606581
|> to_multiline_range(
607582
prepended_columns,
608-
{lines_before_reversed_lengths, cursor_line_length, lines_after_lengths}
583+
lines_before_lengths,
584+
lines_current_and_after_lengths
609585
)
610586
end
611587

@@ -810,22 +786,13 @@ defmodule Code.Fragment do
810786
defp enum_reverse_at([h | t], n, acc) when n > 0, do: enum_reverse_at(t, n - 1, [h | acc])
811787
defp enum_reverse_at(rest, _, acc), do: {acc, rest}
812788

813-
@comment_strip ~r/(?<!\<)\#(?!\{).*$/
814-
815789
defp last_line(binary) when is_binary(binary) do
816790
[last_line | lines_reverse] =
817791
binary
818792
|> String.split(["\r\n", "\n"])
819793
|> Enum.reverse()
820794

821-
lines_reverse =
822-
lines_reverse
823-
|> Enum.map(&Regex.replace(@comment_strip, &1, ""))
824-
825-
last_line = last_line
826-
827-
prepend_lines_before(last_line, lines_reverse)
828-
|> IO.chardata_to_string()
795+
prepend_cursor_lines(lines_reverse, String.to_charlist(last_line))
829796
end
830797

831798
defp last_line(charlist) when is_list(charlist) do
@@ -836,177 +803,128 @@ defmodule Code.Fragment do
836803
|> :string.split('\n', :all)
837804
|> Enum.reverse()
838805

839-
lines_reverse =
840-
lines_reverse
841-
|> Enum.map(&Regex.replace(@comment_strip, List.to_string(&1), ""))
842-
843-
last_line = last_line |> List.to_string()
844-
845-
prepend_lines_before(last_line, lines_reverse)
846-
|> IO.chardata_to_string()
847-
|> String.to_charlist()
848-
end
849-
850-
defp prepend_lines_before(cursor_line, lines_before_reverse) do
851-
lines_before_reverse
852-
|> Enum.reduce_while([cursor_line], fn line, [head | _] = acc ->
853-
line_trimmed = String.trim(line)
854-
acc_trimmed = String.trim_leading(head)
855-
856-
if String.starts_with?(acc_trimmed, ".") or line_trimmed == "" or
857-
String.ends_with?(line_trimmed, ".") or String.ends_with?(line_trimmed, "(") do
858-
{:cont, [line | acc]}
859-
else
860-
{:halt, acc}
861-
end
862-
end)
806+
prepend_cursor_lines(lines_reverse, last_line)
863807
end
864808

865-
defp append_lines_after(cursor_line, lines_after) do
866-
lines_after
867-
|> Enum.reduce_while([cursor_line], fn line, [head | _] = acc ->
868-
line_trimmed = String.trim_leading(line)
869-
acc_trimmed = String.trim(head)
870-
871-
if String.starts_with?(line_trimmed, ".") or acc_trimmed == "" or
872-
String.ends_with?(acc_trimmed, ".") or String.ends_with?(acc_trimmed, "(") do
873-
{:cont, [line | acc]}
874-
else
875-
{:halt, acc}
876-
end
877-
end)
878-
|> Enum.reverse()
809+
defp prepend_cursor_lines(lines, last_line) do
810+
with [line | lines] <- lines,
811+
{trimmed_line, incomplete?} = ends_as_incomplete(to_charlist(line), [], true),
812+
true <- incomplete? or starts_with_dot?(last_line) do
813+
prepend_cursor_lines(lines, Enum.reverse(trimmed_line, last_line))
814+
else
815+
_ -> last_line
816+
end
879817
end
880818

881-
def join_lines(binary, line, column) when is_binary(binary) do
882-
lines =
883-
binary
884-
|> String.split(["\r\n", "\n"])
885-
886-
lines_before_reverse =
887-
if line > 1 do
888-
lines
889-
|> Enum.slice(0..(line - 2))
890-
|> Enum.reverse()
891-
|> Enum.map(&Regex.replace(@comment_strip, &1, ""))
892-
else
893-
[]
894-
end
819+
defp starts_with_dot?([?. | _]), do: true
820+
defp starts_with_dot?([h | t]) when h in @space, do: starts_with_dot?(t)
821+
defp starts_with_dot?(_), do: false
895822

896-
lines_after =
897-
lines
898-
|> Enum.slice(line..-1)
899-
|> Enum.map(&Regex.replace(@comment_strip, &1, ""))
823+
defp ends_as_incomplete([?# | _], acc, incomplete?),
824+
do: {acc, incomplete?}
900825

901-
cursor_line =
902-
lines
903-
|> Enum.at(line - 1)
826+
defp ends_as_incomplete([h | t], acc, _incomplete?) when h in [?(, ?.],
827+
do: ends_as_incomplete(t, [h | acc], true)
904828

905-
cursor_line_stripped = Regex.replace(@comment_strip, cursor_line, "")
829+
defp ends_as_incomplete([h | t], acc, incomplete?) when h in @space,
830+
do: ends_as_incomplete(t, [h | acc], incomplete?)
906831

907-
cursor_line_stripped =
908-
if column - 1 > String.length(cursor_line_stripped) do
909-
cursor_line
910-
else
911-
cursor_line_stripped
912-
end
832+
defp ends_as_incomplete([h | t], acc, _incomplete?),
833+
do: ends_as_incomplete(t, [h | acc], false)
913834

914-
added_before_lines = prepend_lines_before(cursor_line_stripped, lines_before_reverse)
915-
[_ | added_after_lines] = append_lines_after(cursor_line_stripped, lines_after)
835+
defp ends_as_incomplete([], acc, incomplete?),
836+
do: {acc, incomplete?}
916837

917-
built =
918-
[added_before_lines, added_after_lines]
919-
|> IO.chardata_to_string()
838+
defp surround_line(binary, line, column) when is_binary(binary) do
839+
binary
840+
|> String.split(["\r\n", "\n"])
841+
|> Enum.map(&String.to_charlist/1)
842+
|> surround_lines(line, column)
843+
end
920844

921-
{built,
922-
{Enum.map(lines_before_reverse, &String.length/1), String.length(cursor_line_stripped),
923-
Enum.map(lines_after, &String.length/1)}}
845+
defp surround_line(charlist, line, column) when is_list(charlist) do
846+
charlist
847+
|> :string.replace('\r\n', '\n', :all)
848+
|> :string.join('')
849+
|> :string.split('\n', :all)
850+
|> surround_lines(line, column)
924851
end
925852

926-
def join_lines(charlist, line, column) when is_list(charlist) do
927-
lines =
928-
charlist
929-
|> :string.replace('\r\n', '\n', :all)
930-
|> :string.join('')
931-
|> :string.split('\n', :all)
853+
defp surround_lines(lines, line, column) do
854+
{lines_before_reverse, cursor_line, lines_after} = split_at(lines, line, [])
855+
{trimmed_cursor_line, incomplete?} = ends_as_incomplete(to_charlist(cursor_line), [], true)
932856

933-
lines_before_reverse =
934-
if line > 1 do
935-
lines
936-
|> Enum.slice(0..(line - 2))
937-
|> Enum.reverse()
938-
|> Enum.map(&Regex.replace(@comment_strip, List.to_string(&1), ""))
857+
reversed_cursor_line =
858+
if column - 1 > length(trimmed_cursor_line) do
859+
# Don't strip comments if cursor is inside a comment
860+
Enum.reverse(cursor_line)
939861
else
940-
[]
862+
trimmed_cursor_line
941863
end
942864

943-
lines_after =
944-
lines
945-
|> Enum.slice(line..-1)
946-
|> Enum.map(&Regex.replace(@comment_strip, List.to_string(&1), ""))
947-
948-
cursor_line =
949-
lines
950-
|> Enum.at(line - 1)
951-
|> List.to_string()
865+
{cursor_line, after_lengths} =
866+
append_surround_lines(lines_after, [], [reversed_cursor_line], incomplete?)
952867

953-
cursor_line_stripped = Regex.replace(@comment_strip, cursor_line, "")
954-
955-
cursor_line_stripped =
956-
if column - 1 > String.length(cursor_line_stripped) do
957-
# no comment strip if cursor is inside a comment
958-
# we want to provide results inside comment
959-
cursor_line
960-
else
961-
cursor_line_stripped
962-
end
868+
{cursor_line, before_lengths} = prepend_surround_lines(lines_before_reverse, [], cursor_line)
869+
{cursor_line, before_lengths, [length(reversed_cursor_line) | after_lengths]}
870+
end
963871

964-
added_before_lines = prepend_lines_before(cursor_line_stripped, lines_before_reverse)
965-
[_ | added_after_lines] = append_lines_after(cursor_line_stripped, lines_after)
872+
defp split_at([line], _, acc), do: {acc, line, []}
873+
defp split_at([line | lines], 1, acc), do: {acc, line, lines}
874+
defp split_at([line | lines], count, acc), do: split_at(lines, count - 1, [line | acc])
966875

967-
built =
968-
[added_before_lines, added_after_lines]
969-
|> IO.chardata_to_string()
876+
defp prepend_surround_lines(lines, lengths, last_line) do
877+
with [line | lines] <- lines,
878+
{trimmed_line, incomplete?} = ends_as_incomplete(to_charlist(line), [], true),
879+
true <- incomplete? or starts_with_dot?(last_line) do
880+
lengths = [length(trimmed_line) | lengths]
881+
prepend_surround_lines(lines, lengths, Enum.reverse(trimmed_line, last_line))
882+
else
883+
_ -> {last_line, Enum.reverse(lengths)}
884+
end
885+
end
970886

971-
{built,
972-
{Enum.map(lines_before_reverse, &String.length/1), String.length(cursor_line_stripped),
973-
Enum.map(lines_after, &String.length/1)}}
887+
defp append_surround_lines(lines, lengths, acc_lines, incomplete?) do
888+
with [line | lines] <- lines,
889+
line = to_charlist(line),
890+
true <- incomplete? or starts_with_dot?(line) do
891+
{trimmed_line, incomplete?} = ends_as_incomplete(line, [], true)
892+
lengths = [length(trimmed_line) | lengths]
893+
append_surround_lines(lines, lengths, [trimmed_line | acc_lines], incomplete?)
894+
else
895+
_ -> {Enum.reduce(acc_lines, [], &Enum.reverse/2), Enum.reverse(lengths)}
896+
end
974897
end
975898

976-
defp to_multiline_range(:none, _, _), do: :none
899+
defp to_multiline_range(:none, _, _, _), do: :none
977900

978901
defp to_multiline_range(
979902
%{begin: {begin_line, begin_column}, end: {end_line, end_column}} = context,
980903
prepended,
981-
{lines_before_reversed_lengths, cursor_line_length, lines_after_lengths}
904+
lines_before_lengths,
905+
lines_current_and_after_lengths
982906
) do
983907
{begin_line, begin_column} =
984-
lines_before_reversed_lengths
985-
|> Enum.reduce_while({begin_line, begin_column - prepended}, fn line_length,
986-
{acc_line, acc_column} ->
987-
if acc_column < 1 do
988-
{:cont, {acc_line - 1, acc_column + line_length}}
989-
else
990-
{:halt, {acc_line, acc_column}}
991-
end
908+
Enum.reduce_while(lines_before_lengths, {begin_line, begin_column - prepended}, fn
909+
line_length, {acc_line, acc_column} ->
910+
if acc_column < 1 do
911+
{:cont, {acc_line - 1, acc_column + line_length}}
912+
else
913+
{:halt, {acc_line, acc_column}}
914+
end
992915
end)
993916

994917
{end_line, end_column} =
995-
[cursor_line_length | lines_after_lengths]
996-
|> Enum.reduce_while({end_line, end_column - prepended}, fn line_length,
997-
{acc_line, acc_column} ->
998-
if acc_column > line_length + 1 do
999-
{:cont, {acc_line + 1, acc_column - line_length}}
1000-
else
1001-
{:halt, {acc_line, acc_column}}
1002-
end
918+
Enum.reduce_while(lines_current_and_after_lengths, {end_line, end_column - prepended}, fn
919+
line_length, {acc_line, acc_column} ->
920+
if acc_column > line_length + 1 do
921+
{:cont, {acc_line + 1, acc_column - line_length}}
922+
else
923+
{:halt, {acc_line, acc_column}}
924+
end
1003925
end)
1004926

1005-
context
1006-
|> Map.merge(%{
1007-
begin: {begin_line, begin_column},
1008-
end: {end_line, end_column}
1009-
})
927+
%{context | begin: {begin_line, begin_column}, end: {end_line, end_column}}
1010928
end
1011929

1012930
@doc """

lib/elixir/test/elixir/code_fragment_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,9 @@ defmodule CodeFragmentTest do
591591
end: {2, 7}
592592
}
593593

594-
assert CF.surround_context("hello. # comment\n\n wor", {3, 4}) == %{
594+
assert CF.surround_context("123 + hello. # comment\n\n wor", {3, 4}) == %{
595595
context: {:dot, {:var, 'hello'}, 'wor'},
596-
begin: {1, 1},
596+
begin: {1, 7},
597597
end: {3, 6}
598598
}
599599

0 commit comments

Comments
 (0)