Skip to content

Commit 4e7e2c1

Browse files
scohenscottming
authored andcommitted
Added fragment capabilities to document (lexical-lsp#170)
While talking with Jose Valim about how he could help lexical, we discussed giving a document fragment to a function in Code.Fragment. It seemed to me at the time that one of the functions already there might fit the bill, so I coded up the ability to request a fragment of a document using positions. Unfortunately, the function in question won't work for us, but I felt that document fragments might be helpful in the future, so here's a PR.
1 parent 48bc736 commit 4e7e2c1

2 files changed

Lines changed: 155 additions & 0 deletions

File tree

apps/common/lib/lexical/document.ex

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Lexical.Document do
1212
alias Lexical.Document.Lines
1313
alias Lexical.Document.Position
1414
alias Lexical.Document.Range
15+
alias Lexical.Math
1516

1617
import Lexical.Document.Line
1718

@@ -22,6 +23,7 @@ defmodule Lexical.Document do
2223
defstruct [:uri, :path, :version, dirty?: false, lines: nil]
2324

2425
@type version :: non_neg_integer()
26+
@type fragment_position :: Position.t() | Convertible.t()
2527
@type t :: %__MODULE__{
2628
uri: String.t(),
2729
version: version(),
@@ -96,6 +98,59 @@ defmodule Lexical.Document do
9698
end
9799
end
98100

101+
@doc """
102+
Returns a fragment defined by the from and to arguments
103+
104+
Builds a string that represents the text of the document from the two positions given.
105+
The from position, defaults to `:beginning` meaning the start of the document.
106+
Positions can be a `Document.Position.t` or anything that will convert to a position using
107+
`Convertible.to_native/2`.
108+
"""
109+
@spec fragment(t, fragment_position() | :beginning, fragment_position()) :: String.t()
110+
@spec fragment(t, fragment_position()) :: String.t()
111+
def fragment(%__MODULE__{} = document, from \\ :beginning, to) do
112+
line_count = size(document)
113+
from_pos = convert_fragment_position(document, from)
114+
to_pos = convert_fragment_position(document, to)
115+
116+
from_line = Math.clamp(from_pos.line, document.lines.starting_index, line_count)
117+
to_line = Math.clamp(to_pos.line, from_line, line_count + 1)
118+
119+
# source code positions are 1 based, but string slices are zero-based. Need an ad-hoc conversion
120+
# here.
121+
from_character = from_pos.character - 1
122+
to_character = to_pos.character - 1
123+
124+
line_range = from_line..to_line
125+
126+
line_range
127+
|> Enum.reduce([], fn line_number, acc ->
128+
to_append =
129+
case fetch_line_at(document, line_number) do
130+
{:ok, line(text: text, ending: ending)} ->
131+
line_text = text <> ending
132+
133+
cond do
134+
line_number == from_line ->
135+
slice_length = String.length(line_text) - from_character
136+
String.slice(line_text, from_character, slice_length)
137+
138+
line_number == to_line ->
139+
String.slice(line_text, 0, to_character)
140+
141+
true ->
142+
line_text
143+
end
144+
145+
:error ->
146+
[]
147+
end
148+
149+
[acc, to_append]
150+
end)
151+
|> IO.iodata_to_binary()
152+
end
153+
99154
@spec apply_content_changes(t, version(), [Convertible.t() | nil]) ::
100155
{:ok, t} | change_application_error()
101156
def apply_content_changes(%__MODULE__{version: current_version}, new_version, _)
@@ -271,4 +326,18 @@ defmodule Lexical.Document do
271326
defp to_iodata(%__MODULE__{} = document) do
272327
Lines.to_iodata(document.lines)
273328
end
329+
330+
@spec convert_fragment_position(t, Position.t() | :beginning | Convertible.t()) :: Position.t()
331+
defp convert_fragment_position(%__MODULE__{}, %Position{} = pos) do
332+
pos
333+
end
334+
335+
defp convert_fragment_position(%__MODULE__{}, :beginning) do
336+
Position.new(1, 1)
337+
end
338+
339+
defp convert_fragment_position(%__MODULE__{} = document, convertible) do
340+
{:ok, %Position{} = converted} = Convertible.to_native(convertible, document)
341+
converted
342+
end
274343
end

apps/server/test/document_test.exs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,4 +638,90 @@ defmodule Lexical.DocumentTest do
638638
])
639639
end
640640
end
641+
642+
def document(text) do
643+
new(file_uri(), String.trim(text), 0)
644+
end
645+
646+
describe "fragment/2" do
647+
test "works with out of bounds lines" do
648+
doc =
649+
"""
650+
one
651+
two
652+
three
653+
"""
654+
|> document()
655+
|> Document.fragment(new_position(3, 0))
656+
657+
assert "one\ntwo\nthree" == doc
658+
end
659+
660+
test "can omit entire lines" do
661+
doc =
662+
"""
663+
one
664+
two
665+
three
666+
"""
667+
|> document()
668+
|> Document.fragment(new_position(2, 0))
669+
670+
assert "one\ntwo\n" == doc
671+
end
672+
673+
test "can truncate the end of a line" do
674+
doc =
675+
"""
676+
one
677+
two
678+
three
679+
"""
680+
|> document()
681+
|> Document.fragment(new_position(2, 2))
682+
683+
assert "one\ntwo\nth" == doc
684+
end
685+
end
686+
687+
describe "fragment/3" do
688+
test "can omit an entire line" do
689+
doc =
690+
"""
691+
one
692+
two
693+
three
694+
"""
695+
|> document()
696+
|> Document.fragment(new_position(1, 0), new_position(3, 0))
697+
698+
assert "two\nthree" == doc
699+
end
700+
701+
test "can truncate the beginning of a line" do
702+
doc =
703+
"""
704+
one
705+
two
706+
three
707+
"""
708+
|> document()
709+
|> Document.fragment(new_position(0, 2), new_position(3, 0))
710+
711+
assert "e\ntwo\nthree" == doc
712+
end
713+
714+
test "can truncate both the beginning and end of a line" do
715+
doc =
716+
"""
717+
one
718+
two
719+
three
720+
"""
721+
|> document()
722+
|> Document.fragment(new_position(0, 2), new_position(1, 1))
723+
724+
assert "e\nt" == doc
725+
end
726+
end
641727
end

0 commit comments

Comments
 (0)