|
| 1 | +defmodule ElixirLS.LanguageServer.Experimental.SourceFile do |
| 2 | + alias ElixirLS.LanguageServer.Experimental.SourceFile.Conversions |
| 3 | + alias ElixirLS.LanguageServer.Experimental.SourceFile.Document |
| 4 | + alias ElixirLS.LanguageServer.Experimental.SourceFile.Position |
| 5 | + alias ElixirLS.LanguageServer.Experimental.SourceFile.Range |
| 6 | + alias ElixirLS.LanguageServer.SourceFile |
| 7 | + import ElixirLS.LanguageServer.Protocol, only: [range: 4] |
| 8 | + import ElixirLS.LanguageServer.Experimental.SourceFile.Line |
| 9 | + |
| 10 | + defstruct [:uri, :path, :version, dirty?: false, document: nil] |
| 11 | + |
| 12 | + @type t :: %__MODULE__{ |
| 13 | + uri: String.t(), |
| 14 | + version: pos_integer(), |
| 15 | + dirty?: boolean, |
| 16 | + document: Document.t(), |
| 17 | + path: String.t() |
| 18 | + } |
| 19 | + |
| 20 | + @type version :: pos_integer() |
| 21 | + @type change_application_error :: {:error, {:invalid_range, map()}} |
| 22 | + # public |
| 23 | + @spec new(URI.t(), String.t(), pos_integer()) :: t |
| 24 | + def new(uri, text, version) do |
| 25 | + %__MODULE__{ |
| 26 | + uri: uri, |
| 27 | + version: version, |
| 28 | + document: Document.new(text), |
| 29 | + path: SourceFile.Path.from_uri(uri) |
| 30 | + } |
| 31 | + end |
| 32 | + |
| 33 | + @spec mark_dirty(t) :: t |
| 34 | + def mark_dirty(%__MODULE__{} = source) do |
| 35 | + %__MODULE__{source | dirty?: true} |
| 36 | + end |
| 37 | + |
| 38 | + @spec mark_clean(t) :: t |
| 39 | + def mark_clean(%__MODULE__{} = source) do |
| 40 | + %__MODULE__{source | dirty?: false} |
| 41 | + end |
| 42 | + |
| 43 | + @spec fetch_text_at(t, version()) :: {:ok, String.t()} | :error |
| 44 | + def fetch_text_at(%__MODULE{} = source, line_number) do |
| 45 | + with {:ok, line(text: text)} <- Document.fetch_line(source.document, line_number) do |
| 46 | + {:ok, text} |
| 47 | + else |
| 48 | + _ -> |
| 49 | + :error |
| 50 | + end |
| 51 | + end |
| 52 | + |
| 53 | + @spec apply_content_changes(t, pos_integer(), [map]) :: |
| 54 | + {:ok, t} | change_application_error() |
| 55 | + def apply_content_changes(%__MODULE__{version: current_version}, new_version, _) |
| 56 | + when new_version <= current_version do |
| 57 | + {:error, :invalid_version} |
| 58 | + end |
| 59 | + |
| 60 | + def apply_content_changes(%__MODULE__{} = source, _, []) do |
| 61 | + {:ok, source} |
| 62 | + end |
| 63 | + |
| 64 | + def apply_content_changes(%__MODULE__{} = source, version, changes) when is_list(changes) do |
| 65 | + result = |
| 66 | + Enum.reduce_while(changes, source, fn change, source -> |
| 67 | + case apply_change(source, change) do |
| 68 | + {:ok, new_source} -> |
| 69 | + {:cont, new_source} |
| 70 | + |
| 71 | + error -> |
| 72 | + {:halt, error} |
| 73 | + end |
| 74 | + end) |
| 75 | + |
| 76 | + case result do |
| 77 | + %__MODULE__{} = source -> |
| 78 | + source = mark_dirty(%__MODULE__{source | version: version}) |
| 79 | + |
| 80 | + {:ok, source} |
| 81 | + |
| 82 | + error -> |
| 83 | + error |
| 84 | + end |
| 85 | + end |
| 86 | + |
| 87 | + def to_string(%__MODULE__{} = source) do |
| 88 | + source |
| 89 | + |> to_iodata() |
| 90 | + |> IO.iodata_to_binary() |
| 91 | + end |
| 92 | + |
| 93 | + # private |
| 94 | + |
| 95 | + defp line_count(%__MODULE__{} = source) do |
| 96 | + Document.size(source.document) |
| 97 | + end |
| 98 | + |
| 99 | + defp apply_change( |
| 100 | + %__MODULE__{} = source, |
| 101 | + %Range{start: %Position{} = start_pos, end: %Position{} = end_pos}, |
| 102 | + new_text |
| 103 | + ) do |
| 104 | + start_line = start_pos.line |
| 105 | + |
| 106 | + new_lines_iodata = |
| 107 | + cond do |
| 108 | + start_line > line_count(source) -> |
| 109 | + append_to_end(source, new_text) |
| 110 | + |
| 111 | + start_line <= 0 -> |
| 112 | + prepend_to_beginning(source, new_text) |
| 113 | + |
| 114 | + true -> |
| 115 | + apply_valid_edits(source, new_text, start_pos, end_pos) |
| 116 | + end |
| 117 | + |
| 118 | + new_document = |
| 119 | + new_lines_iodata |
| 120 | + |> IO.iodata_to_binary() |
| 121 | + |> Document.new() |
| 122 | + |
| 123 | + {:ok, %__MODULE__{source | document: new_document}} |
| 124 | + end |
| 125 | + |
| 126 | + defp apply_change( |
| 127 | + %__MODULE__{} = source, |
| 128 | + %{ |
| 129 | + "range" => range(start_line, start_char, end_line, end_char) = range, |
| 130 | + "text" => new_text |
| 131 | + } |
| 132 | + ) |
| 133 | + when start_line >= 0 and start_char >= 0 and end_line >= 0 and end_char >= 0 do |
| 134 | + with {:ok, ex_range} <- Conversions.to_elixir(range, source) do |
| 135 | + apply_change(source, ex_range, new_text) |
| 136 | + else |
| 137 | + _ -> |
| 138 | + {:error, {:invalid_range, range}} |
| 139 | + end |
| 140 | + end |
| 141 | + |
| 142 | + defp apply_change(%__MODULE__{}, %{"range" => invalid_range}) do |
| 143 | + {:error, {:invalid_range, invalid_range}} |
| 144 | + end |
| 145 | + |
| 146 | + defp apply_change( |
| 147 | + %__MODULE__{} = source, |
| 148 | + %{"text" => new_text} |
| 149 | + ) do |
| 150 | + {:ok, %__MODULE__{source | document: Document.new(new_text)}} |
| 151 | + end |
| 152 | + |
| 153 | + defp append_to_end(%__MODULE__{} = source, edit_text) do |
| 154 | + [to_iodata(source), edit_text] |
| 155 | + end |
| 156 | + |
| 157 | + defp prepend_to_beginning(%__MODULE__{} = source, edit_text) do |
| 158 | + [edit_text, to_iodata(source)] |
| 159 | + end |
| 160 | + |
| 161 | + defp apply_valid_edits(%__MODULE{} = source, edit_text, start_pos, end_pos) do |
| 162 | + Enum.reduce(source.document, [], fn line() = line, acc -> |
| 163 | + case edit_action(line, edit_text, start_pos, end_pos) do |
| 164 | + :drop -> |
| 165 | + acc |
| 166 | + |
| 167 | + {:append, io_data} -> |
| 168 | + [acc, io_data] |
| 169 | + end |
| 170 | + end) |
| 171 | + end |
| 172 | + |
| 173 | + defp edit_action(line() = line, edit_text, %Position{} = start_pos, %Position{} = end_pos) do |
| 174 | + %Position{line: start_line, character: start_char} = start_pos |
| 175 | + %Position{line: end_line, character: end_char} = end_pos |
| 176 | + |
| 177 | + line(line_number: line_number, text: text, ending: ending) = line |
| 178 | + |
| 179 | + cond do |
| 180 | + line_number < start_line -> |
| 181 | + {:append, [text, ending]} |
| 182 | + |
| 183 | + line_number > end_line -> |
| 184 | + {:append, [text, ending]} |
| 185 | + |
| 186 | + line_number == start_line && line_number == end_line -> |
| 187 | + prefix_text = utf8_prefix(text, start_char) |
| 188 | + suffix_text = utf8_suffix(text, end_char) |
| 189 | + |
| 190 | + {:append, [prefix_text, edit_text, suffix_text, ending]} |
| 191 | + |
| 192 | + line_number == start_line -> |
| 193 | + prefix_text = utf8_prefix(text, start_char) |
| 194 | + {:append, [prefix_text, edit_text]} |
| 195 | + |
| 196 | + line_number == end_line -> |
| 197 | + suffix_text = utf8_suffix(text, end_char) |
| 198 | + {:append, [suffix_text, ending]} |
| 199 | + |
| 200 | + true -> |
| 201 | + :drop |
| 202 | + end |
| 203 | + end |
| 204 | + |
| 205 | + defp utf8_prefix(text, start_index) do |
| 206 | + length = max(0, start_index) |
| 207 | + binary_part(text, 0, length) |
| 208 | + end |
| 209 | + |
| 210 | + defp utf8_suffix(text, start_index) do |
| 211 | + byte_count = byte_size(text) |
| 212 | + start_index = min(start_index, byte_count) |
| 213 | + length = byte_count - start_index |
| 214 | + binary_part(text, start_index, length) |
| 215 | + end |
| 216 | + |
| 217 | + defp to_iodata(%__MODULE__{} = source) do |
| 218 | + Document.to_iodata(source.document) |
| 219 | + end |
| 220 | + |
| 221 | + defp increment_version(%__MODULE__{} = source) do |
| 222 | + version = |
| 223 | + case source.version do |
| 224 | + v when is_integer(v) -> v + 1 |
| 225 | + _ -> 1 |
| 226 | + end |
| 227 | + |
| 228 | + %__MODULE__{source | version: version} |
| 229 | + end |
| 230 | +end |
0 commit comments