refactor: Improve change navigation with block-based approach and context scrolling
This commit is contained in:
parent
60b3af9108
commit
bbfba3c644
110
gtm
110
gtm
|
|
@ -134,25 +134,47 @@ class AppState:
|
|||
wrap_lines: bool = True
|
||||
|
||||
# Change navigation
|
||||
change_positions: List[int] = field(default_factory=list)
|
||||
change_blocks: List[Tuple[int, int]] = field(default_factory=list) # (start_line, end_line) of each change block
|
||||
current_change_idx: int = -1 # -1 means no change is selected
|
||||
|
||||
# --- Actions (Controller) ---
|
||||
|
||||
def calculate_change_positions(state: AppState) -> AppState:
|
||||
"""Calculate positions of all changes (additions and deletions) in the file."""
|
||||
positions = []
|
||||
# Add positions of added lines
|
||||
def calculate_change_blocks(state: AppState) -> AppState:
|
||||
"""Calculate positions of change blocks (consecutive changes) in the file."""
|
||||
if not state.added_lines and not state.deleted_lines:
|
||||
return replace(state, change_blocks=[], current_change_idx=-1)
|
||||
|
||||
# Collect all change positions
|
||||
all_changes = []
|
||||
for line_num, _ in state.added_lines:
|
||||
if line_num not in positions:
|
||||
positions.append(line_num)
|
||||
# Add positions of deleted lines
|
||||
all_changes.append(line_num)
|
||||
for line_num, _ in state.deleted_lines:
|
||||
if line_num not in positions:
|
||||
positions.append(line_num)
|
||||
# Sort positions
|
||||
positions.sort()
|
||||
return replace(state, change_positions=positions, current_change_idx=-1)
|
||||
all_changes.append(line_num)
|
||||
|
||||
# Sort all positions
|
||||
all_changes.sort()
|
||||
|
||||
if not all_changes:
|
||||
return replace(state, change_blocks=[], current_change_idx=-1)
|
||||
|
||||
# Group into blocks (changes within 3 lines of each other are considered one block)
|
||||
blocks = []
|
||||
current_block_start = all_changes[0]
|
||||
current_block_end = all_changes[0]
|
||||
|
||||
for pos in all_changes[1:]:
|
||||
if pos <= current_block_end + 3: # Within 3 lines of previous change
|
||||
current_block_end = pos
|
||||
else:
|
||||
# Start a new block
|
||||
blocks.append((current_block_start, current_block_end))
|
||||
current_block_start = pos
|
||||
current_block_end = pos
|
||||
|
||||
# Add the last block
|
||||
blocks.append((current_block_start, current_block_end))
|
||||
|
||||
return replace(state, change_blocks=blocks, current_change_idx=-1)
|
||||
|
||||
def load_commit_content(state: AppState) -> AppState:
|
||||
if not state.commits:
|
||||
|
|
@ -199,8 +221,8 @@ def load_commit_content(state: AppState) -> AppState:
|
|||
right_scroll_offset=right_scroll_offset
|
||||
)
|
||||
|
||||
# Calculate change positions for navigation
|
||||
return calculate_change_positions(new_state)
|
||||
# Calculate change blocks for navigation
|
||||
return calculate_change_blocks(new_state)
|
||||
|
||||
def update_dimensions(state: AppState, height: int, width: int) -> AppState:
|
||||
return replace(state, height=height, width=width)
|
||||
|
|
@ -263,52 +285,54 @@ def draw_left_pane(stdscr, state):
|
|||
stdscr.attroff(curses.A_REVERSE)
|
||||
|
||||
def jump_to_next_change(state: AppState) -> AppState:
|
||||
"""Jump to the next change after the current scroll position."""
|
||||
if not state.change_positions:
|
||||
"""Jump to the next change block after the current scroll position."""
|
||||
if not state.change_blocks:
|
||||
return state
|
||||
|
||||
# Find the next change after current position
|
||||
# Find the next change block after current position
|
||||
current_pos = state.right_scroll_offset
|
||||
next_idx = -1
|
||||
for i, pos in enumerate(state.change_positions):
|
||||
if pos > current_pos:
|
||||
for i, (start_pos, _) in enumerate(state.change_blocks):
|
||||
if start_pos > current_pos:
|
||||
next_idx = i
|
||||
break
|
||||
|
||||
# If no next change found, wrap to the first change
|
||||
if next_idx == -1 and state.change_positions:
|
||||
if next_idx == -1 and state.change_blocks:
|
||||
next_idx = 0
|
||||
|
||||
if next_idx != -1:
|
||||
new_scroll = state.change_positions[next_idx]
|
||||
# Get the start position of the change block
|
||||
new_scroll = max(0, state.change_blocks[next_idx][0] - 3) # Show 3 lines of context above
|
||||
return replace(state, right_scroll_offset=new_scroll, current_change_idx=next_idx)
|
||||
return state
|
||||
|
||||
def jump_to_prev_change(state: AppState) -> AppState:
|
||||
"""Jump to the previous change before the current scroll position."""
|
||||
if not state.change_positions:
|
||||
"""Jump to the previous change block before the current scroll position."""
|
||||
if not state.change_blocks:
|
||||
return state
|
||||
|
||||
# Find the previous change before current position
|
||||
# Find the previous change block before current position
|
||||
current_pos = state.right_scroll_offset
|
||||
prev_idx = -1
|
||||
for i in range(len(state.change_positions) - 1, -1, -1):
|
||||
if state.change_positions[i] < current_pos:
|
||||
for i in range(len(state.change_blocks) - 1, -1, -1):
|
||||
if state.change_blocks[i][0] < current_pos:
|
||||
prev_idx = i
|
||||
break
|
||||
|
||||
# If no previous change found, wrap to the last change
|
||||
if prev_idx == -1 and state.change_positions:
|
||||
prev_idx = len(state.change_positions) - 1
|
||||
if prev_idx == -1 and state.change_blocks:
|
||||
prev_idx = len(state.change_blocks) - 1
|
||||
|
||||
if prev_idx != -1:
|
||||
new_scroll = state.change_positions[prev_idx]
|
||||
# Get the start position of the change block
|
||||
new_scroll = max(0, state.change_blocks[prev_idx][0] - 3) # Show 3 lines of context above
|
||||
return replace(state, right_scroll_offset=new_scroll, current_change_idx=prev_idx)
|
||||
return state
|
||||
|
||||
def draw_change_markers(stdscr, state):
|
||||
"""Draw markers in the scrollbar area to indicate where changes are located."""
|
||||
if not state.change_positions or not (state.show_whole_diff or state.show_additions or state.show_deletions):
|
||||
if not state.change_blocks or not (state.show_whole_diff or state.show_additions or state.show_deletions):
|
||||
return
|
||||
|
||||
right_start = 0 if not state.show_sidebar else state.divider_col + 2
|
||||
|
|
@ -323,14 +347,11 @@ def draw_change_markers(stdscr, state):
|
|||
scale_factor = (state.height - 1) / total_lines
|
||||
|
||||
# Draw markers
|
||||
for i, pos in enumerate(state.change_positions):
|
||||
marker_y = int(pos * scale_factor)
|
||||
for i, (start_pos, _) in enumerate(state.change_blocks):
|
||||
marker_y = int(start_pos * scale_factor)
|
||||
if 0 <= marker_y < state.height - 1:
|
||||
# Determine marker color based on change type
|
||||
is_addition = any(added_line_num == pos for added_line_num, _ in state.added_lines)
|
||||
is_deletion = any(del_line_num == pos for del_line_num, _ in state.deleted_lines)
|
||||
|
||||
attr = curses.color_pair(3) if is_addition else curses.color_pair(4)
|
||||
# Use a single color for change blocks
|
||||
attr = curses.color_pair(5) # New color pair for change blocks
|
||||
marker_char = '>' if i == state.current_change_idx else '|'
|
||||
|
||||
try:
|
||||
|
|
@ -516,8 +537,8 @@ def draw_status_bars(stdscr, state):
|
|||
# Add wrap indicator and change position to commit message
|
||||
wrap_indicator = " [W] " if state.wrap_lines else " [NW] "
|
||||
change_indicator = ""
|
||||
if state.change_positions and state.current_change_idx != -1:
|
||||
change_indicator = f" [Change {state.current_change_idx + 1}/{len(state.change_positions)}] "
|
||||
if state.change_blocks and state.current_change_idx != -1:
|
||||
change_indicator = f" [Block {state.current_change_idx + 1}/{len(state.change_blocks)}] "
|
||||
|
||||
commit_message = wrap_indicator + change_indicator + commit_message
|
||||
|
||||
|
|
@ -594,7 +615,9 @@ def draw_ui(stdscr, state):
|
|||
draw_left_pane(stdscr, state)
|
||||
draw_right_pane(stdscr, state)
|
||||
draw_divider(stdscr, state)
|
||||
draw_change_markers(stdscr, state)
|
||||
# Only draw change markers in the right pane
|
||||
if state.focus == "right":
|
||||
draw_change_markers(stdscr, state)
|
||||
draw_status_bars(stdscr, state)
|
||||
draw_selection(stdscr, state)
|
||||
stdscr.refresh()
|
||||
|
|
@ -795,8 +818,9 @@ def main(stdscr, filename, show_diff, show_add, show_del, mouse, wrap_lines=True
|
|||
# Use a safe background color (7 or less) to avoid "Color number is greater than COLORS-1" error
|
||||
curses.init_pair(1, curses.COLOR_BLACK, 7) # Active pane: black on white
|
||||
curses.init_pair(2, curses.COLOR_WHITE, 0) # Inactive pane: white on black
|
||||
curses.init_pair(3, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(4, curses.COLOR_RED, -1)
|
||||
curses.init_pair(3, curses.COLOR_GREEN, -1) # Added lines
|
||||
curses.init_pair(4, curses.COLOR_RED, -1) # Deleted lines
|
||||
curses.init_pair(5, curses.COLOR_YELLOW, -1) # Change block markers
|
||||
|
||||
height, width = stdscr.getmaxyx()
|
||||
state = AppState(
|
||||
|
|
|
|||
Loading…
Reference in New Issue