refactor: Improve change navigation with block-based approach and context scrolling

This commit is contained in:
n loewen (aider) 2025-06-08 02:00:28 +01:00
parent 60b3af9108
commit bbfba3c644
1 changed files with 67 additions and 43 deletions

110
gtm
View File

@ -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(