From 3a1af93e9c0f983da64070d3774596844c2a26e1 Mon Sep 17 00:00:00 2001 From: pommicket Date: Tue, 30 Sep 2025 10:42:13 -0400 Subject: Initial implementation of code actions --- buffer.c | 5 ++ command.c | 4 +- ide-code-action.c | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++- ide-rename-symbol.c | 80 +------------------------------- lsp-parse.c | 50 ++++++++++++++++++++ lsp.c | 7 +++ lsp.h | 20 +++++++- main.c | 10 +++- ted-internal.h | 11 ++++- ted.c | 81 ++++++++++++++++++++++++++++++++ ted.cfg | 1 + ted.h | 8 ++++ 12 files changed, 322 insertions(+), 86 deletions(-) diff --git a/buffer.c b/buffer.c index 9b8a79d..11b239d 100644 --- a/buffer.c +++ b/buffer.c @@ -2660,6 +2660,9 @@ void buffer_delete_chars_at_pos(TextBuffer *buffer, BufferPos pos, i64 nchars_) // just in case buffer_pos_validate(buffer, &buffer->cursor_pos); buffer_pos_validate(buffer, &buffer->selection_pos); + if (buffer_pos_eq(buffer->cursor_pos, buffer->selection_pos)) { + buffer->selection = false; + } // we need to do this *after* making the change to the buffer // because of how non-incremental syncing works. @@ -3529,6 +3532,8 @@ bool buffer_handle_click(Ted *ted, TextBuffer *buffer, vec2 click, u8 times) { else autocomplete_close(ted); // close autocomplete menu if user clicks outside of it } + if (code_action_is_open(ted)) + return false; if (buffer_pixels_to_pos(buffer, click, &buffer_pos)) { // user clicked on buffer if (!menu_is_any_open(ted) || buffer->is_line_buffer) { diff --git a/command.c b/command.c index 66ae4bb..4cae9c8 100644 --- a/command.c +++ b/command.c @@ -641,6 +641,8 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *full_argumen *ted->message_shown = '\0'; } else if (autocomplete_is_open(ted)) { autocomplete_close(ted); + } else if (code_action_is_open(ted)) { + code_action_close(ted); } else if (menu_is_any_open(ted)) { menu_escape(ted); } else { @@ -742,7 +744,7 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *full_argumen buffer_print_undo_history(buffer); break; case CMD_CODE_ACTION: - code_action_start(ted); + code_action_open(ted); break; } } diff --git a/ide-code-action.c b/ide-code-action.c index 13a1b36..93b95a9 100644 --- a/ide-code-action.c +++ b/ide-code-action.c @@ -1,5 +1,10 @@ #include "ted-internal.h" +struct CodeAction { + LSPServerRequestID last_request; + LSPResponse response; +}; + static bool ranges_touch(BufferPos p1, BufferPos p2, BufferPos q1, BufferPos q2) { int cmp21 = buffer_pos_cmp(p2, q1); if (cmp21 < 0) { @@ -14,9 +19,18 @@ static bool ranges_touch(BufferPos p1, BufferPos p2, BufferPos q1, BufferPos q2) return true; } -void code_action_start(Ted *ted) { +void code_action_init(Ted *ted) { + ted->code_action = calloc(1, sizeof *ted->code_action); +} + +void code_action_open(Ted *ted) { + CodeAction *c = ted->code_action; + ted_cancel_lsp_request(ted, &c->last_request); TextBuffer *buffer = ted_active_buffer(ted); + if (!buffer) return; LSP *lsp = buffer_lsp(buffer); + if (!lsp) return; + autocomplete_close(ted); BufferPos range_start = {0}, range_end = {0}; LSPRange range = {0}; if (buffer_selection_pos(buffer, &range_start)) { @@ -43,5 +57,118 @@ void code_action_start(Ted *ted) { arr_add(code_action_req->raw_diagnostics, raw); } } - lsp_send_request(lsp, &req); + c->last_request = lsp_send_request(lsp, &req); +} + +bool code_action_is_open(Ted *ted) { + CodeAction *c = ted->code_action; + return arr_len(c->response.data.code_action.actions) != 0; +} + +void code_action_close(Ted *ted) { + CodeAction *c = ted->code_action; + lsp_response_free(&c->response); + memset(&c->response, 0, sizeof c->response); +} + +bool code_action_process_lsp_response(Ted *ted, const LSPResponse *response) { + CodeAction *c = ted->code_action; + if (response->request.id != c->last_request.id + || response->request.type != LSP_REQUEST_CODE_ACTION) { + return false; + } + if (arr_len(response->data.code_action.actions) == 0) { + // no code actions + code_action_close(ted); + ted_flash_error_cursor(ted); + return false; + } + lsp_response_free(&c->response); + c->response = *response; + return true; +} + +void code_action_quit(Ted *ted) { + CodeAction *c = ted->code_action; + code_action_close(ted); + free(c); + ted->code_action = NULL; +} + +static void code_action_perform(Ted *ted, const LSPCodeAction *action) { + CodeAction *c = ted->code_action; + const LSPResponse *response = &c->response; + LSPServerRequestID request_id = c->last_request; + LSP *lsp = ted_get_lsp_by_id(ted, request_id.lsp); + ted_perform_workspace_edit(ted, lsp, response, &action->edit); +} + +void code_action_frame(Ted *ted) { + CodeAction *c = ted->code_action; + LSPResponse *response = &c->response; + const LSPCodeAction *code_actions = response->data.code_action.actions; + if (arr_len(code_actions) == 0) + return; + TextBuffer *buffer = ted_active_buffer(ted); + if (!buffer) { + code_action_close(ted); + return; + } + const Settings *settings = ted_active_settings(ted); + vec2 cursor_pos = buffer_pos_to_pixels(buffer, buffer_cursor_pos(buffer)); + float x = cursor_pos.x, y = cursor_pos.y; + Font *font = ted->font; + float char_height = text_font_char_height(font); + float padding = settings->padding; + float border_thickness = settings->border_thickness; + float panel_width = 0, panel_height = (char_height + border_thickness) * (float)arr_len(code_actions); + arr_foreach_ptr(code_actions, const LSPCodeAction, action) { + const char *name = lsp_response_string(response, action->name); + float row_width = text_get_size_vec2(font, name).x + + char_height * 6 + padding * 2; + if (row_width > panel_width) + panel_width = row_width; + } + if (x > ted->window_width / 2) { + x -= panel_width; + } + if (y > ted->window_height / 2) { + y -= panel_height + char_height; + } else { + y += char_height; + } + Rect panel_rect = {{x,y},{panel_width,panel_height}}; + gl_geometry_rect(panel_rect, settings_color(settings, COLOR_BG)); + gl_geometry_rect_border(panel_rect, border_thickness, settings_color(settings, COLOR_AUTOCOMPLETE_BORDER)); + const LSPCodeAction *selected_action = NULL; + arr_foreach_ptr(code_actions, const LSPCodeAction, action) { + const char *name = lsp_response_string(response, action->name); + Rect entry_rect = {{x, y}, {panel_width, border_thickness + char_height}}; + if (rect_contains_point(entry_rect, ted->mouse_pos)) { + ted->cursor = ted->cursor_hand; + gl_geometry_rect(entry_rect, settings_color(settings, COLOR_AUTOCOMPLETE_HL)); + } + arr_foreach_ptr(ted->mouse_clicks[SDL_BUTTON_LEFT], const MouseClick, click) + if (rect_contains_point(entry_rect, click->pos)) + selected_action = action; + if (action != code_actions) { + Rect border = {{x, y}, {panel_width, border_thickness}}; + gl_geometry_rect(border, settings_color(settings, COLOR_AUTOCOMPLETE_BORDER)); + y += border_thickness; + }; + text_utf8(font, name, x + padding, y, settings_color(settings, COLOR_TEXT)); + y += char_height; + } + gl_geometry_draw(); + text_render(font); + if (selected_action) { + code_action_perform(ted, selected_action); + code_action_close(ted); + } else { + arr_foreach_ptr(ted->mouse_clicks[SDL_BUTTON_LEFT], const MouseClick, click) { + if (!rect_contains_point(panel_rect, click->pos)) { + code_action_close(ted); + } + } + } } diff --git a/ide-rename-symbol.c b/ide-rename-symbol.c index dbbed0f..95b4a56 100644 --- a/ide-rename-symbol.c +++ b/ide-rename-symbol.c @@ -175,85 +175,7 @@ void rename_symbol_process_lsp_response(Ted *ted, const LSPResponse *response) { // LSP crashed or something return; } - TextBuffer *const start_buffer = ted_active_buffer(ted); - - arr_foreach_ptr(data->changes, const LSPWorkspaceChange, change) { - if (change->type == LSP_CHANGE_DELETE && change->data.delete.recursive) { - ted_error(ted, "refusing to perform rename because it involves a recursive deletion\n" - "I'm too scared to go through with this"); - return; - } - } - - arr_foreach_ptr(data->changes, const LSPWorkspaceChange, change) { - switch (change->type) { - case LSP_CHANGE_EDITS: { - const LSPWorkspaceChangeEdit *change_data = &change->data.edit; - const char *path = lsp_document_path(lsp, change_data->document); - if (!ted_open_file(ted, path)) goto done; - - TextBuffer *buffer = ted_get_buffer_with_file(ted, path); - // chain all edits together so they can be undone with one ctrl+z - buffer_start_edit_chain(buffer); - - if (!buffer) { - // this should never happen since we just - // successfully opened it - assert(0); - goto done; - } - - buffer_apply_lsp_text_edits(buffer, response, change_data->edits, arr_len(change_data->edits)); - } - break; - case LSP_CHANGE_RENAME: { - const LSPWorkspaceChangeRename *rename = &change->data.rename; - const char *old = lsp_document_path(lsp, rename->old); - const char *new = lsp_document_path(lsp, rename->new); - FsType new_type = fs_path_type(new); - if (new_type == FS_DIRECTORY) { - ted_error(ted, "Aborting rename since it's asking to overwrite a directory."); - goto done; - } - - if (rename->ignore_if_exists && new_type != FS_NON_EXISTENT) { - break; - } - if (!rename->overwrite && new_type != FS_NON_EXISTENT) { - ted_error(ted, "Aborting rename since it would overwrite a file."); - goto done; - } - os_rename_overwrite(old, new); - if (ted_close_buffer_with_file(ted, old)) - ted_open_file(ted, new); - } break; - case LSP_CHANGE_DELETE: { - const LSPWorkspaceChangeDelete *delete = &change->data.delete; - const char *path = lsp_document_path(lsp, delete->document); - remove(path); - ted_close_buffer_with_file(ted, path); - } break; - case LSP_CHANGE_CREATE: { - const LSPWorkspaceChangeCreate *create = &change->data.create; - const char *path = lsp_document_path(lsp, create->document); - FILE *fp = fopen(path, create->overwrite ? "wb" : "ab"); - if (fp) fclose(fp); - ted_open_file(ted, path); - } break; - } - } - done: - - { - // end all edit chains in all buffers - // they're almost definitely all created by us - arr_foreach_ptr(ted->buffers, TextBufferPtr, pbuffer) { - buffer_end_edit_chain(*pbuffer); - } - - ted_save_all(ted); - } - ted_switch_to_buffer(ted, start_buffer); + ted_perform_workspace_edit(ted, lsp, response, data); } void rename_symbol_init(Ted *ted) { diff --git a/lsp-parse.c b/lsp-parse.c index 4046f6f..b718f87 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -1090,6 +1090,53 @@ static bool parse_formatting_response(LSP *lsp, const JSON *json, LSPResponse *r return true; } +static bool parse_command(LSP *lsp, const JSON *json, JSONObject command_in, LSPCommand *command_out) { + JSONString command_str = json_object_get_string(json, command_in, "command"); + char command[64]; + json_string_get(json, command_str, command, sizeof command); + lsp_set_error(lsp, "Unrecognized command: %s\n", command); + (void)command_out; + return false; +} + +static bool parse_code_action_response(LSP *lsp, const JSON *json, LSPResponse *response) { + JSONValue actions_val = json_get(json, "result"); + if (actions_val.type == JSON_NULL) { + // nothing there + return true; + } + if (actions_val.type != JSON_ARRAY) { + lsp_set_error(lsp, "Expected array or null for code action response; got %s", + json_type_to_str(actions_val.type)); + return false; + } + JSONArray actions = json_force_array(actions_val); + for (u32 i = 0; i < actions.len; i++) { + JSONObject action = json_array_get_object(json, actions, i); + JSONValue command_val = json_object_get(json, action, "command"); + LSPCodeAction action_out = {0}; + JSONString title_str = json_object_get_string(json, action, "title"); + action_out.name = lsp_response_add_json_string(response, json, title_str); + bool understood = true; + if (command_val.type == JSON_STRING) { + // this action is a Command + understood &= parse_command(lsp, json, action, &action_out.command); + } else { + // this action is a CodeAction + JSONValue edit = json_object_get(json, action, "edit"); + if (edit.type == JSON_OBJECT) { + understood &= parse_workspace_edit(lsp, response, json, edit.val.object, &action_out.edit); + } + if (command_val.type == JSON_OBJECT) { + understood &= parse_command(lsp, json, command_val.val.object, &action_out.command); + } + } + if (understood) + arr_add(response->data.code_action.actions, action_out); + } + return true; +} + void process_message(LSP *lsp, JSON *json) { #if 0 @@ -1173,6 +1220,9 @@ void process_message(LSP *lsp, JSON *json) { case LSP_REQUEST_DOCUMENT_LINK: add_to_messages = parse_document_link_response(lsp, json, &response); break; + case LSP_REQUEST_CODE_ACTION: + add_to_messages = parse_code_action_response(lsp, json, &response); + break; case LSP_REQUEST_INITIALIZE: if (!lsp->initialized) { // it's the response to our initialize request! diff --git a/lsp.c b/lsp.c index 34548f0..9cf96fe 100644 --- a/lsp.c +++ b/lsp.c @@ -189,6 +189,13 @@ void lsp_response_free(LSPResponse *r) { case LSP_REQUEST_FORMATTING: arr_free(r->data.formatting.edits); break; + case LSP_REQUEST_CODE_ACTION: { + LSPResponseCodeAction *c = &r->data.code_action; + arr_foreach_ptr(c->actions, LSPCodeAction, action) { + arr_free(action->edit.changes); + } + arr_free(c->actions); + } break; default: break; } diff --git a/lsp.h b/lsp.h index c891246..ebaacbb 100644 --- a/lsp.h +++ b/lsp.h @@ -575,6 +575,20 @@ typedef struct { LSPTextEdit *edits; } LSPResponseFormatting; +typedef struct { + int type; +} LSPCommand; + +typedef struct { + LSPString name; + LSPWorkspaceEdit edit; + LSPCommand command; +} LSPCodeAction; + +typedef struct { + LSPCodeAction *actions; +} LSPResponseCodeAction; + typedef struct { LSPMessageBase base; /// the request which this is a response to @@ -596,6 +610,7 @@ typedef struct { LSPResponseDocumentLink document_link; /// `LSP_REQUEST_FORMATTING` or `LSP_REQUEST_RANGE_FORMATTING` LSPResponseFormatting formatting; + LSPResponseCodeAction code_action; } data; } LSPResponse; @@ -680,6 +695,7 @@ void lsp_register_language(u64 id, const char *lsp_identifier); // if clear = true, the error will be cleared. // you can set error = NULL, error_size = 0, clear = true to just clear the error bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear); +void lsp_response_free(LSPResponse *response); void lsp_message_free(LSPMessage *message); u32 lsp_document_id(LSP *lsp, const char *path); // returned pointer lives as long as lsp. @@ -952,8 +968,8 @@ void lsp_write_quit(void); char *json_reserialize(const JSON *json, JSONValue value); /// print server-to-client communication -#define LSP_SHOW_S2C 1 +#define LSP_SHOW_S2C 0 /// print client-to-server communication -#define LSP_SHOW_C2S 1 +#define LSP_SHOW_C2S 0 #endif // LSP_INTERNAL diff --git a/main.c b/main.c index 425984b..6d419a9 100644 --- a/main.c +++ b/main.c @@ -588,6 +588,7 @@ int main(int argc, char **argv) { find_init(ted); macros_init(ted); definitions_init(ted); + code_action_init(ted); autocomplete_init(ted); format_init(ted); signature_help_init(ted); @@ -969,6 +970,7 @@ int main(int argc, char **argv) { LSP *lsp = ted->lsps[i]; LSPMessage message = {0}; while (lsp_next_message(lsp, &message)) { + bool message_moved = false; switch (message.type) { case LSP_REQUEST: { LSPRequest *r = &message.request; @@ -1006,10 +1008,15 @@ int main(int argc, char **argv) { highlights_process_lsp_response(ted, r); usages_process_lsp_response(ted, r); document_link_process_lsp_response(ted, r); + if (code_action_process_lsp_response(ted, r)) { + message_moved = true; + break; + } rename_symbol_process_lsp_response(ted, r); } break; } - lsp_message_free(&message); + if (!message_moved) + lsp_message_free(&message); } } @@ -1102,6 +1109,7 @@ int main(int argc, char **argv) { float y1 = padding; node_frame(ted, node, rect4(x1, y1, x2, y)); autocomplete_frame(ted); + code_action_frame(ted); signature_help_frame(ted); hover_frame(ted, frame_dt); definitions_frame(ted); diff --git a/ted-internal.h b/ted-internal.h index a4b5574..bf05f91 100644 --- a/ted-internal.h +++ b/ted-internal.h @@ -260,6 +260,9 @@ typedef struct Definitions Definitions; /// "highlight" information from LSP server typedef struct Highlights Highlights; +/// "code action" information from LSP server +typedef struct CodeAction CodeAction; + typedef struct Macro Macro; typedef struct LoadedFont LoadedFont; @@ -364,6 +367,7 @@ struct Ted { Usages *usages; RenameSymbol *rename_symbol; Formatting *formatting; + CodeAction *code_action; /// process ID int pid; @@ -649,7 +653,10 @@ void autocomplete_frame(Ted *ted); void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *response); // === ide-code-action.c === -void code_action_start(Ted *ted); +void code_action_init(Ted *ted); +void code_action_quit(Ted *ted); +void code_action_frame(Ted *ted); +bool code_action_process_lsp_response(Ted *ted, const LSPResponse *response); // === ide-definitions.c === void definitions_init(Ted *ted); @@ -788,5 +795,7 @@ void ted_free_fonts(Ted *ted); void ted_process_publish_diagnostics(Ted *ted, LSP *lsp, LSPRequest *request); /// check inotify fd for events void ted_check_inotify(Ted *ted); +/// perform LSP WorkspaceEdit +void ted_perform_workspace_edit(Ted *ted, LSP *lsp, const LSPResponse *response, const LSPWorkspaceEdit *edit); #endif // TED_INTERNAL_H_ diff --git a/ted.c b/ted.c index b5403c6..fb95bcc 100644 --- a/ted.c +++ b/ted.c @@ -982,6 +982,87 @@ void ted_test(Ted *ted) { printf("all good as far as i know :3\n"); } +void ted_perform_workspace_edit(Ted *ted, LSP *lsp, const LSPResponse *response, const LSPWorkspaceEdit *edit) { + TextBuffer *const start_buffer = ted_active_buffer(ted); + arr_foreach_ptr(edit->changes, const LSPWorkspaceChange, change) { + if (change->type == LSP_CHANGE_DELETE && change->data.delete.recursive) { + ted_error(ted, "refusing to perform edit because it involves a recursive deletion\n" + "I'm too scared to go through with this"); + return; + } + } + + arr_foreach_ptr(edit->changes, const LSPWorkspaceChange, change) { + switch (change->type) { + case LSP_CHANGE_EDITS: { + const LSPWorkspaceChangeEdit *change_data = &change->data.edit; + const char *path = lsp_document_path(lsp, change_data->document); + if (!ted_open_file(ted, path)) goto done; + + TextBuffer *buffer = ted_get_buffer_with_file(ted, path); + // chain all edits together so they can be undone with one ctrl+z + buffer_start_edit_chain(buffer); + + if (!buffer) { + // this should never happen since we just + // successfully opened it + assert(0); + goto done; + } + + buffer_apply_lsp_text_edits(buffer, response, change_data->edits, arr_len(change_data->edits)); + } + break; + case LSP_CHANGE_RENAME: { + const LSPWorkspaceChangeRename *rename = &change->data.rename; + const char *old = lsp_document_path(lsp, rename->old); + const char *new = lsp_document_path(lsp, rename->new); + FsType new_type = fs_path_type(new); + if (new_type == FS_DIRECTORY) { + ted_error(ted, "Aborting rename since it's asking to overwrite a directory."); + goto done; + } + + if (rename->ignore_if_exists && new_type != FS_NON_EXISTENT) { + break; + } + if (!rename->overwrite && new_type != FS_NON_EXISTENT) { + ted_error(ted, "Aborting rename since it would overwrite a file."); + goto done; + } + os_rename_overwrite(old, new); + if (ted_close_buffer_with_file(ted, old)) + ted_open_file(ted, new); + } break; + case LSP_CHANGE_DELETE: { + const LSPWorkspaceChangeDelete *delete = &change->data.delete; + const char *path = lsp_document_path(lsp, delete->document); + remove(path); + ted_close_buffer_with_file(ted, path); + } break; + case LSP_CHANGE_CREATE: { + const LSPWorkspaceChangeCreate *create = &change->data.create; + const char *path = lsp_document_path(lsp, create->document); + FILE *fp = fopen(path, create->overwrite ? "wb" : "ab"); + if (fp) fclose(fp); + ted_open_file(ted, path); + } break; + } + } + done: + + { + // end all edit chains in all buffers + // they're almost definitely all created by us + arr_foreach_ptr(ted->buffers, TextBufferPtr, pbuffer) { + buffer_end_edit_chain(*pbuffer); + } + + ted_save_all(ted); + } + ted_switch_to_buffer(ted, start_buffer); +} + #if HAS_INOTIFY void ted_check_inotify(Ted *ted) { // see buffer_externally_changed definition for why this exists diff --git a/ted.cfg b/ted.cfg index 44c4b6a..5565c82 100644 --- a/ted.cfg +++ b/ted.cfg @@ -346,6 +346,7 @@ Ctrl+Space = :autocomplete Ctrl+Shift+Space = :autocomplete-back Ctrl+u = :find-usages Ctrl+r = :rename-symbol +Alt+Space = :code-action Ctrl+z = :undo Ctrl+Shift+z = :redo diff --git a/ted.h b/ted.h index 68814d9..8906dda 100644 --- a/ted.h +++ b/ted.h @@ -928,6 +928,14 @@ void autocomplete_prev(Ted *ted); /// close completion menu void autocomplete_close(Ted *ted); +// === ide-code-action.c == +/// Show suggested code actions +void code_action_open(Ted *ted); +/// Hide suggested code actions +void code_action_close(Ted *ted); +/// Are code actions being shown? +bool code_action_is_open(Ted *ted); + // === ide-definitions.c === /// cancel the last go-to-definition / find symbols request. void definition_cancel_lookup(Ted *ted); -- cgit v1.2.3