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