@@ -25,7 +25,8 @@ module command_handler_module |
| 25 | 25 | use binary_prompt_module, only: binary_file_prompt |
| 26 | 26 | use lsp_server_manager_module, only: request_completion, request_hover, request_definition, & |
| 27 | 27 | request_references, request_code_actions, request_document_symbols, & |
| 28 | | - request_signature_help |
| 28 | + request_signature_help, request_rename |
| 29 | + use rename_prompt_module, only: show_rename_prompt |
| 29 | 30 | use completion_popup_module, only: show_completion_popup, hide_completion_popup, & |
| 30 | 31 | handle_completion_response, navigate_completion_up, & |
| 31 | 32 | navigate_completion_down, get_selected_completion, & |
@@ -144,11 +145,10 @@ contains |
| 144 | 145 | case('ctrl-q') |
| 145 | 146 | should_quit = .true. |
| 146 | 147 | |
| 147 | | - case('ctrl-b', 'ctrl-shift-b', 'f2', 'f3') |
| 148 | + case('ctrl-b', 'ctrl-shift-b', 'f3') |
| 148 | 149 | ! Toggle fuss mode (file tree) |
| 149 | 150 | ! ctrl-b: Original binding (conflicts with tmux prefix) |
| 150 | 151 | ! ctrl-shift-b: Alternative (tmux may still catch this) |
| 151 | | - ! f2: Alternative function key binding |
| 152 | 152 | ! f3: Tmux/terminal-safe alternative (recommended) |
| 153 | 153 | call toggle_fuss_mode(editor) |
| 154 | 154 | |
@@ -1257,6 +1257,61 @@ contains |
| 1257 | 1257 | end if |
| 1258 | 1258 | end if |
| 1259 | 1259 | |
| 1260 | + case('f2') |
| 1261 | + ! Rename symbol |
| 1262 | + if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then |
| 1263 | + if (editor%tabs(editor%active_tab_index)%lsp_server_index > 0) then |
| 1264 | + ! Get word under cursor as old name |
| 1265 | + block |
| 1266 | + character(len=:), allocatable :: line, old_name, new_name |
| 1267 | + integer :: word_start, word_end, lsp_line, lsp_char, request_id |
| 1268 | + logical :: cancelled |
| 1269 | + |
| 1270 | + line = buffer_get_line(buffer, editor%cursors(editor%active_cursor)%line) |
| 1271 | + call find_word_boundaries(line, editor%cursors(editor%active_cursor)%column, & |
| 1272 | + word_start, word_end) |
| 1273 | + |
| 1274 | + if (word_start > 0 .and. word_end >= word_start) then |
| 1275 | + old_name = line(word_start:word_end) |
| 1276 | + |
| 1277 | + ! Show rename prompt |
| 1278 | + call show_rename_prompt(editor%screen_rows, old_name, new_name, cancelled) |
| 1279 | + |
| 1280 | + if (.not. cancelled .and. allocated(new_name)) then |
| 1281 | + ! Send rename request |
| 1282 | + lsp_line = editor%cursors(editor%active_cursor)%line - 1 |
| 1283 | + lsp_char = editor%cursors(editor%active_cursor)%column - 1 |
| 1284 | + |
| 1285 | + ! Save editor state for callback |
| 1286 | + if (.not. allocated(saved_editor_for_callback)) then |
| 1287 | + allocate(saved_editor_for_callback) |
| 1288 | + end if |
| 1289 | + saved_editor_for_callback = editor |
| 1290 | + |
| 1291 | + request_id = request_rename(editor%lsp_manager, & |
| 1292 | + editor%tabs(editor%active_tab_index)%lsp_server_index, & |
| 1293 | + editor%tabs(editor%active_tab_index)%filename, & |
| 1294 | + lsp_line, lsp_char, new_name, handle_rename_response_wrapper) |
| 1295 | + |
| 1296 | + if (request_id > 0) then |
| 1297 | + call terminal_move_cursor(editor%screen_rows, 1) |
| 1298 | + call terminal_write('Renaming symbol... ') |
| 1299 | + end if |
| 1300 | + |
| 1301 | + deallocate(new_name) |
| 1302 | + end if |
| 1303 | + |
| 1304 | + if (allocated(old_name)) deallocate(old_name) |
| 1305 | + else |
| 1306 | + call terminal_move_cursor(editor%screen_rows, 1) |
| 1307 | + call terminal_write('No symbol under cursor ') |
| 1308 | + end if |
| 1309 | + |
| 1310 | + if (allocated(line)) deallocate(line) |
| 1311 | + end block |
| 1312 | + end if |
| 1313 | + end if |
| 1314 | + |
| 1260 | 1315 | case('ctrl-shift-o') |
| 1261 | 1316 | ! Document symbols outline |
| 1262 | 1317 | if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then |
@@ -5764,4 +5819,193 @@ contains |
| 5764 | 5819 | end if |
| 5765 | 5820 | end subroutine handle_signature_response_wrapper |
| 5766 | 5821 | |
| 5822 | + ! Wrapper callback that matches the LSP callback signature for rename |
| 5823 | + subroutine handle_rename_response_wrapper(request_id, response) |
| 5824 | + use lsp_protocol_module, only: lsp_message_t |
| 5825 | + use json_module, only: json_value_t, json_stringify |
| 5826 | + integer, intent(in) :: request_id |
| 5827 | + type(lsp_message_t), intent(in) :: response |
| 5828 | + |
| 5829 | + character(len=:), allocatable :: result_str |
| 5830 | + integer :: changes_applied |
| 5831 | + |
| 5832 | + if (.not. allocated(saved_editor_for_callback)) return |
| 5833 | + |
| 5834 | + ! Convert result to string for apply_workspace_edit |
| 5835 | + result_str = json_stringify(response%result) |
| 5836 | + if (.not. allocated(result_str) .or. result_str == 'null' .or. len_trim(result_str) == 0) then |
| 5837 | + call terminal_move_cursor(saved_editor_for_callback%screen_rows, 1) |
| 5838 | + call terminal_write('Rename failed or not supported ') |
| 5839 | + if (allocated(result_str)) deallocate(result_str) |
| 5840 | + return |
| 5841 | + end if |
| 5842 | + |
| 5843 | + ! Apply workspace edit |
| 5844 | + call apply_workspace_edit(saved_editor_for_callback, result_str, changes_applied) |
| 5845 | + |
| 5846 | + call terminal_move_cursor(saved_editor_for_callback%screen_rows, 1) |
| 5847 | + if (changes_applied > 0) then |
| 5848 | + block |
| 5849 | + character(len=64) :: msg |
| 5850 | + write(msg, '(A,I0,A)') 'Renamed symbol (', changes_applied, ' changes applied)' |
| 5851 | + call terminal_write(trim(msg) // ' ') |
| 5852 | + end block |
| 5853 | + else |
| 5854 | + call terminal_write('No changes applied ') |
| 5855 | + end if |
| 5856 | + |
| 5857 | + if (allocated(result_str)) deallocate(result_str) |
| 5858 | + end subroutine handle_rename_response_wrapper |
| 5859 | + |
| 5860 | + ! Apply a workspace edit from LSP |
| 5861 | + subroutine apply_workspace_edit(editor, edit_json, changes_applied) |
| 5862 | + use json_module, only: json_parse, json_value_t, json_get_array, json_array_size, & |
| 5863 | + json_get_array_element, json_get_object, json_get_string, & |
| 5864 | + json_get_number, json_has_key |
| 5865 | + type(editor_state_t), intent(inout) :: editor |
| 5866 | + character(len=*), intent(in) :: edit_json |
| 5867 | + integer, intent(out) :: changes_applied |
| 5868 | + |
| 5869 | + type(json_value_t) :: edit_obj, doc_changes_arr, file_change_obj |
| 5870 | + type(json_value_t) :: text_doc_obj, edits_arr |
| 5871 | + character(len=:), allocatable :: uri |
| 5872 | + integer :: num_files, i |
| 5873 | + |
| 5874 | + changes_applied = 0 |
| 5875 | + |
| 5876 | + ! Parse the edit JSON |
| 5877 | + edit_obj = json_parse(edit_json) |
| 5878 | + |
| 5879 | + ! Try to get documentChanges first (newer format) |
| 5880 | + if (json_has_key(edit_obj, 'documentChanges')) then |
| 5881 | + doc_changes_arr = json_get_array(edit_obj, 'documentChanges') |
| 5882 | + num_files = json_array_size(doc_changes_arr) |
| 5883 | + |
| 5884 | + do i = 0, num_files - 1 ! 0-based index |
| 5885 | + file_change_obj = json_get_array_element(doc_changes_arr, i) |
| 5886 | + |
| 5887 | + ! Get text document URI |
| 5888 | + if (json_has_key(file_change_obj, 'textDocument')) then |
| 5889 | + text_doc_obj = json_get_object(file_change_obj, 'textDocument') |
| 5890 | + uri = json_get_string(text_doc_obj, 'uri') |
| 5891 | + end if |
| 5892 | + |
| 5893 | + ! Get edits array |
| 5894 | + if (json_has_key(file_change_obj, 'edits') .and. allocated(uri)) then |
| 5895 | + edits_arr = json_get_array(file_change_obj, 'edits') |
| 5896 | + call apply_file_edits_obj(editor, uri, edits_arr, changes_applied) |
| 5897 | + deallocate(uri) |
| 5898 | + end if |
| 5899 | + end do |
| 5900 | + return |
| 5901 | + end if |
| 5902 | + |
| 5903 | + ! Fall back to changes format (older format - map of URI to edits) |
| 5904 | + if (json_has_key(edit_obj, 'changes')) then |
| 5905 | + call terminal_move_cursor(editor%screen_rows, 1) |
| 5906 | + call terminal_write('Workspace edit (changes format) not fully supported') |
| 5907 | + return |
| 5908 | + end if |
| 5909 | + |
| 5910 | + end subroutine apply_workspace_edit |
| 5911 | + |
| 5912 | + ! Apply edits to a specific file (using json_value_t) |
| 5913 | + subroutine apply_file_edits_obj(editor, uri, edits_arr, changes_applied) |
| 5914 | + use json_module, only: json_value_t, json_array_size, json_get_array_element, & |
| 5915 | + json_get_object, json_get_string, json_get_number, json_has_key |
| 5916 | + type(editor_state_t), intent(inout) :: editor |
| 5917 | + character(len=*), intent(in) :: uri |
| 5918 | + type(json_value_t), intent(in) :: edits_arr |
| 5919 | + integer, intent(inout) :: changes_applied |
| 5920 | + |
| 5921 | + type(json_value_t) :: edit_obj, range_obj, start_obj, end_obj |
| 5922 | + character(len=:), allocatable :: filename, new_text |
| 5923 | + integer :: num_edits, i, j, tab_idx |
| 5924 | + integer :: start_line, start_char, end_line, end_char |
| 5925 | + |
| 5926 | + ! Convert URI to filename |
| 5927 | + if (len(uri) >= 7 .and. uri(1:7) == 'file://') then |
| 5928 | + filename = uri(8:) |
| 5929 | + else |
| 5930 | + filename = uri |
| 5931 | + end if |
| 5932 | + |
| 5933 | + ! Find the tab with this file |
| 5934 | + tab_idx = 0 |
| 5935 | + do j = 1, size(editor%tabs) |
| 5936 | + if (allocated(editor%tabs(j)%filename)) then |
| 5937 | + if (trim(editor%tabs(j)%filename) == trim(filename)) then |
| 5938 | + tab_idx = j |
| 5939 | + exit |
| 5940 | + end if |
| 5941 | + end if |
| 5942 | + end do |
| 5943 | + |
| 5944 | + if (tab_idx == 0) then |
| 5945 | + ! File not open - skip for now |
| 5946 | + if (allocated(filename)) deallocate(filename) |
| 5947 | + return |
| 5948 | + end if |
| 5949 | + |
| 5950 | + ! Apply edits in reverse order (to preserve line numbers) |
| 5951 | + num_edits = json_array_size(edits_arr) |
| 5952 | + |
| 5953 | + do i = num_edits - 1, 0, -1 ! 0-based index, reverse order |
| 5954 | + edit_obj = json_get_array_element(edits_arr, i) |
| 5955 | + |
| 5956 | + ! Get range |
| 5957 | + if (.not. json_has_key(edit_obj, 'range')) cycle |
| 5958 | + range_obj = json_get_object(edit_obj, 'range') |
| 5959 | + |
| 5960 | + if (json_has_key(range_obj, 'start') .and. json_has_key(range_obj, 'end')) then |
| 5961 | + start_obj = json_get_object(range_obj, 'start') |
| 5962 | + end_obj = json_get_object(range_obj, 'end') |
| 5963 | + |
| 5964 | + start_line = int(json_get_number(start_obj, 'line', 0.0d0)) + 1 |
| 5965 | + start_char = int(json_get_number(start_obj, 'character', 0.0d0)) + 1 |
| 5966 | + end_line = int(json_get_number(end_obj, 'line', 0.0d0)) + 1 |
| 5967 | + end_char = int(json_get_number(end_obj, 'character', 0.0d0)) + 1 |
| 5968 | + |
| 5969 | + ! Get new text |
| 5970 | + new_text = json_get_string(edit_obj, 'newText') |
| 5971 | + |
| 5972 | + if (allocated(new_text)) then |
| 5973 | + ! Apply the edit to the buffer |
| 5974 | + call apply_single_edit(editor%tabs(tab_idx)%buffer, & |
| 5975 | + start_line, start_char, end_line, end_char, new_text) |
| 5976 | + changes_applied = changes_applied + 1 |
| 5977 | + deallocate(new_text) |
| 5978 | + end if |
| 5979 | + end if |
| 5980 | + end do |
| 5981 | + |
| 5982 | + if (allocated(filename)) deallocate(filename) |
| 5983 | + end subroutine apply_file_edits_obj |
| 5984 | + |
| 5985 | + ! Apply a single text edit to a buffer |
| 5986 | + subroutine apply_single_edit(buffer, start_line, start_char, end_line, end_char, new_text) |
| 5987 | + type(buffer_t), intent(inout) :: buffer |
| 5988 | + integer, intent(in) :: start_line, start_char, end_line, end_char |
| 5989 | + character(len=*), intent(in) :: new_text |
| 5990 | + |
| 5991 | + integer :: start_pos, end_pos, delete_count |
| 5992 | + |
| 5993 | + ! Calculate buffer positions |
| 5994 | + start_pos = get_buffer_position(buffer, start_line, start_char) |
| 5995 | + end_pos = get_buffer_position(buffer, end_line, end_char) |
| 5996 | + |
| 5997 | + if (start_pos <= 0 .or. end_pos <= 0) return |
| 5998 | + |
| 5999 | + ! Delete the old text |
| 6000 | + delete_count = end_pos - start_pos |
| 6001 | + if (delete_count > 0) then |
| 6002 | + call buffer_delete(buffer, start_pos, delete_count) |
| 6003 | + end if |
| 6004 | + |
| 6005 | + ! Insert the new text |
| 6006 | + if (len(new_text) > 0) then |
| 6007 | + call buffer_insert(buffer, start_pos, new_text) |
| 6008 | + end if |
| 6009 | + end subroutine apply_single_edit |
| 6010 | + |
| 5767 | 6011 | end module command_handler_module |