fortrangoingonforty/facsimile / bbdc1e2

Browse files

Add Rename Symbol (F2)

Authored by espadonne
SHA
bbdc1e25cea69366cdac1f515d39302567686d75
Parents
d34c3da
Tree
e4d9897

4 changed files

StatusFile+-
M Makefile 1 0
M src/commands/command_handler_module.f90 247 3
M src/lsp/lsp_server_manager_module.f90 28 1
A src/ui/rename_prompt_module.f90 36 0
Makefilemodified
@@ -103,6 +103,7 @@ SOURCES = src/version_module.f90 \
103103
           src/terminal/renderer_module.f90 \
104104
           src/ui/help_display_module.f90 \
105105
           src/ui/text_prompt_module.f90 \
106
+          src/ui/rename_prompt_module.f90 \
106107
           src/ui/search_prompt_module.f90 \
107108
           src/ui/replace_prompt_module.f90 \
108109
           src/ui/unified_search_module.f90 \
src/commands/command_handler_module.f90modified
@@ -25,7 +25,8 @@ module command_handler_module
2525
     use binary_prompt_module, only: binary_file_prompt
2626
     use lsp_server_manager_module, only: request_completion, request_hover, request_definition, &
2727
                                          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
2930
     use completion_popup_module, only: show_completion_popup, hide_completion_popup, &
3031
                                         handle_completion_response, navigate_completion_up, &
3132
                                         navigate_completion_down, get_selected_completion, &
@@ -144,11 +145,10 @@ contains
144145
         case('ctrl-q')
145146
             should_quit = .true.
146147
 
147
-        case('ctrl-b', 'ctrl-shift-b', 'f2', 'f3')
148
+        case('ctrl-b', 'ctrl-shift-b', 'f3')
148149
             ! Toggle fuss mode (file tree)
149150
             ! ctrl-b: Original binding (conflicts with tmux prefix)
150151
             ! ctrl-shift-b: Alternative (tmux may still catch this)
151
-            ! f2: Alternative function key binding
152152
             ! f3: Tmux/terminal-safe alternative (recommended)
153153
             call toggle_fuss_mode(editor)
154154
 
@@ -1257,6 +1257,61 @@ contains
12571257
                 end if
12581258
             end if
12591259
 
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
+
12601315
         case('ctrl-shift-o')
12611316
             ! Document symbols outline
12621317
             if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then
@@ -5764,4 +5819,193 @@ contains
57645819
         end if
57655820
     end subroutine handle_signature_response_wrapper
57665821
 
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
+
57676011
 end module command_handler_module
src/lsp/lsp_server_manager_module.f90modified
@@ -17,7 +17,7 @@ module lsp_server_manager_module
1717
     public :: get_language_for_file, start_lsp_for_file
1818
     public :: notify_file_opened, notify_file_changed, notify_file_saved, notify_file_closed
1919
     public :: request_completion, request_hover, request_definition, request_references, request_code_actions
20
-    public :: request_document_symbols, request_signature_help
20
+    public :: request_document_symbols, request_signature_help, request_rename
2121
     public :: set_diagnostics_handler
2222
 
2323
     ! Language server configuration
@@ -982,6 +982,33 @@ contains
982982
         call send_request(manager%servers(server_index), msg, callback)
983983
     end function request_signature_help
984984
 
985
+    ! Request rename
986
+    function request_rename(manager, server_index, filename, line, character, new_name, callback) result(request_id)
987
+        use lsp_protocol_module, only: create_rename_request
988
+        type(lsp_manager_t), intent(inout) :: manager
989
+        integer, intent(in) :: server_index
990
+        character(len=*), intent(in) :: filename
991
+        integer, intent(in) :: line, character  ! 0-based LSP positions
992
+        character(len=*), intent(in) :: new_name
993
+        procedure(response_callback), optional :: callback
994
+        integer :: request_id
995
+        type(lsp_message_t) :: msg
996
+        character(len=256) :: uri
997
+
998
+        request_id = -1
999
+        if (server_index < 1 .or. server_index > manager%num_servers) return
1000
+        if (.not. manager%servers(server_index)%initialized) return
1001
+
1002
+        ! Convert filename to URI
1003
+        uri = "file://" // trim(filename)
1004
+
1005
+        ! Create rename request
1006
+        msg = create_rename_request(uri, line, character, new_name)
1007
+        request_id = msg%id
1008
+
1009
+        call send_request(manager%servers(server_index), msg, callback)
1010
+    end function request_rename
1011
+
9851012
     ! Set the diagnostics notification handler
9861013
     subroutine set_diagnostics_handler(manager, handler)
9871014
         type(lsp_manager_t), intent(inout) :: manager
src/ui/rename_prompt_module.f90added
@@ -0,0 +1,36 @@
1
+module rename_prompt_module
2
+    use iso_fortran_env, only: int32
3
+    use text_prompt_module, only: show_text_prompt
4
+    implicit none
5
+    private
6
+
7
+    public :: show_rename_prompt
8
+
9
+contains
10
+
11
+    subroutine show_rename_prompt(screen_rows, old_name, new_name, cancelled)
12
+        integer(int32), intent(in) :: screen_rows
13
+        character(len=*), intent(in) :: old_name
14
+        character(len=:), allocatable, intent(out) :: new_name
15
+        logical, intent(out) :: cancelled
16
+
17
+        character(len=256) :: prompt_text
18
+        character(len=512) :: input_text
19
+
20
+        ! Create prompt message with old name
21
+        write(prompt_text, '(A)') "Rename '" // trim(old_name) // "' to: "
22
+
23
+        ! Show the text prompt
24
+        call show_text_prompt(trim(prompt_text), input_text, cancelled, screen_rows)
25
+
26
+        if (.not. cancelled) then
27
+            if (len_trim(input_text) > 0 .and. trim(input_text) /= trim(old_name)) then
28
+                allocate(character(len=len_trim(input_text)) :: new_name)
29
+                new_name = trim(input_text)
30
+            else
31
+                cancelled = .true.
32
+            end if
33
+        end if
34
+    end subroutine show_rename_prompt
35
+
36
+end module rename_prompt_module