Fortran · 46262 bytes Raw Blame History
1 program facsimile
2 use iso_fortran_env, only: error_unit, input_unit, output_unit, int64
3 use version_module
4 use terminal_io_module
5 use input_handler_module, only: get_key_input
6 use editor_state_module
7 use text_buffer_module
8 use renderer_module
9 use command_handler_module, only: handle_key_command, init_command_handler, cleanup_command_handler, &
10 save_initial_state_for_undo, search_pattern, match_case_sensitive, &
11 g_lsp_modified_buffer, g_lsp_ui_changed, g_cursor_only_move
12 use workspace_module
13 use backup_module
14 use save_prompt_module
15 use command_palette_module, only: register_command
16 use welcome_menu_module, only: show_welcome_menu
17 use fortress_navigator_module, only: open_fortress_navigator
18 use binary_prompt_module, only: binary_file_prompt
19 use lsp_server_manager_module, only: notify_file_opened, notify_file_changed, &
20 notify_file_closed, process_server_messages, &
21 set_diagnostics_handler, set_lsp_workspace_root
22 use lsp_protocol_module, only: lsp_message_t
23 use app_state_module, only: is_first_run, mark_first_run_complete
24 use lsp_server_installer_panel_module, only: show_lsp_server_installer_panel
25 implicit none
26
27 type(editor_state_t) :: editor
28 type(buffer_t) :: buffer
29 character(len=32) :: key_input
30 character(len=512) :: filename, arg, workspace_dir, lsp_workspace
31 logical :: running, should_quit, is_workspace_mode, workspace_success
32 logical :: welcome_cancelled, is_browse, nav_cancelled, is_directory
33 logical :: explicit_lsp_workspace
34 character(len=:), allocatable :: selected_path
35 integer :: status, argc, rows, cols, i
36
37
38 ! Get command line arguments
39 argc = command_argument_count()
40 is_workspace_mode = .false.
41 workspace_dir = ""
42 filename = ""
43 lsp_workspace = ""
44 explicit_lsp_workspace = .false.
45
46 ! First pass: look for -w/--workspace flag
47 i = 1
48 do while (i <= argc)
49 call get_command_argument(i, arg)
50 if (trim(arg) == '-w' .or. trim(arg) == '--workspace') then
51 if (i < argc) then
52 call get_command_argument(i + 1, lsp_workspace)
53 explicit_lsp_workspace = .true.
54 i = i + 2
55 else
56 write(error_unit, '(A)') 'Error: -w/--workspace requires a directory argument'
57 stop 1
58 end if
59 else
60 i = i + 1
61 end if
62 end do
63
64 ! Second pass: handle other arguments
65 i = 1
66 do while (i <= argc)
67 call get_command_argument(i, arg)
68
69 ! Skip -w and its argument (already processed)
70 if (trim(arg) == '-w' .or. trim(arg) == '--workspace') then
71 i = i + 2
72 cycle
73 end if
74
75 ! Handle version flags
76 if (trim(arg) == '--version' .or. trim(arg) == '-v') then
77 write(output_unit, '(A,A)') 'fac version ', VERSION
78 stop
79 end if
80
81 ! Handle help flags
82 if (trim(arg) == '--help' .or. trim(arg) == '-h') then
83 call print_help()
84 stop
85 end if
86
87 ! This must be the file/directory argument
88 ! Check if argument is a directory (workspace mode)
89 ! Use test -d which is POSIX compliant (works on Linux, macOS, BSD)
90 call execute_command_line("test -d '" // trim(arg) // &
91 "' && echo 'Directory' > /tmp/.fac_filetype || " // &
92 "echo 'File' > /tmp/.fac_filetype", wait=.true.)
93 call read_file_type(status)
94 if (status == 0) then
95 ! Directory - workspace mode
96 is_workspace_mode = .true.
97 call workspace_get_path(trim(arg), workspace_dir)
98 else
99 ! File - check if parent directory has a workspace
100 workspace_dir = workspace_detect_from_file(trim(arg))
101 if (len_trim(workspace_dir) > 0) then
102 is_workspace_mode = .true.
103 end if
104 filename = arg
105 end if
106 i = i + 1
107 end do
108
109 if (argc == 0) then
110 ! No arguments - launch Fortress welcome menu (Phase 5)
111 call terminal_init()
112 call show_welcome_menu(selected_path, welcome_cancelled)
113
114 if (welcome_cancelled) then
115 ! Check if user wants to browse filesystem
116 is_browse = .false.
117 if (allocated(selected_path) .and. selected_path == "BROWSE") then
118 is_browse = .true.
119 end if
120
121 call terminal_cleanup()
122
123 if (is_browse) then
124 ! Launch fortress navigator
125 call terminal_init()
126 call open_fortress_navigator(selected_path, is_directory, nav_cancelled)
127 call terminal_cleanup()
128
129 if (nav_cancelled .or. .not. allocated(selected_path)) then
130 ! User cancelled navigation too
131 stop
132 end if
133
134 ! Handle the selection from navigator
135 if (is_directory) then
136 ! Directory - open as workspace
137 is_workspace_mode = .true.
138 call workspace_get_path(trim(selected_path), workspace_dir)
139 else
140 ! File - open in single-file mode (like `fac filename`)
141 filename = selected_path
142 ! Check if parent directory has a workspace
143 workspace_dir = workspace_detect_from_file(trim(filename))
144 if (len_trim(workspace_dir) > 0) then
145 is_workspace_mode = .true.
146 end if
147 end if
148 else
149 ! User just cancelled
150 stop
151 end if
152 end if
153
154 ! User selected a workspace from welcome menu (not browse)
155 ! This handles favorites, recents, and CURRENT DIRECTORY
156 if (allocated(selected_path) .and. .not. is_browse) then
157 ! Check if user selected CURRENT DIRECTORY option
158 if (selected_path == "CWD") then
159 ! Get actual current working directory
160 call get_workspace_path(selected_path)
161 arg = selected_path
162 else
163 arg = selected_path
164 end if
165
166 ! Check if it's a directory
167 call execute_command_line("test -d '" // trim(arg) // &
168 "' && echo 'Directory' > /tmp/.fac_filetype || " // &
169 "echo 'File' > /tmp/.fac_filetype", wait=.true.)
170 call read_file_type(status)
171 if (status == 0) then
172 ! Directory - workspace mode
173 is_workspace_mode = .true.
174 call workspace_get_path(trim(arg), workspace_dir)
175 else
176 ! Invalid selection (favorites/recents should only have directories)
177 write(error_unit, '(A)') 'Error: Selected path is not a directory'
178 stop 1
179 end if
180 end if
181 end if
182
183 ! Handle workspace mode
184 if (is_workspace_mode) then
185 ! Check if workspace exists, create if not
186 if (.not. workspace_exists(workspace_dir)) then
187 call workspace_init(workspace_dir, workspace_success)
188 if (.not. workspace_success) then
189 write(error_unit, '(A)') 'Error: Failed to create workspace'
190 stop 1
191 end if
192 else
193 ! Load existing workspace
194 call workspace_load(workspace_dir, workspace_success)
195 if (.not. workspace_success) then
196 write(error_unit, '(A)') 'Error: Failed to load workspace'
197 stop 1
198 end if
199 end if
200 end if
201
202 ! Initialize editor
203 call init_editor(editor)
204 running = .true.
205
206 ! Set LSP workspace root if explicit -w flag was provided
207 if (explicit_lsp_workspace) then
208 call set_lsp_workspace_root(editor%lsp_manager, trim(lsp_workspace))
209 end if
210
211 ! Set up diagnostics handler for LSP
212 call set_diagnostics_handler(editor%lsp_manager, handle_diagnostics)
213
214 ! Register all commands for command palette
215 call register_all_commands()
216
217 ! Initialize terminal early (needed for workspace restoration warnings)
218 call terminal_init()
219 call terminal_clear_screen()
220
221 ! Initialize main buffer early (needed for workspace restoration)
222 call init_buffer(buffer)
223
224 ! Set workspace path
225 if (is_workspace_mode) then
226 ! Use detected/created workspace directory
227 allocate(character(len=len_trim(workspace_dir)) :: editor%workspace_path)
228 editor%workspace_path = trim(workspace_dir)
229
230 ! Restore workspace state (tabs, cursor positions, etc.)
231 call workspace_restore_state(editor, editor%workspace_path, workspace_success)
232
233 ! Sync restored active tab's buffer to main buffer
234 if (workspace_success .and. allocated(editor%tabs) .and. editor%active_tab_index > 0) then
235 if (editor%active_tab_index <= size(editor%tabs)) then
236 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
237 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
238 ! Copy active pane's buffer to main buffer (replaces the empty init)
239 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%panes(1)%buffer)
240 end if
241 end if
242 end if
243 else
244 ! Single-file mode - use current directory
245 call get_workspace_path(editor%workspace_path)
246 end if
247
248 ! Check for backups and offer restoration (after workspace load, after terminal init)
249 if (is_workspace_mode .and. backup_detect(editor%workspace_path)) then
250 call handle_backup_restoration(editor, buffer)
251 end if
252
253 ! Get terminal size
254 call terminal_get_size(rows, cols)
255 editor%screen_rows = rows
256 editor%screen_cols = cols
257
258 ! Initialize renderer (pass filename for syntax highlighting detection)
259 if (len_trim(filename) > 0) then
260 call init_renderer(rows, cols, trim(filename))
261 else
262 call init_renderer(rows, cols)
263 end if
264
265 ! Initialize command handler (for yank stack)
266 call init_command_handler()
267
268 ! Initialize buffer and load file if specified
269 if (len_trim(filename) > 0) then
270 ! Create a tab for the initial file
271 call create_tab(editor, trim(filename))
272
273 ! Load file into tab's buffer and first pane's buffer
274 if (editor%active_tab_index > 0) then
275 call buffer_load_file(editor%tabs(editor%active_tab_index)%buffer, trim(filename), status)
276
277 ! Also load into first pane's buffer
278 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
279 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
280 call buffer_load_file(editor%tabs(editor%active_tab_index)%panes(1)%buffer, trim(filename), status)
281 ! Copy first pane's buffer to main buffer
282 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%panes(1)%buffer)
283 else
284 ! Copy tab's buffer to main buffer
285 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer)
286 end if
287 else
288 call buffer_load_file(buffer, trim(filename), status)
289 end if
290
291 if (status == 0) then
292 if (allocated(editor%filename)) deallocate(editor%filename)
293 allocate(character(len=len_trim(filename)) :: editor%filename)
294 editor%filename = trim(filename)
295
296 ! Send LSP didOpen notification to ALL active servers
297 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
298 if (editor%tabs(editor%active_tab_index)%num_lsp_servers > 0) then
299 block
300 integer :: srv_i
301 do srv_i = 1, editor%tabs(editor%active_tab_index)%num_lsp_servers
302 call notify_file_opened(editor%lsp_manager, &
303 editor%tabs(editor%active_tab_index)%lsp_server_indices(srv_i), &
304 trim(filename), buffer_to_string(buffer))
305 end do
306 end block
307 end if
308 end if
309 else if (status == -2) then
310 ! Binary file detected - prompt user
311 if (binary_file_prompt(trim(filename))) then
312 ! User wants to view in hex mode
313 call buffer_load_file_as_hex(buffer, trim(filename), status)
314 if (status == 0) then
315 ! Deallocate if already allocated
316 if (allocated(editor%filename)) deallocate(editor%filename)
317 allocate(character(len=len_trim(filename) + 6) :: editor%filename)
318 editor%filename = trim(filename) // ' [HEX]'
319
320 ! Copy hex buffer to tab and pane buffers
321 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
322 call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer)
323 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
324 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
325 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(1)%buffer, buffer)
326 end if
327 end if
328 ! TODO: Mark as read-only in future enhancement
329 else
330 ! Failed to load hex view
331 call terminal_cleanup()
332 write(error_unit, '(A)') 'Error: Failed to load binary file'
333 stop 1
334 end if
335 else
336 ! User cancelled - cleanup and exit
337 call terminal_cleanup()
338 stop
339 end if
340 else
341 ! If file doesn't exist, create empty buffer for new file
342 call init_buffer(buffer)
343 if (allocated(editor%filename)) deallocate(editor%filename)
344 allocate(character(len=len_trim(filename)) :: editor%filename)
345 editor%filename = trim(filename)
346 end if
347 else
348 ! Only initialize empty buffer if we don't have restored tabs
349 if (.not. (allocated(editor%tabs) .and. editor%active_tab_index > 0)) then
350 call init_buffer(buffer)
351 end if
352 end if
353
354 ! Save initial file state for undo (position 0)
355 call save_initial_state_for_undo(buffer, editor)
356
357 ! First-run experience: show LSP server installer panel
358 if (is_first_run()) then
359 call show_lsp_server_installer_panel(editor%lsp_installer_panel)
360 call mark_first_run_complete()
361 end if
362
363 ! Initial render
364 call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
365
366 ! Main event loop
367 do while (running)
368 ! Process any LSP messages
369 call process_server_messages(editor%lsp_manager)
370
371 ! Sync local buffer from tab after LSP processing (in case LSP modified it)
372 block
373 logical :: should_render
374 should_render = .false.
375
376 ! Check if LSP set the UI changed flag (e.g., code actions panel shown)
377 if (g_lsp_ui_changed) then
378 should_render = .true.
379 g_lsp_ui_changed = .false.
380 end if
381
382 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
383 ! Check if LSP set the modified flag
384 if (g_lsp_modified_buffer) then
385 should_render = .true.
386 g_lsp_modified_buffer = .false.
387 end if
388
389 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer)
390
391 ! Also sync to active pane buffer if panes exist (so pane doesn't overwrite LSP changes)
392 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
393 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
394 status = editor%tabs(editor%active_tab_index)%active_pane_index
395 if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then
396 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(status)%buffer, buffer)
397 end if
398 end if
399 end if
400
401 ! Render immediately if LSP modified the buffer (do this OUTSIDE the if block)
402 if (should_render) then
403 if (editor%fuss_mode_active) then
404 call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive)
405 else
406 call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
407 end if
408 end if
409 end block
410
411 ! Flush any pending document changes to LSP
412 call flush_pending_document_changes(editor)
413
414 ! Get input
415 call get_key_input(key_input, status)
416
417 if (status == 0) then
418 ! Sync buffer before and after input when using panes
419 ! Before: copy active pane's buffer -> global buffer
420 ! After: copy global buffer -> active pane's buffer
421 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
422 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
423 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
424 ! Get active pane index
425 status = editor%tabs(editor%active_tab_index)%active_pane_index
426 if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then
427 ! Copy active pane's buffer to main buffer
428 call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%panes(status)%buffer)
429 end if
430 end if
431 end if
432
433 ! Process input
434 call handle_key_command(key_input, editor, buffer, should_quit)
435
436 ! Sync back to active pane and other instances
437 ! Skip buffer sync for cursor-only moves (buffer content unchanged)
438 if (.not. g_cursor_only_move) then
439 if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
440 if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. &
441 size(editor%tabs(editor%active_tab_index)%panes) > 0) then
442 ! Get active pane index
443 status = editor%tabs(editor%active_tab_index)%active_pane_index
444 if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then
445 ! Copy main buffer back to active pane's buffer
446 call copy_buffer(editor%tabs(editor%active_tab_index)%panes(status)%buffer, buffer)
447
448 ! Sync to all instances of this file
449 if (allocated(editor%tabs(editor%active_tab_index)%panes(status)%filename)) then
450 call sync_buffer_to_all_instances(editor, &
451 editor%tabs(editor%active_tab_index)%panes(status)%filename, buffer)
452 end if
453 end if
454
455 ! Also update tab buffer for backwards compatibility
456 call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer)
457
458 ! Sync modified flag from buffer to tab
459 editor%tabs(editor%active_tab_index)%modified = buffer%modified
460 end if
461 end if
462 end if
463
464 if (should_quit) then
465 running = .false.
466 else
467 ! Re-render screen after each command
468 ! Use fast path for cursor-only movements
469 if (g_cursor_only_move) then
470 call render_cursor_only(buffer, editor, allocated(search_pattern), match_case_sensitive)
471 g_cursor_only_move = .false.
472 else if (editor%fuss_mode_active) then
473 call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive)
474 else
475 call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
476 end if
477 end if
478 end if
479 end do
480
481 ! IMPORTANT: Save workspace state FIRST, before any prompts or cleanup
482 ! This ensures we capture the current state before tabs might be closed
483 if (should_quit .and. allocated(editor%workspace_path)) then
484 call workspace_save_state(editor, editor%workspace_path, workspace_success)
485 end if
486
487 ! Handle unsaved files - prompt for save/backup
488 if (allocated(editor%tabs) .and. allocated(editor%workspace_path)) then
489 ! Workspace mode - handle all modified tabs
490 call handle_unsaved_files_on_quit(editor, buffer, should_quit)
491 ! If user cancelled (should_quit = .false.), skip cleanup and restart loop
492 else if (buffer%modified .and. allocated(editor%workspace_path) .and. allocated(editor%filename)) then
493 ! Single-file mode - handle the current buffer if modified
494 call handle_single_file_on_quit(buffer, editor, should_quit)
495 end if
496
497 ! Only proceed with cleanup if actually quitting
498 if (should_quit) then
499
500 ! Cleanup
501 call cleanup_renderer()
502 call cleanup_command_handler()
503 call terminal_cleanup()
504 call cleanup_editor(editor)
505 call cleanup_buffer(buffer)
506 else
507 ! User cancelled quit - re-render and continue
508 running = .true.
509 call terminal_clear_screen()
510 if (editor%fuss_mode_active) then
511 call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive)
512 else
513 call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
514 end if
515 end if
516
517 contains
518
519 ! Handler for LSP diagnostics notifications (with server attribution)
520 subroutine handle_diagnostics(notification, server_index)
521 use lsp_protocol_module, only: lsp_message_t
522 use diagnostics_module, only: parse_diagnostics_from_params_with_server
523 use terminal_io_module, only: terminal_write
524 type(lsp_message_t), intent(in) :: notification
525 integer, intent(in) :: server_index
526
527 ! Parse and store diagnostics with server attribution (for multi-LSP)
528 ! This keeps diagnostics from different servers separate
529 call parse_diagnostics_from_params_with_server(editor%diagnostics, notification%params, server_index)
530 end subroutine handle_diagnostics
531
532 ! Flush pending document changes for all tabs
533 subroutine flush_pending_document_changes(editor)
534 use document_sync_module, only: flush_pending_changes
535 type(editor_state_t), intent(inout) :: editor
536 integer :: i
537
538 ! Check all tabs for pending changes
539 if (allocated(editor%tabs)) then
540 do i = 1, size(editor%tabs)
541 if (editor%tabs(i)%num_lsp_servers > 0) then
542 call flush_pending_changes(editor%tabs(i)%document_sync, &
543 editor%lsp_manager, .false.)
544 end if
545 end do
546 end if
547 end subroutine flush_pending_document_changes
548
549 subroutine read_file_type(is_directory)
550 integer, intent(out) :: is_directory
551 character(len=20) :: file_type
552 integer :: unit, ios
553
554 is_directory = 1 ! Default to not a directory
555
556 open(newunit=unit, file='/tmp/.fac_filetype', status='old', iostat=ios)
557 if (ios == 0) then
558 read(unit, '(A)', iostat=ios) file_type
559 close(unit)
560 call execute_command_line('rm -f /tmp/.fac_filetype', wait=.true.)
561 if (ios == 0) then
562 if (trim(file_type) == 'Directory') then
563 is_directory = 0
564 end if
565 end if
566 end if
567 end subroutine read_file_type
568
569 subroutine get_workspace_path(path)
570 character(len=:), allocatable, intent(out) :: path
571 character(len=1024) :: buffer
572 integer :: status
573
574 ! Use execute_command_line to get current directory
575 call execute_command_line('pwd > /tmp/fac_pwd.txt', wait=.true., exitstat=status)
576 if (status == 0) then
577 open(unit=99, file='/tmp/fac_pwd.txt', status='old', action='read', iostat=status)
578 if (status == 0) then
579 read(99, '(A)', iostat=status) buffer
580 close(99, status='delete')
581 if (status == 0) then
582 path = trim(buffer)
583 return
584 end if
585 end if
586 end if
587 ! Fallback if command fails
588 path = '.'
589 end subroutine get_workspace_path
590
591 subroutine print_help()
592 write(output_unit, '(A)') 'fac - Fortran text editor'
593 write(output_unit, '(A,A)') 'Version: ', VERSION
594 write(output_unit, '(A)') ''
595 write(output_unit, '(A)') 'Usage:'
596 write(output_unit, '(A)') ' fac [filename] Open a file for editing'
597 write(output_unit, '(A)') ' fac [directory] Open directory in workspace mode'
598 write(output_unit, '(A)') ' fac Start with empty buffer'
599 write(output_unit, '(A)') ' fac --version, -v Show version information'
600 write(output_unit, '(A)') ' fac --help, -h Show this help message'
601 write(output_unit, '(A)') ' fac -w <dir> [file] Set LSP workspace root to <dir>'
602 write(output_unit, '(A)') ' fac --workspace <dir> Same as -w'
603 write(output_unit, '(A)') ''
604 write(output_unit, '(A)') 'Key Bindings:'
605 write(output_unit, '(A)') ' Ctrl-Q Quit'
606 write(output_unit, '(A)') ' Ctrl-S Save'
607 write(output_unit, '(A)') ' Ctrl-F Find/Replace (unified prompt)'
608 write(output_unit, '(A)') ' Ctrl-G Go to line'
609 write(output_unit, '(A)') ' Ctrl-Z Undo'
610 write(output_unit, '(A)') ' Ctrl-Y Redo'
611 write(output_unit, '(A)') ' Ctrl-X Cut line'
612 write(output_unit, '(A)') ' Ctrl-C Copy line'
613 write(output_unit, '(A)') ' Ctrl-V Paste'
614 write(output_unit, '(A)') ' Ctrl-D Delete line'
615 write(output_unit, '(A)') ' Ctrl-L Toggle line numbers'
616 write(output_unit, '(A)') ' Ctrl-P Cycle yank stack backward'
617 write(output_unit, '(A)') ' Ctrl-N Cycle yank stack forward'
618 write(output_unit, '(A)') ' Ctrl-T New tab'
619 write(output_unit, '(A)') ' Ctrl-W Close tab'
620 write(output_unit, '(A)') ' Alt-1 to Alt-9 Switch to tab 1-9'
621 write(output_unit, '(A)') ' Ctrl-B Toggle file tree (fuss mode)'
622 write(output_unit, '(A)') ' Ctrl-\ Split pane vertically'
623 write(output_unit, '(A)') ' Ctrl-_ Split pane horizontally'
624 write(output_unit, '(A)') ' Ctrl-Arrow Navigate between panes'
625 write(output_unit, '(A)') ' Alt-X Close current pane'
626 write(output_unit, '(A)') ''
627 write(output_unit, '(A)') 'Find/Replace Mode (Ctrl-F):'
628 write(output_unit, '(A)') ' Ctrl-F Find next match'
629 write(output_unit, '(A)') ' Ctrl-R Replace current match'
630 write(output_unit, '(A)') ' Ctrl-A Replace all matches'
631 write(output_unit, '(A)') ' Alt-C Toggle case sensitivity'
632 write(output_unit, '(A)') ' Alt-W Toggle whole word match'
633 write(output_unit, '(A)') ' Alt-R Toggle regex mode'
634 write(output_unit, '(A)') ' Tab Switch between find/replace fields'
635 write(output_unit, '(A)') ' Enter Jump to match and exit'
636 write(output_unit, '(A)') ' ESC Exit find/replace mode'
637 end subroutine print_help
638
639 !> Prompt for filename and save an untitled file
640 subroutine prompt_for_filename_and_save(editor, buffer, tab_index, success)
641 use text_prompt_module, only: show_text_prompt
642 type(editor_state_t), intent(inout) :: editor
643 type(buffer_t), intent(inout) :: buffer
644 integer, intent(in) :: tab_index
645 logical, intent(out) :: success
646 character(len=512) :: new_filename
647 logical :: cancelled
648 integer :: save_status
649
650 success = .false.
651
652 ! Prompt for filename
653 call show_text_prompt('Save as: ', new_filename, cancelled, editor%screen_rows)
654
655 if (cancelled .or. len_trim(new_filename) == 0) then
656 ! User cancelled or entered empty filename
657 return
658 end if
659
660 ! Update the tab filename
661 if (allocated(editor%tabs(tab_index)%filename)) then
662 deallocate(editor%tabs(tab_index)%filename)
663 end if
664 allocate(character(len=len_trim(new_filename)) :: editor%tabs(tab_index)%filename)
665 editor%tabs(tab_index)%filename = trim(new_filename)
666
667 ! Update pane filename if exists
668 if (allocated(editor%tabs(tab_index)%panes)) then
669 if (size(editor%tabs(tab_index)%panes) > 0) then
670 if (allocated(editor%tabs(tab_index)%panes(1)%filename)) then
671 deallocate(editor%tabs(tab_index)%panes(1)%filename)
672 end if
673 allocate(character(len=len_trim(new_filename)) :: editor%tabs(tab_index)%panes(1)%filename)
674 editor%tabs(tab_index)%panes(1)%filename = trim(new_filename)
675 end if
676 end if
677
678 ! Update editor filename
679 if (allocated(editor%filename)) then
680 deallocate(editor%filename)
681 end if
682 allocate(character(len=len_trim(new_filename)) :: editor%filename)
683 editor%filename = trim(new_filename)
684
685 ! Now save the file
686 call buffer_save_file(buffer, new_filename, save_status)
687 if (save_status == 0) then
688 buffer%modified = .false.
689 editor%tabs(tab_index)%modified = .false.
690 success = .true.
691 end if
692 end subroutine prompt_for_filename_and_save
693
694 !> Handle unsaved files on quit - prompt for save/backup
695 subroutine handle_unsaved_files_on_quit(editor, buffer, should_quit)
696 type(editor_state_t), intent(inout) :: editor
697 type(buffer_t), intent(inout) :: buffer
698 logical, intent(inout) :: should_quit
699 type(save_prompt_result_t) :: prompt_result
700 integer :: i, save_status, modified_count, current_modified
701 logical :: backup_success, save_all
702
703 ! Count total modified tabs
704 modified_count = 0
705 do i = 1, size(editor%tabs)
706 if (editor%tabs(i)%modified) then
707 modified_count = modified_count + 1
708 end if
709 end do
710
711 ! Process each modified tab
712 current_modified = 0
713 save_all = .false.
714 do i = 1, size(editor%tabs)
715 if (editor%tabs(i)%modified) then
716 current_modified = current_modified + 1
717
718 ! Skip prompt if "save all" was selected
719 if (.not. save_all) then
720 ! Prompt user for this file with progress
721 call save_prompt(editor%tabs(i)%filename, prompt_result, current_modified, modified_count)
722
723 if (prompt_result%action == 'a') then
724 ! Save all - set flag and treat as save for this file
725 save_all = .true.
726 prompt_result%action = 's'
727 end if
728
729 if (prompt_result%action == 'c') then
730 ! User cancelled - don't quit
731 should_quit = .false.
732 return
733 end if
734 else
735 ! Save all is active - auto-save this file
736 prompt_result%action = 's'
737 end if
738
739 if (prompt_result%action == 's') then
740 ! User wants to save - switch to this tab and save
741 editor%active_tab_index = i
742 call switch_to_tab_with_buffer(editor, i, buffer)
743
744 ! Check if this is an [Untitled] file - need to prompt for filename
745 if (index(editor%tabs(i)%filename, '[Untitled') == 1) then
746 call prompt_for_filename_and_save(editor, buffer, i, should_quit)
747 if (.not. should_quit) return ! User cancelled
748 else
749 ! Save the file (no backup - it's saved!)
750 call buffer_save_file(buffer, editor%tabs(i)%filename, save_status)
751 if (save_status == 0) then
752 buffer%modified = .false.
753 editor%tabs(i)%modified = .false.
754 end if
755 end if
756
757 else if (prompt_result%action == 'd') then
758 ! User wants to discard - create backup for later recovery
759 ! But skip [Untitled] files - they're in-memory only
760 if (index(editor%tabs(i)%filename, '[Untitled') /= 1) then
761 call backup_create(editor%workspace_path, editor%tabs(i)%filename, backup_success)
762 end if
763 ! Continue even if backup fails
764 end if
765 end if
766 end do
767
768 ! All handled - proceed with quit
769 should_quit = .true.
770 end subroutine handle_unsaved_files_on_quit
771
772 !> Handle single file on quit (for non-workspace mode)
773 subroutine handle_single_file_on_quit(buffer, editor, should_quit)
774 type(buffer_t), intent(inout) :: buffer
775 type(editor_state_t), intent(inout) :: editor
776 logical, intent(inout) :: should_quit
777 type(save_prompt_result_t) :: prompt_result
778 integer :: save_status
779 logical :: backup_success
780
781 if (.not. buffer%modified) return
782 if (.not. allocated(editor%filename)) return
783
784 ! Prompt user for this file
785 call save_prompt(editor%filename, prompt_result)
786
787 if (prompt_result%action == 'y') then
788 ! User wants to save
789 call buffer_save_file(buffer, editor%filename, save_status)
790 if (save_status == 0) then
791 buffer%modified = .false.
792 editor%modified = .false.
793 end if
794 else if (prompt_result%action == 'n') then
795 ! User wants to skip - create backup
796 call backup_create(editor%workspace_path, editor%filename, backup_success)
797 ! Continue even if backup fails
798 else if (prompt_result%action == 'c') then
799 ! User cancelled - don't quit
800 should_quit = .false.
801 return
802 end if
803
804 ! Proceed with quit
805 should_quit = .true.
806 end subroutine handle_single_file_on_quit
807
808 !> Handle a restored file - open in tab or reload existing tab
809 subroutine handle_restored_file(editor, buffer, restored_file)
810 use text_buffer_module, only: buffer_load_file
811 type(editor_state_t), intent(inout) :: editor
812 type(buffer_t), intent(inout) :: buffer
813 character(len=*), intent(in) :: restored_file
814 integer :: tab_idx, status
815 logical :: found
816
817 ! Check if file is already open in a tab
818 found = .false.
819 if (allocated(editor%tabs)) then
820 do tab_idx = 1, size(editor%tabs)
821 if (allocated(editor%tabs(tab_idx)%filename)) then
822 if (trim(editor%tabs(tab_idx)%filename) == trim(restored_file)) then
823 ! File is already in a tab - reload it
824 found = .true.
825 call switch_to_tab_with_buffer(editor, tab_idx, buffer)
826 call buffer_load_file(buffer, restored_file, status)
827 if (status == 0) then
828 buffer%modified = .false.
829 editor%tabs(tab_idx)%modified = .false.
830 end if
831 exit
832 end if
833 end if
834 end do
835 end if
836
837 ! If not found, create a new tab with this file
838 if (.not. found) then
839 call create_tab(editor, restored_file)
840 if (allocated(editor%tabs) .and. editor%active_tab_index > 0) then
841 ! Load the restored file into the buffer
842 call buffer_load_file(buffer, restored_file, status)
843 if (status == 0) then
844 buffer%modified = .false.
845 ! Now switch to the tab and sync the buffer
846 call switch_to_tab_with_buffer(editor, editor%active_tab_index, buffer)
847 if (allocated(editor%tabs) .and. editor%active_tab_index <= size(editor%tabs)) then
848 editor%tabs(editor%active_tab_index)%modified = .false.
849 end if
850 end if
851 end if
852 end if
853 end subroutine handle_restored_file
854
855 !> Handle backup restoration on workspace load
856 subroutine handle_backup_restoration(editor, buffer)
857 use backup_module, only: backup_list, backup_info_t, backup_restore, backup_delete
858 type(editor_state_t), intent(inout) :: editor
859 type(buffer_t), intent(inout) :: buffer
860 type(backup_info_t), allocatable :: backups(:), unique_backups(:)
861 integer :: backup_count, unique_count, i, j, status
862 character :: choice
863 character(len=32) :: key_input
864 logical :: restore_success, found
865 integer(int64) :: current_timestamp, best_timestamp
866
867 ! Get list of backups
868 call backup_list(editor%workspace_path, backups, backup_count)
869
870 ! Deduplicate - keep only the most recent backup for each unique file
871 allocate(unique_backups(backup_count))
872 unique_count = 0
873
874 do i = 1, backup_count
875 if (len_trim(backups(i)%original_file) == 0) cycle
876
877 ! Skip [Untitled] backups - they're in-memory only
878 if (index(backups(i)%original_file, '[Untitled') == 1) then
879 call backup_delete(backups(i)%backup_file)
880 cycle
881 end if
882
883 ! Check if we already have a backup for this file
884 found = .false.
885 do j = 1, unique_count
886 if (trim(unique_backups(j)%original_file) == trim(backups(i)%original_file)) then
887 ! Found duplicate - keep the one with newer timestamp
888 found = .true.
889 read(backups(i)%timestamp, *, iostat=status) current_timestamp
890 read(unique_backups(j)%timestamp, *, iostat=status) best_timestamp
891 if (status == 0 .and. current_timestamp > best_timestamp) then
892 ! This backup is newer - replace it and delete old one
893 call backup_delete(unique_backups(j)%backup_file)
894 unique_backups(j) = backups(i)
895 else
896 ! Keep existing, delete this duplicate
897 call backup_delete(backups(i)%backup_file)
898 end if
899 exit
900 end if
901 end do
902
903 ! If not found, add to unique list
904 if (.not. found) then
905 unique_count = unique_count + 1
906 unique_backups(unique_count) = backups(i)
907 end if
908 end do
909
910 ! Prompt for each unique backup
911 i = 1
912 do while (i <= unique_count)
913 ! Show restore prompt with progress
914 choice = backup_prompt_restore(unique_backups(i)%original_file, i, unique_count, &
915 unique_backups(i)%timestamp)
916
917 if (choice == 'r') then
918 ! Restore the backup
919 call backup_restore(unique_backups(i)%backup_file, &
920 unique_backups(i)%original_file, restore_success)
921
922 if (restore_success) then
923 ! After successful restore, open/reload this file in a tab
924 call handle_restored_file(editor, buffer, unique_backups(i)%original_file)
925 end if
926
927 ! Show confirmation
928 call terminal_clear_screen()
929 call terminal_move_cursor(1, 1)
930 if (restore_success) then
931 call terminal_write('Restored: ' // trim(unique_backups(i)%original_file))
932 else
933 call terminal_write('Failed to restore: ' // trim(unique_backups(i)%original_file))
934 end if
935 call terminal_move_cursor(3, 1)
936 call terminal_write('Press any key to continue...')
937 call get_key_input(key_input, status)
938
939 i = i + 1 ! Move to next
940
941 else if (choice == 'd') then
942 ! Delete backup - keep current file
943 call backup_delete(unique_backups(i)%backup_file)
944 i = i + 1 ! Move to next
945
946 else if (choice == 'c') then
947 ! Compare - show diff (then loop back to prompt again)
948 call show_backup_diff(unique_backups(i)%backup_file, &
949 unique_backups(i)%original_file)
950 ! Don't increment i - re-prompt for same file
951 end if
952 end do
953
954 ! Clear screen after all prompts
955 call terminal_clear_screen()
956 end subroutine handle_backup_restoration
957
958 !> Show diff between backup and current file
959 subroutine show_backup_diff(backup_file, original_file)
960 character(len=*), intent(in) :: backup_file, original_file
961 character(len=512) :: cmd
962 character(len=32) :: key_input
963 integer :: status
964
965 ! backup_file already contains full path, use it directly
966 ! Clear screen and show diff
967 call terminal_clear_screen()
968 call terminal_move_cursor(1, 1)
969 call terminal_write('Diff: ' // trim(original_file) // ' vs backup')
970 call terminal_move_cursor(2, 1)
971 call terminal_write('=' // repeat('=', 70))
972
973 ! Shell out to diff command
974 write(cmd, '(A,A,A,A,A)') "diff -u '", trim(backup_file), "' '", trim(original_file), "'"
975 call execute_command_line(trim(cmd), wait=.true.)
976
977 ! Wait for user
978 call terminal_move_cursor(24, 1)
979 call terminal_write('Press any key to continue...')
980 call get_key_input(key_input, status)
981 end subroutine show_backup_diff
982
983 ! Register all available commands for the command palette
984 subroutine register_all_commands()
985 ! File operations
986 call register_command('Save File', 'save', 'Ctrl+S', 'File')
987 call register_command('Save All', 'save-all', 'Ctrl+Shift+S', 'File')
988 call register_command('Quit', 'quit', 'Ctrl+Q', 'File')
989 call register_command('Open File', 'open', 'Ctrl+O', 'File')
990 call register_command('Toggle File Tree', 'toggle-tree', 'F3', 'File')
991
992 ! Edit operations
993 call register_command('Copy', 'copy', 'Ctrl+C', 'Edit')
994 call register_command('Paste', 'paste', 'Ctrl+V', 'Edit')
995 call register_command('Cut', 'cut', 'Ctrl+X', 'Edit')
996 call register_command('Undo', 'undo', 'Ctrl+Z', 'Edit')
997 call register_command('Redo', 'redo', 'Ctrl+Y', 'Edit')
998
999 ! Search operations
1000 call register_command('Find', 'find', 'Ctrl+F', 'Search')
1001 call register_command('Replace', 'replace', 'Ctrl+H', 'Search')
1002 call register_command('Find Next', 'find-next', 'Ctrl+G', 'Search')
1003 call register_command('Find Previous', 'find-prev', 'Shift+Ctrl+G', 'Search')
1004
1005 ! Navigation
1006 call register_command('Go to Line', 'goto-line', 'Ctrl+G', 'Navigation')
1007 call register_command('Go to Definition', 'goto-def', 'F12', 'Navigation')
1008 call register_command('Find References', 'find-refs', 'Shift+F12', 'Navigation')
1009 call register_command('Jump Back', 'jump-back', 'Alt+,', 'Navigation')
1010 call register_command('Go to Symbol', 'goto-symbol', 'Ctrl+Shift+O', 'Navigation')
1011
1012 ! LSP features
1013 call register_command('Code Actions', 'code-actions', 'Ctrl+.', 'LSP')
1014 call register_command('Rename Symbol', 'rename', 'F2', 'LSP')
1015 call register_command('Show Diagnostics', 'diagnostics', 'Ctrl+Shift+D', 'LSP')
1016 call register_command('Show Hover Info', 'hover', 'Ctrl+K Ctrl+I', 'LSP')
1017
1018 ! View
1019 call register_command('Split Vertical', 'split-v', 'Ctrl+\\', 'View')
1020 call register_command('Split Horizontal', 'split-h', 'Ctrl+Shift+\\', 'View')
1021 call register_command('Close Pane', 'close-pane', 'Ctrl+W', 'View')
1022 call register_command('Navigate Pane Left', 'pane-left', 'Ctrl+H', 'View')
1023 call register_command('Navigate Pane Right', 'pane-right', 'Ctrl+L', 'View')
1024 call register_command('Navigate Pane Up', 'pane-up', 'Ctrl+K', 'View')
1025 call register_command('Navigate Pane Down', 'pane-down', 'Ctrl+J', 'View')
1026
1027 ! Help
1028 call register_command('Show Help', 'help', '?', 'Help')
1029 call register_command('Command Palette', 'palette', 'Ctrl+Shift+P', 'Help')
1030 end subroutine register_all_commands
1031
1032 end program facsimile