Fortran · 45996 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 if (editor%fuss_mode_active) then
469 call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive)
470 else
471 call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
472 end if
473 end if
474 end if
475 end do
476
477 ! IMPORTANT: Save workspace state FIRST, before any prompts or cleanup
478 ! This ensures we capture the current state before tabs might be closed
479 if (should_quit .and. allocated(editor%workspace_path)) then
480 call workspace_save_state(editor, editor%workspace_path, workspace_success)
481 end if
482
483 ! Handle unsaved files - prompt for save/backup
484 if (allocated(editor%tabs) .and. allocated(editor%workspace_path)) then
485 ! Workspace mode - handle all modified tabs
486 call handle_unsaved_files_on_quit(editor, buffer, should_quit)
487 ! If user cancelled (should_quit = .false.), skip cleanup and restart loop
488 else if (buffer%modified .and. allocated(editor%workspace_path) .and. allocated(editor%filename)) then
489 ! Single-file mode - handle the current buffer if modified
490 call handle_single_file_on_quit(buffer, editor, should_quit)
491 end if
492
493 ! Only proceed with cleanup if actually quitting
494 if (should_quit) then
495
496 ! Cleanup
497 call cleanup_renderer()
498 call cleanup_command_handler()
499 call terminal_cleanup()
500 call cleanup_editor(editor)
501 call cleanup_buffer(buffer)
502 else
503 ! User cancelled quit - re-render and continue
504 running = .true.
505 call terminal_clear_screen()
506 if (editor%fuss_mode_active) then
507 call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive)
508 else
509 call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive)
510 end if
511 end if
512
513 contains
514
515 ! Handler for LSP diagnostics notifications (with server attribution)
516 subroutine handle_diagnostics(notification, server_index)
517 use lsp_protocol_module, only: lsp_message_t
518 use diagnostics_module, only: parse_diagnostics_from_params_with_server
519 use terminal_io_module, only: terminal_write
520 type(lsp_message_t), intent(in) :: notification
521 integer, intent(in) :: server_index
522
523 ! Parse and store diagnostics with server attribution (for multi-LSP)
524 ! This keeps diagnostics from different servers separate
525 call parse_diagnostics_from_params_with_server(editor%diagnostics, notification%params, server_index)
526 end subroutine handle_diagnostics
527
528 ! Flush pending document changes for all tabs
529 subroutine flush_pending_document_changes(editor)
530 use document_sync_module, only: flush_pending_changes
531 type(editor_state_t), intent(inout) :: editor
532 integer :: i
533
534 ! Check all tabs for pending changes
535 if (allocated(editor%tabs)) then
536 do i = 1, size(editor%tabs)
537 if (editor%tabs(i)%num_lsp_servers > 0) then
538 call flush_pending_changes(editor%tabs(i)%document_sync, &
539 editor%lsp_manager, .false.)
540 end if
541 end do
542 end if
543 end subroutine flush_pending_document_changes
544
545 subroutine read_file_type(is_directory)
546 integer, intent(out) :: is_directory
547 character(len=20) :: file_type
548 integer :: unit, ios
549
550 is_directory = 1 ! Default to not a directory
551
552 open(newunit=unit, file='/tmp/.fac_filetype', status='old', iostat=ios)
553 if (ios == 0) then
554 read(unit, '(A)', iostat=ios) file_type
555 close(unit)
556 call execute_command_line('rm -f /tmp/.fac_filetype', wait=.true.)
557 if (ios == 0) then
558 if (trim(file_type) == 'Directory') then
559 is_directory = 0
560 end if
561 end if
562 end if
563 end subroutine read_file_type
564
565 subroutine get_workspace_path(path)
566 character(len=:), allocatable, intent(out) :: path
567 character(len=1024) :: buffer
568 integer :: status
569
570 ! Use execute_command_line to get current directory
571 call execute_command_line('pwd > /tmp/fac_pwd.txt', wait=.true., exitstat=status)
572 if (status == 0) then
573 open(unit=99, file='/tmp/fac_pwd.txt', status='old', action='read', iostat=status)
574 if (status == 0) then
575 read(99, '(A)', iostat=status) buffer
576 close(99, status='delete')
577 if (status == 0) then
578 path = trim(buffer)
579 return
580 end if
581 end if
582 end if
583 ! Fallback if command fails
584 path = '.'
585 end subroutine get_workspace_path
586
587 subroutine print_help()
588 write(output_unit, '(A)') 'fac - Fortran text editor'
589 write(output_unit, '(A,A)') 'Version: ', VERSION
590 write(output_unit, '(A)') ''
591 write(output_unit, '(A)') 'Usage:'
592 write(output_unit, '(A)') ' fac [filename] Open a file for editing'
593 write(output_unit, '(A)') ' fac [directory] Open directory in workspace mode'
594 write(output_unit, '(A)') ' fac Start with empty buffer'
595 write(output_unit, '(A)') ' fac --version, -v Show version information'
596 write(output_unit, '(A)') ' fac --help, -h Show this help message'
597 write(output_unit, '(A)') ' fac -w <dir> [file] Set LSP workspace root to <dir>'
598 write(output_unit, '(A)') ' fac --workspace <dir> Same as -w'
599 write(output_unit, '(A)') ''
600 write(output_unit, '(A)') 'Key Bindings:'
601 write(output_unit, '(A)') ' Ctrl-Q Quit'
602 write(output_unit, '(A)') ' Ctrl-S Save'
603 write(output_unit, '(A)') ' Ctrl-F Find/Replace (unified prompt)'
604 write(output_unit, '(A)') ' Ctrl-G Go to line'
605 write(output_unit, '(A)') ' Ctrl-Z Undo'
606 write(output_unit, '(A)') ' Ctrl-Y Redo'
607 write(output_unit, '(A)') ' Ctrl-X Cut line'
608 write(output_unit, '(A)') ' Ctrl-C Copy line'
609 write(output_unit, '(A)') ' Ctrl-V Paste'
610 write(output_unit, '(A)') ' Ctrl-D Delete line'
611 write(output_unit, '(A)') ' Ctrl-L Toggle line numbers'
612 write(output_unit, '(A)') ' Ctrl-P Cycle yank stack backward'
613 write(output_unit, '(A)') ' Ctrl-N Cycle yank stack forward'
614 write(output_unit, '(A)') ' Ctrl-T New tab'
615 write(output_unit, '(A)') ' Ctrl-W Close tab'
616 write(output_unit, '(A)') ' Alt-1 to Alt-9 Switch to tab 1-9'
617 write(output_unit, '(A)') ' Ctrl-B Toggle file tree (fuss mode)'
618 write(output_unit, '(A)') ' Ctrl-\ Split pane vertically'
619 write(output_unit, '(A)') ' Ctrl-_ Split pane horizontally'
620 write(output_unit, '(A)') ' Ctrl-Arrow Navigate between panes'
621 write(output_unit, '(A)') ' Alt-X Close current pane'
622 write(output_unit, '(A)') ''
623 write(output_unit, '(A)') 'Find/Replace Mode (Ctrl-F):'
624 write(output_unit, '(A)') ' Ctrl-F Find next match'
625 write(output_unit, '(A)') ' Ctrl-R Replace current match'
626 write(output_unit, '(A)') ' Ctrl-A Replace all matches'
627 write(output_unit, '(A)') ' Alt-C Toggle case sensitivity'
628 write(output_unit, '(A)') ' Alt-W Toggle whole word match'
629 write(output_unit, '(A)') ' Alt-R Toggle regex mode'
630 write(output_unit, '(A)') ' Tab Switch between find/replace fields'
631 write(output_unit, '(A)') ' Enter Jump to match and exit'
632 write(output_unit, '(A)') ' ESC Exit find/replace mode'
633 end subroutine print_help
634
635 !> Prompt for filename and save an untitled file
636 subroutine prompt_for_filename_and_save(editor, buffer, tab_index, success)
637 use text_prompt_module, only: show_text_prompt
638 type(editor_state_t), intent(inout) :: editor
639 type(buffer_t), intent(inout) :: buffer
640 integer, intent(in) :: tab_index
641 logical, intent(out) :: success
642 character(len=512) :: new_filename
643 logical :: cancelled
644 integer :: save_status
645
646 success = .false.
647
648 ! Prompt for filename
649 call show_text_prompt('Save as: ', new_filename, cancelled, editor%screen_rows)
650
651 if (cancelled .or. len_trim(new_filename) == 0) then
652 ! User cancelled or entered empty filename
653 return
654 end if
655
656 ! Update the tab filename
657 if (allocated(editor%tabs(tab_index)%filename)) then
658 deallocate(editor%tabs(tab_index)%filename)
659 end if
660 allocate(character(len=len_trim(new_filename)) :: editor%tabs(tab_index)%filename)
661 editor%tabs(tab_index)%filename = trim(new_filename)
662
663 ! Update pane filename if exists
664 if (allocated(editor%tabs(tab_index)%panes)) then
665 if (size(editor%tabs(tab_index)%panes) > 0) then
666 if (allocated(editor%tabs(tab_index)%panes(1)%filename)) then
667 deallocate(editor%tabs(tab_index)%panes(1)%filename)
668 end if
669 allocate(character(len=len_trim(new_filename)) :: editor%tabs(tab_index)%panes(1)%filename)
670 editor%tabs(tab_index)%panes(1)%filename = trim(new_filename)
671 end if
672 end if
673
674 ! Update editor filename
675 if (allocated(editor%filename)) then
676 deallocate(editor%filename)
677 end if
678 allocate(character(len=len_trim(new_filename)) :: editor%filename)
679 editor%filename = trim(new_filename)
680
681 ! Now save the file
682 call buffer_save_file(buffer, new_filename, save_status)
683 if (save_status == 0) then
684 buffer%modified = .false.
685 editor%tabs(tab_index)%modified = .false.
686 success = .true.
687 end if
688 end subroutine prompt_for_filename_and_save
689
690 !> Handle unsaved files on quit - prompt for save/backup
691 subroutine handle_unsaved_files_on_quit(editor, buffer, should_quit)
692 type(editor_state_t), intent(inout) :: editor
693 type(buffer_t), intent(inout) :: buffer
694 logical, intent(inout) :: should_quit
695 type(save_prompt_result_t) :: prompt_result
696 integer :: i, save_status, modified_count, current_modified
697 logical :: backup_success, save_all
698
699 ! Count total modified tabs
700 modified_count = 0
701 do i = 1, size(editor%tabs)
702 if (editor%tabs(i)%modified) then
703 modified_count = modified_count + 1
704 end if
705 end do
706
707 ! Process each modified tab
708 current_modified = 0
709 save_all = .false.
710 do i = 1, size(editor%tabs)
711 if (editor%tabs(i)%modified) then
712 current_modified = current_modified + 1
713
714 ! Skip prompt if "save all" was selected
715 if (.not. save_all) then
716 ! Prompt user for this file with progress
717 call save_prompt(editor%tabs(i)%filename, prompt_result, current_modified, modified_count)
718
719 if (prompt_result%action == 'a') then
720 ! Save all - set flag and treat as save for this file
721 save_all = .true.
722 prompt_result%action = 's'
723 end if
724
725 if (prompt_result%action == 'c') then
726 ! User cancelled - don't quit
727 should_quit = .false.
728 return
729 end if
730 else
731 ! Save all is active - auto-save this file
732 prompt_result%action = 's'
733 end if
734
735 if (prompt_result%action == 's') then
736 ! User wants to save - switch to this tab and save
737 editor%active_tab_index = i
738 call switch_to_tab_with_buffer(editor, i, buffer)
739
740 ! Check if this is an [Untitled] file - need to prompt for filename
741 if (index(editor%tabs(i)%filename, '[Untitled') == 1) then
742 call prompt_for_filename_and_save(editor, buffer, i, should_quit)
743 if (.not. should_quit) return ! User cancelled
744 else
745 ! Save the file (no backup - it's saved!)
746 call buffer_save_file(buffer, editor%tabs(i)%filename, save_status)
747 if (save_status == 0) then
748 buffer%modified = .false.
749 editor%tabs(i)%modified = .false.
750 end if
751 end if
752
753 else if (prompt_result%action == 'd') then
754 ! User wants to discard - create backup for later recovery
755 ! But skip [Untitled] files - they're in-memory only
756 if (index(editor%tabs(i)%filename, '[Untitled') /= 1) then
757 call backup_create(editor%workspace_path, editor%tabs(i)%filename, backup_success)
758 end if
759 ! Continue even if backup fails
760 end if
761 end if
762 end do
763
764 ! All handled - proceed with quit
765 should_quit = .true.
766 end subroutine handle_unsaved_files_on_quit
767
768 !> Handle single file on quit (for non-workspace mode)
769 subroutine handle_single_file_on_quit(buffer, editor, should_quit)
770 type(buffer_t), intent(inout) :: buffer
771 type(editor_state_t), intent(inout) :: editor
772 logical, intent(inout) :: should_quit
773 type(save_prompt_result_t) :: prompt_result
774 integer :: save_status
775 logical :: backup_success
776
777 if (.not. buffer%modified) return
778 if (.not. allocated(editor%filename)) return
779
780 ! Prompt user for this file
781 call save_prompt(editor%filename, prompt_result)
782
783 if (prompt_result%action == 'y') then
784 ! User wants to save
785 call buffer_save_file(buffer, editor%filename, save_status)
786 if (save_status == 0) then
787 buffer%modified = .false.
788 editor%modified = .false.
789 end if
790 else if (prompt_result%action == 'n') then
791 ! User wants to skip - create backup
792 call backup_create(editor%workspace_path, editor%filename, backup_success)
793 ! Continue even if backup fails
794 else if (prompt_result%action == 'c') then
795 ! User cancelled - don't quit
796 should_quit = .false.
797 return
798 end if
799
800 ! Proceed with quit
801 should_quit = .true.
802 end subroutine handle_single_file_on_quit
803
804 !> Handle a restored file - open in tab or reload existing tab
805 subroutine handle_restored_file(editor, buffer, restored_file)
806 use text_buffer_module, only: buffer_load_file
807 type(editor_state_t), intent(inout) :: editor
808 type(buffer_t), intent(inout) :: buffer
809 character(len=*), intent(in) :: restored_file
810 integer :: tab_idx, status
811 logical :: found
812
813 ! Check if file is already open in a tab
814 found = .false.
815 if (allocated(editor%tabs)) then
816 do tab_idx = 1, size(editor%tabs)
817 if (allocated(editor%tabs(tab_idx)%filename)) then
818 if (trim(editor%tabs(tab_idx)%filename) == trim(restored_file)) then
819 ! File is already in a tab - reload it
820 found = .true.
821 call switch_to_tab_with_buffer(editor, tab_idx, buffer)
822 call buffer_load_file(buffer, restored_file, status)
823 if (status == 0) then
824 buffer%modified = .false.
825 editor%tabs(tab_idx)%modified = .false.
826 end if
827 exit
828 end if
829 end if
830 end do
831 end if
832
833 ! If not found, create a new tab with this file
834 if (.not. found) then
835 call create_tab(editor, restored_file)
836 if (allocated(editor%tabs) .and. editor%active_tab_index > 0) then
837 ! Load the restored file into the buffer
838 call buffer_load_file(buffer, restored_file, status)
839 if (status == 0) then
840 buffer%modified = .false.
841 ! Now switch to the tab and sync the buffer
842 call switch_to_tab_with_buffer(editor, editor%active_tab_index, buffer)
843 if (allocated(editor%tabs) .and. editor%active_tab_index <= size(editor%tabs)) then
844 editor%tabs(editor%active_tab_index)%modified = .false.
845 end if
846 end if
847 end if
848 end if
849 end subroutine handle_restored_file
850
851 !> Handle backup restoration on workspace load
852 subroutine handle_backup_restoration(editor, buffer)
853 use backup_module, only: backup_list, backup_info_t, backup_restore, backup_delete
854 type(editor_state_t), intent(inout) :: editor
855 type(buffer_t), intent(inout) :: buffer
856 type(backup_info_t), allocatable :: backups(:), unique_backups(:)
857 integer :: backup_count, unique_count, i, j, status
858 character :: choice
859 character(len=32) :: key_input
860 logical :: restore_success, found
861 integer(int64) :: current_timestamp, best_timestamp
862
863 ! Get list of backups
864 call backup_list(editor%workspace_path, backups, backup_count)
865
866 ! Deduplicate - keep only the most recent backup for each unique file
867 allocate(unique_backups(backup_count))
868 unique_count = 0
869
870 do i = 1, backup_count
871 if (len_trim(backups(i)%original_file) == 0) cycle
872
873 ! Skip [Untitled] backups - they're in-memory only
874 if (index(backups(i)%original_file, '[Untitled') == 1) then
875 call backup_delete(backups(i)%backup_file)
876 cycle
877 end if
878
879 ! Check if we already have a backup for this file
880 found = .false.
881 do j = 1, unique_count
882 if (trim(unique_backups(j)%original_file) == trim(backups(i)%original_file)) then
883 ! Found duplicate - keep the one with newer timestamp
884 found = .true.
885 read(backups(i)%timestamp, *, iostat=status) current_timestamp
886 read(unique_backups(j)%timestamp, *, iostat=status) best_timestamp
887 if (status == 0 .and. current_timestamp > best_timestamp) then
888 ! This backup is newer - replace it and delete old one
889 call backup_delete(unique_backups(j)%backup_file)
890 unique_backups(j) = backups(i)
891 else
892 ! Keep existing, delete this duplicate
893 call backup_delete(backups(i)%backup_file)
894 end if
895 exit
896 end if
897 end do
898
899 ! If not found, add to unique list
900 if (.not. found) then
901 unique_count = unique_count + 1
902 unique_backups(unique_count) = backups(i)
903 end if
904 end do
905
906 ! Prompt for each unique backup
907 i = 1
908 do while (i <= unique_count)
909 ! Show restore prompt with progress
910 choice = backup_prompt_restore(unique_backups(i)%original_file, i, unique_count, &
911 unique_backups(i)%timestamp)
912
913 if (choice == 'r') then
914 ! Restore the backup
915 call backup_restore(unique_backups(i)%backup_file, &
916 unique_backups(i)%original_file, restore_success)
917
918 if (restore_success) then
919 ! After successful restore, open/reload this file in a tab
920 call handle_restored_file(editor, buffer, unique_backups(i)%original_file)
921 end if
922
923 ! Show confirmation
924 call terminal_clear_screen()
925 call terminal_move_cursor(1, 1)
926 if (restore_success) then
927 call terminal_write('Restored: ' // trim(unique_backups(i)%original_file))
928 else
929 call terminal_write('Failed to restore: ' // trim(unique_backups(i)%original_file))
930 end if
931 call terminal_move_cursor(3, 1)
932 call terminal_write('Press any key to continue...')
933 call get_key_input(key_input, status)
934
935 i = i + 1 ! Move to next
936
937 else if (choice == 'd') then
938 ! Delete backup - keep current file
939 call backup_delete(unique_backups(i)%backup_file)
940 i = i + 1 ! Move to next
941
942 else if (choice == 'c') then
943 ! Compare - show diff (then loop back to prompt again)
944 call show_backup_diff(unique_backups(i)%backup_file, &
945 unique_backups(i)%original_file)
946 ! Don't increment i - re-prompt for same file
947 end if
948 end do
949
950 ! Clear screen after all prompts
951 call terminal_clear_screen()
952 end subroutine handle_backup_restoration
953
954 !> Show diff between backup and current file
955 subroutine show_backup_diff(backup_file, original_file)
956 character(len=*), intent(in) :: backup_file, original_file
957 character(len=512) :: cmd
958 character(len=32) :: key_input
959 integer :: status
960
961 ! backup_file already contains full path, use it directly
962 ! Clear screen and show diff
963 call terminal_clear_screen()
964 call terminal_move_cursor(1, 1)
965 call terminal_write('Diff: ' // trim(original_file) // ' vs backup')
966 call terminal_move_cursor(2, 1)
967 call terminal_write('=' // repeat('=', 70))
968
969 ! Shell out to diff command
970 write(cmd, '(A,A,A,A,A)') "diff -u '", trim(backup_file), "' '", trim(original_file), "'"
971 call execute_command_line(trim(cmd), wait=.true.)
972
973 ! Wait for user
974 call terminal_move_cursor(24, 1)
975 call terminal_write('Press any key to continue...')
976 call get_key_input(key_input, status)
977 end subroutine show_backup_diff
978
979 ! Register all available commands for the command palette
980 subroutine register_all_commands()
981 ! File operations
982 call register_command('Save File', 'save', 'Ctrl+S', 'File')
983 call register_command('Save All', 'save-all', 'Ctrl+Shift+S', 'File')
984 call register_command('Quit', 'quit', 'Ctrl+Q', 'File')
985 call register_command('Open File', 'open', 'Ctrl+O', 'File')
986 call register_command('Toggle File Tree', 'toggle-tree', 'F3', 'File')
987
988 ! Edit operations
989 call register_command('Copy', 'copy', 'Ctrl+C', 'Edit')
990 call register_command('Paste', 'paste', 'Ctrl+V', 'Edit')
991 call register_command('Cut', 'cut', 'Ctrl+X', 'Edit')
992 call register_command('Undo', 'undo', 'Ctrl+Z', 'Edit')
993 call register_command('Redo', 'redo', 'Ctrl+Y', 'Edit')
994
995 ! Search operations
996 call register_command('Find', 'find', 'Ctrl+F', 'Search')
997 call register_command('Replace', 'replace', 'Ctrl+H', 'Search')
998 call register_command('Find Next', 'find-next', 'Ctrl+G', 'Search')
999 call register_command('Find Previous', 'find-prev', 'Shift+Ctrl+G', 'Search')
1000
1001 ! Navigation
1002 call register_command('Go to Line', 'goto-line', 'Ctrl+G', 'Navigation')
1003 call register_command('Go to Definition', 'goto-def', 'F12', 'Navigation')
1004 call register_command('Find References', 'find-refs', 'Shift+F12', 'Navigation')
1005 call register_command('Jump Back', 'jump-back', 'Alt+,', 'Navigation')
1006 call register_command('Go to Symbol', 'goto-symbol', 'Ctrl+Shift+O', 'Navigation')
1007
1008 ! LSP features
1009 call register_command('Code Actions', 'code-actions', 'Ctrl+.', 'LSP')
1010 call register_command('Rename Symbol', 'rename', 'F2', 'LSP')
1011 call register_command('Show Diagnostics', 'diagnostics', 'Ctrl+Shift+D', 'LSP')
1012 call register_command('Show Hover Info', 'hover', 'Ctrl+K Ctrl+I', 'LSP')
1013
1014 ! View
1015 call register_command('Split Vertical', 'split-v', 'Ctrl+\\', 'View')
1016 call register_command('Split Horizontal', 'split-h', 'Ctrl+Shift+\\', 'View')
1017 call register_command('Close Pane', 'close-pane', 'Ctrl+W', 'View')
1018 call register_command('Navigate Pane Left', 'pane-left', 'Ctrl+H', 'View')
1019 call register_command('Navigate Pane Right', 'pane-right', 'Ctrl+L', 'View')
1020 call register_command('Navigate Pane Up', 'pane-up', 'Ctrl+K', 'View')
1021 call register_command('Navigate Pane Down', 'pane-down', 'Ctrl+J', 'View')
1022
1023 ! Help
1024 call register_command('Show Help', 'help', '?', 'Help')
1025 call register_command('Command Palette', 'palette', 'Ctrl+Shift+P', 'Help')
1026 end subroutine register_all_commands
1027
1028 end program facsimile