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
|
# Line wrapping settings
|
||||||
wrap_lines: bool = True
|
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) ---
|
# --- 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:
|
def load_commit_content(state: AppState) -> AppState:
|
||||||
if not state.commits:
|
if not state.commits:
|
||||||
return state
|
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))
|
right_scroll_offset = max(0, min(right_scroll_offset, max_scroll_new))
|
||||||
|
|
||||||
return replace(state,
|
new_state = replace(state,
|
||||||
file_lines=file_lines,
|
file_lines=file_lines,
|
||||||
added_lines=added_lines,
|
added_lines=added_lines,
|
||||||
deleted_lines=deleted_lines,
|
deleted_lines=deleted_lines,
|
||||||
right_scroll_offset=right_scroll_offset
|
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:
|
def update_dimensions(state: AppState, height: int, width: int) -> AppState:
|
||||||
return replace(state, height=height, width=width)
|
return replace(state, height=height, width=width)
|
||||||
|
|
@ -240,6 +262,82 @@ def draw_left_pane(stdscr, state):
|
||||||
if display_index == state.selected_commit_idx:
|
if display_index == state.selected_commit_idx:
|
||||||
stdscr.attroff(curses.A_REVERSE)
|
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):
|
def draw_right_pane(stdscr, state):
|
||||||
# If sidebar is hidden, right pane starts at column 0
|
# If sidebar is hidden, right pane starts at column 0
|
||||||
right_start = 0 if not state.show_sidebar else state.divider_col + 2
|
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:
|
if len(parts) >= 4:
|
||||||
commit_message = parts[3]
|
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] "
|
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
|
# Status bar percentages
|
||||||
if len(state.file_lines) > 0:
|
if len(state.file_lines) > 0:
|
||||||
|
|
@ -492,6 +594,7 @@ def draw_ui(stdscr, state):
|
||||||
draw_left_pane(stdscr, state)
|
draw_left_pane(stdscr, state)
|
||||||
draw_right_pane(stdscr, state)
|
draw_right_pane(stdscr, state)
|
||||||
draw_divider(stdscr, state)
|
draw_divider(stdscr, state)
|
||||||
|
draw_change_markers(stdscr, state)
|
||||||
draw_status_bars(stdscr, state)
|
draw_status_bars(stdscr, state)
|
||||||
draw_selection(stdscr, state)
|
draw_selection(stdscr, state)
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
|
|
@ -644,6 +747,12 @@ def handle_keyboard_input(key, state: AppState) -> AppState:
|
||||||
return toggle_sidebar(state)
|
return toggle_sidebar(state)
|
||||||
elif key == ord('w'):
|
elif key == ord('w'):
|
||||||
return replace(state, wrap_lines=not state.wrap_lines)
|
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')]:
|
elif key in [curses.KEY_LEFT, ord('h')]:
|
||||||
if state.show_sidebar:
|
if state.show_sidebar:
|
||||||
return replace(state, focus="left")
|
return replace(state, focus="left")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue