defmodule Raxol.Terminal.Input.InputBufferUtils do @moduledoc """ Utility functions for Raxol.Terminal.Input.InputBuffer. """ # --- Wrapping Logic --- def wrap_line(line, width) do should_wrap = width > 0 and String.length(line) > width process_line_wrapping(should_wrap, line, width) end defp process_line_wrapping(false, line, _width), do: [line] defp process_line_wrapping(true, line, width) do # Split into words words = String.split(line, " ") Enum.reduce(words, {[], ""}, fn word, {lines, current_line} -> process_word(word, width, {lines, current_line}) end) |> finalize_wrapped_lines() end defp finalize_wrapped_lines({lines, last_line}) do add_final_line(last_line != "", lines, last_line) end defp add_final_line(false, lines, _last_line), do: lines defp add_final_line(true, lines, last_line), do: [last_line | lines] defp process_word(word, width, {lines, current_line}) do word_len = String.length(word) current_line_len = String.length(current_line) process_word_by_fit( word, word_len, width, lines, current_line, current_line_len ) end # Case 1: Word fits perfectly on empty current line (first word) defp process_word_by_fit(word, word_len, width, lines, "", _current_line_len) when word_len <= width do {lines, word} end # Case 2: Word fits on current line with a preceding space defp process_word_by_fit( word, word_len, width, lines, current_line, current_line_len ) when current_line != "" and current_line_len + 1 + word_len <= width do {lines, current_line <> " " <> word} end # Case 3: Word is too long for any line (longer than width) defp process_word_by_fit( word, word_len, width, lines, current_line, _current_line_len ) when word_len > width do handle_long_word(word, width, lines, current_line) end # Case 4: Word doesn't fit on current line, start a new line defp process_word_by_fit( word, _word_len, _width, lines, current_line, _current_line_len ) do {[current_line | lines], word} end defp handle_long_word(word, width, lines, current_line) do # Break the long word {new_lines, remaining_part} = break_long_word(word, width) # Add the completed current line (if any) and the broken parts updated_lines = add_current_line(current_line != "", current_line, lines) # Start new line with remaining part {new_lines ++ updated_lines, remaining_part} end defp add_current_line(false, _current_line, lines), do: lines defp add_current_line(true, current_line, lines), do: [current_line | lines] # Private helper for wrap_line defp break_long_word(word, width) do graphemes = String.graphemes(word) parts = Enum.chunk_every(graphemes, width) |> Enum.map(&Enum.join/1) # The last part might be shorter and becomes the start of the next line {Enum.slice(parts, 0..-2//1), List.last(parts)} end # --- Position Calculation Logic --- def find_logical_position(contents, cursor_pos) do logical_lines = String.split(contents, "\n") char_count = 0 Enum.find_value(Enum.with_index(logical_lines), {0, 0}, fn {line, index} -> line_len = String.length(line) # +1 for the newline character, except for the very last line line_len_with_newline = line_len + calculate_newline_length(index < length(logical_lines) - 1) check_cursor_position( cursor_pos <= char_count + line_len, index, cursor_pos, char_count, line_len_with_newline ) end) end # Renamed to v2 as it takes the pre-built mapping def calculate_new_cursor_pos_v2( # Map of %{old_logical_idx => [new_wrapped_idx1, new_wrapped_idx2...]} line_mapping, wrapped_lines_new, original_logical_line_index, original_pos_in_line, # Used for clamping new_contents ) do target_wrapped_line_indices = Map.get(line_mapping, original_logical_line_index, []) process_cursor_mapping( target_wrapped_line_indices == [], new_contents, target_wrapped_line_indices, wrapped_lines_new, original_logical_line_index, original_pos_in_line ) end defp process_cursor_mapping( true, new_contents, _target_wrapped_line_indices, _wrapped_lines_new, _original_logical_line_index, _original_pos_in_line ) do # Default to end if no mapping found (should indicate an issue) String.length(new_contents) end defp process_cursor_mapping( false, new_contents, target_wrapped_line_indices, wrapped_lines_new, _original_logical_line_index, original_pos_in_line ) do first_target_wrapped_index = Enum.min(target_wrapped_line_indices) # Calculate the starting character offset of the *first* wrapped line # belonging to the original logical line. start_char_offset = Enum.reduce(0..(first_target_wrapped_index - 1), 0, fn i, acc -> # Need to handle potential index out of bounds if mapping is sparse? line_content = Enum.at(wrapped_lines_new, i) # +1 for newline implicitly separating wrapped lines acc + String.length(line_content) + 1 end) # Iterate through ONLY the target wrapped lines originating from the original logical line # to find the character offset *within* this sequence of lines. final_cursor_offset_within_logical_line = find_cursor_offset_in_target_sequence( target_wrapped_line_indices, wrapped_lines_new, original_pos_in_line ) # If reduce_while finished without halting (e.g., original_pos_in_line # was > total length of sequence), default to end of last wrapped line. final_cursor_offset_within_logical_line = resolve_cursor_offset( is_integer(final_cursor_offset_within_logical_line), final_cursor_offset_within_logical_line, target_wrapped_line_indices, wrapped_lines_new ) final_pos = start_char_offset + final_cursor_offset_within_logical_line # Clamp to total length as a safety measure min(final_pos, String.length(new_contents)) end defp calculate_total_length_of_target_sequence( target_wrapped_line_indices, wrapped_lines_new ) do Enum.reduce(target_wrapped_line_indices, 0, fn idx, acc -> acc + String.length(Enum.at(wrapped_lines_new, idx)) end) end defp find_cursor_offset_in_target_sequence( target_wrapped_line_indices, wrapped_lines_new, original_pos_in_line ) do Enum.reduce_while(target_wrapped_line_indices, 0, fn wrapped_idx, pos_within_target_sequence -> line = Enum.at(wrapped_lines_new, wrapped_idx) line_len = String.length(line) handle_cursor_in_line( original_pos_in_line <= pos_within_target_sequence + line_len, original_pos_in_line, pos_within_target_sequence, line_len ) end) end ## Helper Functions for Pattern Matching defp calculate_newline_length(true), do: 1 defp calculate_newline_length(false), do: 0 defp check_cursor_position( true, index, cursor_pos, char_count, _line_len_with_newline ) do # Cursor is within this logical line {index, cursor_pos - char_count} end defp check_cursor_position( false, _index, _cursor_pos, char_count, line_len_with_newline ) do _char_count = char_count + line_len_with_newline # Continue searching nil end defp resolve_cursor_offset( true, final_cursor_offset_within_logical_line, _target_wrapped_line_indices, _wrapped_lines_new ) do final_cursor_offset_within_logical_line end defp resolve_cursor_offset( false, _final_cursor_offset_within_logical_line, target_wrapped_line_indices, wrapped_lines_new ) do calculate_total_length_of_target_sequence( target_wrapped_line_indices, wrapped_lines_new ) end defp handle_cursor_in_line( true, original_pos_in_line, pos_within_target_sequence, _line_len ) do # Cursor position falls within this wrapped line. The offset is the # original position minus the length of preceding lines within sequence. {:halt, original_pos_in_line - pos_within_target_sequence} end defp handle_cursor_in_line( false, _original_pos_in_line, pos_within_target_sequence, line_len ) do # Cursor is beyond this wrapped line, continue checking the next one # from the same logical origin. {:cont, pos_within_target_sequence + line_len} end end