feat: Add change navigation with visual markers and keyboard shortcuts
This commit is contained in:
parent
49bc53d16d
commit
60b3af9108
115
gtm
115
gtm
|
|
@ -132,9 +132,28 @@ class AppState:
|
|||
|
||||
# Line wrapping settings
|
||||
wrap_lines: bool = True
|
||||
|
||||
# Change navigation
|
||||
change_positions: List[int] = field(default_factory=list)
|
||||
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
|
||||
for line_num, _ in state.added_lines:
|
||||
if line_num not in positions:
|
||||
positions.append(line_num)
|
||||
# Add positions of deleted lines
|
||||
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)
|
||||
|
||||
def load_commit_content(state: AppState) -> AppState:
|
||||
if not state.commits:
|
||||
return state
|
||||
|
|
@ -173,12 +192,15 @@ def load_commit_content(state: AppState) -> AppState:
|
|||
|
||||
right_scroll_offset = max(0, min(right_scroll_offset, max_scroll_new))
|
||||
|
||||
return replace(state,
|
||||
new_state = replace(state,
|
||||
file_lines=file_lines,
|
||||
added_lines=added_lines,
|
||||
deleted_lines=deleted_lines,
|
||||
right_scroll_offset=right_scroll_offset
|
||||
)
|
||||
|
||||
# Calculate change positions for navigation
|
||||
return calculate_change_positions(new_state)
|
||||
|
||||
def update_dimensions(state: AppState, height: int, width: int) -> AppState:
|
||||
return replace(state, height=height, width=width)
|
||||
|
|
@ -240,6 +262,82 @@ def draw_left_pane(stdscr, state):
|
|||
if display_index == state.selected_commit_idx:
|
||||
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:
|
||||
return state
|
||||
|
||||
# Find the next change after current position
|
||||
current_pos = state.right_scroll_offset
|
||||
next_idx = -1
|
||||
for i, pos in enumerate(state.change_positions):
|
||||
if 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:
|
||||
next_idx = 0
|
||||
|
||||
if next_idx != -1:
|
||||
new_scroll = state.change_positions[next_idx]
|
||||
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:
|
||||
return state
|
||||
|
||||
# Find the previous change 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:
|
||||
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:
|
||||
new_scroll = state.change_positions[prev_idx]
|
||||
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):
|
||||
return
|
||||
|
||||
right_start = 0 if not state.show_sidebar else state.divider_col + 2
|
||||
right_width = state.width - right_start - 1
|
||||
marker_col = right_start + right_width - 1
|
||||
|
||||
# Calculate scaling factor for marker positions
|
||||
total_lines = len(state.file_lines)
|
||||
if total_lines <= 1:
|
||||
return
|
||||
|
||||
scale_factor = (state.height - 1) / total_lines
|
||||
|
||||
# Draw markers
|
||||
for i, pos in enumerate(state.change_positions):
|
||||
marker_y = int(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)
|
||||
marker_char = '>' if i == state.current_change_idx else '|'
|
||||
|
||||
try:
|
||||
stdscr.addch(marker_y, marker_col, marker_char, attr)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
def draw_right_pane(stdscr, state):
|
||||
# If sidebar is hidden, right pane starts at column 0
|
||||
right_start = 0 if not state.show_sidebar else state.divider_col + 2
|
||||
|
|
@ -415,9 +513,13 @@ def draw_status_bars(stdscr, state):
|
|||
if len(parts) >= 4:
|
||||
commit_message = parts[3]
|
||||
|
||||
# Add wrap indicator to commit message
|
||||
# Add wrap indicator and change position to commit message
|
||||
wrap_indicator = " [W] " if state.wrap_lines else " [NW] "
|
||||
commit_message = wrap_indicator + commit_message
|
||||
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)}] "
|
||||
|
||||
commit_message = wrap_indicator + change_indicator + commit_message
|
||||
|
||||
# Status bar percentages
|
||||
if len(state.file_lines) > 0:
|
||||
|
|
@ -492,6 +594,7 @@ def draw_ui(stdscr, state):
|
|||
draw_left_pane(stdscr, state)
|
||||
draw_right_pane(stdscr, state)
|
||||
draw_divider(stdscr, state)
|
||||
draw_change_markers(stdscr, state)
|
||||
draw_status_bars(stdscr, state)
|
||||
draw_selection(stdscr, state)
|
||||
stdscr.refresh()
|
||||
|
|
@ -644,6 +747,12 @@ def handle_keyboard_input(key, state: AppState) -> AppState:
|
|||
return toggle_sidebar(state)
|
||||
elif key == ord('w'):
|
||||
return replace(state, wrap_lines=not state.wrap_lines)
|
||||
elif key == ord('n'):
|
||||
if state.focus == "right" and (state.show_whole_diff or state.show_additions or state.show_deletions):
|
||||
return jump_to_next_change(state)
|
||||
elif key == ord('p'):
|
||||
if state.focus == "right" and (state.show_whole_diff or state.show_additions or state.show_deletions):
|
||||
return jump_to_prev_change(state)
|
||||
elif key in [curses.KEY_LEFT, ord('h')]:
|
||||
if state.show_sidebar:
|
||||
return replace(state, focus="left")
|
||||
|
|
|
|||
Loading…
Reference in New Issue