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
|
wrap_lines: bool = True
|
||||||
|
|
||||||
# Change navigation
|
# 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
|
current_change_idx: int = -1 # -1 means no change is selected
|
||||||
|
|
||||||
# --- Actions (Controller) ---
|
# --- Actions (Controller) ---
|
||||||
|
|
||||||
def calculate_change_positions(state: AppState) -> AppState:
|
def calculate_change_blocks(state: AppState) -> AppState:
|
||||||
"""Calculate positions of all changes (additions and deletions) in the file."""
|
"""Calculate positions of change blocks (consecutive changes) in the file."""
|
||||||
positions = []
|
if not state.added_lines and not state.deleted_lines:
|
||||||
# Add positions of added lines
|
return replace(state, change_blocks=[], current_change_idx=-1)
|
||||||
|
|
||||||
|
# Collect all change positions
|
||||||
|
all_changes = []
|
||||||
for line_num, _ in state.added_lines:
|
for line_num, _ in state.added_lines:
|
||||||
if line_num not in positions:
|
all_changes.append(line_num)
|
||||||
positions.append(line_num)
|
|
||||||
# Add positions of deleted lines
|
|
||||||
for line_num, _ in state.deleted_lines:
|
for line_num, _ in state.deleted_lines:
|
||||||
if line_num not in positions:
|
all_changes.append(line_num)
|
||||||
positions.append(line_num)
|
|
||||||
# Sort positions
|
# Sort all positions
|
||||||
positions.sort()
|
all_changes.sort()
|
||||||
return replace(state, change_positions=positions, current_change_idx=-1)
|
|
||||||
|
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:
|
def load_commit_content(state: AppState) -> AppState:
|
||||||
if not state.commits:
|
if not state.commits:
|
||||||
|
|
@ -199,8 +221,8 @@ def load_commit_content(state: AppState) -> AppState:
|
||||||
right_scroll_offset=right_scroll_offset
|
right_scroll_offset=right_scroll_offset
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate change positions for navigation
|
# Calculate change blocks for navigation
|
||||||
return calculate_change_positions(new_state)
|
return calculate_change_blocks(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)
|
||||||
|
|
@ -263,52 +285,54 @@ def draw_left_pane(stdscr, state):
|
||||||
stdscr.attroff(curses.A_REVERSE)
|
stdscr.attroff(curses.A_REVERSE)
|
||||||
|
|
||||||
def jump_to_next_change(state: AppState) -> AppState:
|
def jump_to_next_change(state: AppState) -> AppState:
|
||||||
"""Jump to the next change after the current scroll position."""
|
"""Jump to the next change block after the current scroll position."""
|
||||||
if not state.change_positions:
|
if not state.change_blocks:
|
||||||
return state
|
return state
|
||||||
|
|
||||||
# Find the next change after current position
|
# Find the next change block after current position
|
||||||
current_pos = state.right_scroll_offset
|
current_pos = state.right_scroll_offset
|
||||||
next_idx = -1
|
next_idx = -1
|
||||||
for i, pos in enumerate(state.change_positions):
|
for i, (start_pos, _) in enumerate(state.change_blocks):
|
||||||
if pos > current_pos:
|
if start_pos > current_pos:
|
||||||
next_idx = i
|
next_idx = i
|
||||||
break
|
break
|
||||||
|
|
||||||
# If no next change found, wrap to the first change
|
# 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
|
next_idx = 0
|
||||||
|
|
||||||
if next_idx != -1:
|
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 replace(state, right_scroll_offset=new_scroll, current_change_idx=next_idx)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def jump_to_prev_change(state: AppState) -> AppState:
|
def jump_to_prev_change(state: AppState) -> AppState:
|
||||||
"""Jump to the previous change before the current scroll position."""
|
"""Jump to the previous change block before the current scroll position."""
|
||||||
if not state.change_positions:
|
if not state.change_blocks:
|
||||||
return state
|
return state
|
||||||
|
|
||||||
# Find the previous change before current position
|
# Find the previous change block before current position
|
||||||
current_pos = state.right_scroll_offset
|
current_pos = state.right_scroll_offset
|
||||||
prev_idx = -1
|
prev_idx = -1
|
||||||
for i in range(len(state.change_positions) - 1, -1, -1):
|
for i in range(len(state.change_blocks) - 1, -1, -1):
|
||||||
if state.change_positions[i] < current_pos:
|
if state.change_blocks[i][0] < current_pos:
|
||||||
prev_idx = i
|
prev_idx = i
|
||||||
break
|
break
|
||||||
|
|
||||||
# If no previous change found, wrap to the last change
|
# If no previous change found, wrap to the last change
|
||||||
if prev_idx == -1 and state.change_positions:
|
if prev_idx == -1 and state.change_blocks:
|
||||||
prev_idx = len(state.change_positions) - 1
|
prev_idx = len(state.change_blocks) - 1
|
||||||
|
|
||||||
if prev_idx != -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 replace(state, right_scroll_offset=new_scroll, current_change_idx=prev_idx)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def draw_change_markers(stdscr, state):
|
def draw_change_markers(stdscr, state):
|
||||||
"""Draw markers in the scrollbar area to indicate where changes are located."""
|
"""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
|
return
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -323,14 +347,11 @@ def draw_change_markers(stdscr, state):
|
||||||
scale_factor = (state.height - 1) / total_lines
|
scale_factor = (state.height - 1) / total_lines
|
||||||
|
|
||||||
# Draw markers
|
# Draw markers
|
||||||
for i, pos in enumerate(state.change_positions):
|
for i, (start_pos, _) in enumerate(state.change_blocks):
|
||||||
marker_y = int(pos * scale_factor)
|
marker_y = int(start_pos * scale_factor)
|
||||||
if 0 <= marker_y < state.height - 1:
|
if 0 <= marker_y < state.height - 1:
|
||||||
# Determine marker color based on change type
|
# Use a single color for change blocks
|
||||||
is_addition = any(added_line_num == pos for added_line_num, _ in state.added_lines)
|
attr = curses.color_pair(5) # New color pair for change blocks
|
||||||
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 '|'
|
marker_char = '>' if i == state.current_change_idx else '|'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -516,8 +537,8 @@ def draw_status_bars(stdscr, state):
|
||||||
# Add wrap indicator and change position 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] "
|
||||||
change_indicator = ""
|
change_indicator = ""
|
||||||
if state.change_positions and state.current_change_idx != -1:
|
if state.change_blocks and state.current_change_idx != -1:
|
||||||
change_indicator = f" [Change {state.current_change_idx + 1}/{len(state.change_positions)}] "
|
change_indicator = f" [Block {state.current_change_idx + 1}/{len(state.change_blocks)}] "
|
||||||
|
|
||||||
commit_message = wrap_indicator + change_indicator + commit_message
|
commit_message = wrap_indicator + change_indicator + commit_message
|
||||||
|
|
||||||
|
|
@ -594,7 +615,9 @@ 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)
|
# Only draw change markers in the right pane
|
||||||
|
if state.focus == "right":
|
||||||
|
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()
|
||||||
|
|
@ -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
|
# 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(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(2, curses.COLOR_WHITE, 0) # Inactive pane: white on black
|
||||||
curses.init_pair(3, curses.COLOR_GREEN, -1)
|
curses.init_pair(3, curses.COLOR_GREEN, -1) # Added lines
|
||||||
curses.init_pair(4, curses.COLOR_RED, -1)
|
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()
|
height, width = stdscr.getmaxyx()
|
||||||
state = AppState(
|
state = AppState(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue