@@ -11,6 +11,7 @@ module file_tree_module |
| 11 | ! Tree node using linked list structure (first-child, next-sibling) | 11 | ! Tree node using linked list structure (first-child, next-sibling) |
| 12 | type :: tree_node_t | 12 | type :: tree_node_t |
| 13 | character(len=256) :: name = '' | 13 | character(len=256) :: name = '' |
| | 14 | + character(len=512) :: full_path = '' ! Full path for files (for staging) |
| 14 | logical :: is_file = .false. | 15 | logical :: is_file = .false. |
| 15 | logical :: is_staged = .false. | 16 | logical :: is_staged = .false. |
| 16 | logical :: is_unstaged = .false. | 17 | logical :: is_unstaged = .false. |
@@ -29,10 +30,20 @@ module file_tree_module |
| 29 | logical :: has_incoming = .false. | 30 | logical :: has_incoming = .false. |
| 30 | end type file_entry_t | 31 | end type file_entry_t |
| 31 | | 32 | |
| | 33 | + ! Selectable item (files only, in tree traversal order) |
| | 34 | + type :: selectable_file_t |
| | 35 | + character(len=512) :: path = '' |
| | 36 | + logical :: is_staged = .false. |
| | 37 | + logical :: is_unstaged = .false. |
| | 38 | + logical :: is_untracked = .false. |
| | 39 | + end type selectable_file_t |
| | 40 | + |
| 32 | ! Tree state for navigation | 41 | ! Tree state for navigation |
| 33 | type :: tree_state_t | 42 | type :: tree_state_t |
| 34 | type(file_entry_t), allocatable :: files(:) | 43 | type(file_entry_t), allocatable :: files(:) |
| | 44 | + type(selectable_file_t), allocatable :: selectable_files(:) |
| 35 | integer :: n_files = 0 | 45 | integer :: n_files = 0 |
| | 46 | + integer :: n_selectable = 0 |
| 36 | integer :: selected_index = 1 | 47 | integer :: selected_index = 1 |
| 37 | integer :: viewport_offset = 1 | 48 | integer :: viewport_offset = 1 |
| 38 | type(tree_node_t), pointer :: root => null() | 49 | type(tree_node_t), pointer :: root => null() |
@@ -62,8 +73,10 @@ contains |
| 62 | type(tree_state_t), intent(inout) :: state | 73 | type(tree_state_t), intent(inout) :: state |
| 63 | | 74 | |
| 64 | if (allocated(state%files)) deallocate(state%files) | 75 | if (allocated(state%files)) deallocate(state%files) |
| | 76 | + if (allocated(state%selectable_files)) deallocate(state%selectable_files) |
| 65 | if (associated(state%root)) call free_tree(state%root) | 77 | if (associated(state%root)) call free_tree(state%root) |
| 66 | state%n_files = 0 | 78 | state%n_files = 0 |
| | 79 | + state%n_selectable = 0 |
| 67 | state%selected_index = 1 | 80 | state%selected_index = 1 |
| 68 | end subroutine cleanup_tree_state | 81 | end subroutine cleanup_tree_state |
| 69 | | 82 | |
@@ -76,6 +89,7 @@ contains |
| 76 | call free_tree(state%root) | 89 | call free_tree(state%root) |
| 77 | state%root => null() | 90 | state%root => null() |
| 78 | end if | 91 | end if |
| | 92 | + if (allocated(state%selectable_files)) deallocate(state%selectable_files) |
| 79 | | 93 | |
| 80 | ! Get dirty files from git | 94 | ! Get dirty files from git |
| 81 | call get_dirty_files(workspace_path, state%files, state%n_files) | 95 | call get_dirty_files(workspace_path, state%files, state%n_files) |
@@ -83,12 +97,16 @@ contains |
| 83 | ! Build tree from files | 97 | ! Build tree from files |
| 84 | if (state%n_files > 0) then | 98 | if (state%n_files > 0) then |
| 85 | call build_tree(state%files, state%n_files, state%root) | 99 | call build_tree(state%files, state%n_files, state%root) |
| | 100 | + ! Build selectable files list in tree traversal order |
| | 101 | + call build_selectable_list(state%root, state%selectable_files, state%n_selectable) |
| | 102 | + else |
| | 103 | + state%n_selectable = 0 |
| 86 | end if | 104 | end if |
| 87 | | 105 | |
| 88 | ! Clamp selected index | 106 | ! Clamp selected index |
| 89 | - if (state%selected_index > state%n_files .and. state%n_files > 0) then | 107 | + if (state%selected_index > state%n_selectable .and. state%n_selectable > 0) then |
| 90 | - state%selected_index = state%n_files | 108 | + state%selected_index = state%n_selectable |
| 91 | - else if (state%n_files == 0) then | 109 | + else if (state%n_selectable == 0) then |
| 92 | state%selected_index = 1 | 110 | state%selected_index = 1 |
| 93 | end if | 111 | end if |
| 94 | end subroutine refresh_tree_state | 112 | end subroutine refresh_tree_state |
@@ -179,6 +197,7 @@ contains |
| 179 | integer, intent(in) :: n_files | 197 | integer, intent(in) :: n_files |
| 180 | type(tree_node_t), pointer, intent(out) :: root | 198 | type(tree_node_t), pointer, intent(out) :: root |
| 181 | integer :: i | 199 | integer :: i |
| | 200 | + integer :: debug_unit |
| 182 | | 201 | |
| 183 | ! Create root | 202 | ! Create root |
| 184 | allocate(root) | 203 | allocate(root) |
@@ -196,8 +215,38 @@ contains |
| 196 | | 215 | |
| 197 | ! Sort tree | 216 | ! Sort tree |
| 198 | call sort_tree(root) | 217 | call sort_tree(root) |
| | 218 | + |
| | 219 | + ! DEBUG: Write tree structure to file (unconditional) |
| | 220 | + open(newunit=debug_unit, file='/tmp/fac_tree_debug.txt', status='replace', action='write') |
| | 221 | + write(debug_unit, '(A)') '=== FINAL TREE STRUCTURE ===' |
| | 222 | + call debug_print_tree(root, '', debug_unit) |
| | 223 | + close(debug_unit) |
| | 224 | + |
| | 225 | + ! Also write to a simpler path |
| | 226 | + open(10, file='fac_debug.txt', status='replace', action='write') |
| | 227 | + write(10, '(A)') '=== FINAL TREE STRUCTURE ===' |
| | 228 | + call debug_print_tree(root, '', 10) |
| | 229 | + close(10) |
| 199 | end subroutine build_tree | 230 | end subroutine build_tree |
| 200 | | 231 | |
| | 232 | + recursive subroutine debug_print_tree(node, prefix, unit) |
| | 233 | + type(tree_node_t), pointer, intent(in) :: node |
| | 234 | + character(len=*), intent(in) :: prefix |
| | 235 | + integer, intent(in) :: unit |
| | 236 | + type(tree_node_t), pointer :: child |
| | 237 | + |
| | 238 | + if (.not. associated(node)) return |
| | 239 | + |
| | 240 | + write(unit, '(A,A,A,L,A,L)') trim(prefix), trim(node%name), & |
| | 241 | + ' is_file=', node%is_file, ' has_next_sib=', associated(node%next_sibling) |
| | 242 | + |
| | 243 | + child => node%first_child |
| | 244 | + do while (associated(child)) |
| | 245 | + call debug_print_tree(child, prefix // ' ', unit) |
| | 246 | + child => child%next_sibling |
| | 247 | + end do |
| | 248 | + end subroutine debug_print_tree |
| | 249 | + |
| 201 | subroutine add_to_tree(root, path, is_staged, is_unstaged, is_untracked, has_incoming) | 250 | subroutine add_to_tree(root, path, is_staged, is_unstaged, is_untracked, has_incoming) |
| 202 | type(tree_node_t), pointer, intent(inout) :: root | 251 | type(tree_node_t), pointer, intent(inout) :: root |
| 203 | character(len=*), intent(in) :: path | 252 | character(len=*), intent(in) :: path |
@@ -238,12 +287,13 @@ contains |
| 238 | child => new_node | 287 | child => new_node |
| 239 | end if | 288 | end if |
| 240 | | 289 | |
| 241 | - ! If this is the final component, set status | 290 | + ! If this is the final component, set status and full path |
| 242 | if (len_trim(remaining_path) == 0) then | 291 | if (len_trim(remaining_path) == 0) then |
| 243 | child%is_staged = is_staged | 292 | child%is_staged = is_staged |
| 244 | child%is_unstaged = is_unstaged | 293 | child%is_unstaged = is_unstaged |
| 245 | child%is_untracked = is_untracked | 294 | child%is_untracked = is_untracked |
| 246 | child%has_incoming = has_incoming | 295 | child%has_incoming = has_incoming |
| | 296 | + child%full_path = trim(path) |
| 247 | end if | 297 | end if |
| 248 | | 298 | |
| 249 | current => child | 299 | current => child |
@@ -271,9 +321,26 @@ contains |
| 271 | type(tree_node_t), pointer, intent(inout) :: parent | 321 | type(tree_node_t), pointer, intent(inout) :: parent |
| 272 | type(tree_node_t), pointer :: sorted, current, next_node, insert_pos, prev | 322 | type(tree_node_t), pointer :: sorted, current, next_node, insert_pos, prev |
| 273 | logical :: inserted | 323 | logical :: inserted |
| | 324 | + integer :: debug_unit |
| | 325 | + type(tree_node_t), pointer :: check_ptr |
| 274 | | 326 | |
| 275 | if (.not. associated(parent%first_child)) return | 327 | if (.not. associated(parent%first_child)) return |
| 276 | | 328 | |
| | 329 | + ! DEBUG: Write pre-sort state (both file and stderr) |
| | 330 | + if (trim(parent%name) == 'workspace') then |
| | 331 | + open(newunit=debug_unit, file='/tmp/fac_sort_debug.txt', status='replace', action='write') |
| | 332 | + write(debug_unit, '(A)') '=== Sorting workspace children ===' |
| | 333 | + write(debug_unit, '(A)') 'Before sort:' |
| | 334 | + write(0, '(A)') '[DEBUG] Sorting workspace children' |
| | 335 | + write(0, '(A)') '[DEBUG] Before sort:' |
| | 336 | + check_ptr => parent%first_child |
| | 337 | + do while (associated(check_ptr)) |
| | 338 | + write(debug_unit, '(A,A,A,L)') ' ', trim(check_ptr%name), ' next_sib=', associated(check_ptr%next_sibling) |
| | 339 | + write(0, '(A,A,A,L)') '[DEBUG] ', trim(check_ptr%name), ' next_sib=', associated(check_ptr%next_sibling) |
| | 340 | + check_ptr => check_ptr%next_sibling |
| | 341 | + end do |
| | 342 | + end if |
| | 343 | + |
| 277 | sorted => null() | 344 | sorted => null() |
| 278 | | 345 | |
| 279 | current => parent%first_child | 346 | current => parent%first_child |
@@ -309,6 +376,20 @@ contains |
| 309 | end do | 376 | end do |
| 310 | | 377 | |
| 311 | parent%first_child => sorted | 378 | parent%first_child => sorted |
| | 379 | + |
| | 380 | + ! DEBUG: Write post-sort state (both file and stderr) |
| | 381 | + if (trim(parent%name) == 'workspace') then |
| | 382 | + write(debug_unit, '(A)') 'After sort:' |
| | 383 | + write(0, '(A)') '[DEBUG] After sort:' |
| | 384 | + check_ptr => parent%first_child |
| | 385 | + do while (associated(check_ptr)) |
| | 386 | + write(debug_unit, '(A,A,A,L)') ' ', trim(check_ptr%name), ' next_sib=', associated(check_ptr%next_sibling) |
| | 387 | + write(0, '(A,A,A,L)') '[DEBUG] ', trim(check_ptr%name), ' next_sib=', associated(check_ptr%next_sibling) |
| | 388 | + check_ptr => check_ptr%next_sibling |
| | 389 | + end do |
| | 390 | + write(debug_unit, '(A)') '' |
| | 391 | + close(debug_unit) |
| | 392 | + end if |
| 312 | end subroutine sort_children | 393 | end subroutine sort_children |
| 313 | | 394 | |
| 314 | function compare_nodes(a, b) result(cmp) | 395 | function compare_nodes(a, b) result(cmp) |
@@ -332,6 +413,55 @@ contains |
| 332 | end if | 413 | end if |
| 333 | end function compare_nodes | 414 | end function compare_nodes |
| 334 | | 415 | |
| | 416 | + ! Build list of selectable files in tree traversal order |
| | 417 | + subroutine build_selectable_list(root, selectable, n_selectable) |
| | 418 | + type(tree_node_t), pointer, intent(in) :: root |
| | 419 | + type(selectable_file_t), allocatable, intent(out) :: selectable(:) |
| | 420 | + integer, intent(out) :: n_selectable |
| | 421 | + type(selectable_file_t), allocatable :: temp(:) |
| | 422 | + integer :: max_size, count |
| | 423 | + |
| | 424 | + max_size = 1000 |
| | 425 | + allocate(temp(max_size)) |
| | 426 | + count = 0 |
| | 427 | + |
| | 428 | + ! Traverse tree and collect files |
| | 429 | + call collect_files_recursive(root, temp, count, max_size) |
| | 430 | + |
| | 431 | + n_selectable = count |
| | 432 | + allocate(selectable(n_selectable)) |
| | 433 | + if (n_selectable > 0) selectable(1:n_selectable) = temp(1:n_selectable) |
| | 434 | + deallocate(temp) |
| | 435 | + end subroutine build_selectable_list |
| | 436 | + |
| | 437 | + recursive subroutine collect_files_recursive(node, list, count, max_size) |
| | 438 | + type(tree_node_t), pointer, intent(in) :: node |
| | 439 | + type(selectable_file_t), intent(inout) :: list(:) |
| | 440 | + integer, intent(inout) :: count |
| | 441 | + integer, intent(in) :: max_size |
| | 442 | + type(tree_node_t), pointer :: child |
| | 443 | + |
| | 444 | + if (.not. associated(node)) return |
| | 445 | + |
| | 446 | + ! If this is a file, add it to the list |
| | 447 | + if (node%is_file .and. len_trim(node%full_path) > 0) then |
| | 448 | + count = count + 1 |
| | 449 | + if (count <= max_size) then |
| | 450 | + list(count)%path = node%full_path |
| | 451 | + list(count)%is_staged = node%is_staged |
| | 452 | + list(count)%is_unstaged = node%is_unstaged |
| | 453 | + list(count)%is_untracked = node%is_untracked |
| | 454 | + end if |
| | 455 | + end if |
| | 456 | + |
| | 457 | + ! Recursively process children (in order) |
| | 458 | + child => node%first_child |
| | 459 | + do while (associated(child)) |
| | 460 | + call collect_files_recursive(child, list, count, max_size) |
| | 461 | + child => child%next_sibling |
| | 462 | + end do |
| | 463 | + end subroutine collect_files_recursive |
| | 464 | + |
| 335 | recursive subroutine free_tree(node) | 465 | recursive subroutine free_tree(node) |
| 336 | type(tree_node_t), pointer, intent(inout) :: node | 466 | type(tree_node_t), pointer, intent(inout) :: node |
| 337 | type(tree_node_t), pointer :: child, next_child | 467 | type(tree_node_t), pointer :: child, next_child |
@@ -400,7 +530,7 @@ contains |
| 400 | | 530 | |
| 401 | subroutine tree_move_down(state) | 531 | subroutine tree_move_down(state) |
| 402 | type(tree_state_t), intent(inout) :: state | 532 | type(tree_state_t), intent(inout) :: state |
| 403 | - if (state%selected_index < state%n_files) then | 533 | + if (state%selected_index < state%n_selectable) then |
| 404 | state%selected_index = state%selected_index + 1 | 534 | state%selected_index = state%selected_index + 1 |
| 405 | end if | 535 | end if |
| 406 | end subroutine tree_move_down | 536 | end subroutine tree_move_down |
@@ -409,8 +539,8 @@ contains |
| 409 | type(tree_state_t), intent(in) :: state | 539 | type(tree_state_t), intent(in) :: state |
| 410 | character(len=:), allocatable :: path | 540 | character(len=:), allocatable :: path |
| 411 | | 541 | |
| 412 | - if (state%selected_index >= 1 .and. state%selected_index <= state%n_files) then | 542 | + if (state%selected_index >= 1 .and. state%selected_index <= state%n_selectable) then |
| 413 | - path = trim(state%files(state%selected_index)%path) | 543 | + path = trim(state%selectable_files(state%selected_index)%path) |
| 414 | else | 544 | else |
| 415 | path = '' | 545 | path = '' |
| 416 | end if | 546 | end if |
@@ -423,11 +553,11 @@ contains |
| 423 | character(len=1024) :: cmd | 553 | character(len=1024) :: cmd |
| 424 | integer :: status | 554 | integer :: status |
| 425 | | 555 | |
| 426 | - if (state%selected_index < 1 .or. state%selected_index > state%n_files) return | 556 | + if (state%selected_index < 1 .or. state%selected_index > state%n_selectable) return |
| 427 | | 557 | |
| 428 | ! Stage the file | 558 | ! Stage the file |
| 429 | write(cmd, '(A,A,A,A,A)') 'cd "', trim(workspace_path), '" && git add "', & | 559 | write(cmd, '(A,A,A,A,A)') 'cd "', trim(workspace_path), '" && git add "', & |
| 430 | - trim(state%files(state%selected_index)%path), '" 2>/dev/null' | 560 | + trim(state%selectable_files(state%selected_index)%path), '" 2>/dev/null' |
| 431 | call execute_command_line(trim(cmd), exitstat=status) | 561 | call execute_command_line(trim(cmd), exitstat=status) |
| 432 | | 562 | |
| 433 | ! Refresh tree | 563 | ! Refresh tree |
@@ -440,11 +570,11 @@ contains |
| 440 | character(len=1024) :: cmd | 570 | character(len=1024) :: cmd |
| 441 | integer :: status | 571 | integer :: status |
| 442 | | 572 | |
| 443 | - if (state%selected_index < 1 .or. state%selected_index > state%n_files) return | 573 | + if (state%selected_index < 1 .or. state%selected_index > state%n_selectable) return |
| 444 | | 574 | |
| 445 | ! Unstage the file | 575 | ! Unstage the file |
| 446 | write(cmd, '(A,A,A,A,A)') 'cd "', trim(workspace_path), '" && git restore --staged "', & | 576 | write(cmd, '(A,A,A,A,A)') 'cd "', trim(workspace_path), '" && git restore --staged "', & |
| 447 | - trim(state%files(state%selected_index)%path), '" 2>/dev/null' | 577 | + trim(state%selectable_files(state%selected_index)%path), '" 2>/dev/null' |
| 448 | call execute_command_line(trim(cmd), exitstat=status) | 578 | call execute_command_line(trim(cmd), exitstat=status) |
| 449 | | 579 | |
| 450 | ! Refresh tree | 580 | ! Refresh tree |