summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2025-09-30 10:42:13 -0400
committerpommicket <pommicket@gmail.com>2025-09-30 10:42:13 -0400
commit3a1af93e9c0f983da64070d3774596844c2a26e1 (patch)
tree65878feb922fcec4ae9f3cae4bbc1a5ee99d6cb9
parent84da626a18ccc779aef4a178ee0097a93c959520 (diff)
Initial implementation of code actions
-rw-r--r--buffer.c5
-rw-r--r--command.c4
-rw-r--r--ide-code-action.c131
-rw-r--r--ide-rename-symbol.c80
-rw-r--r--lsp-parse.c50
-rw-r--r--lsp.c7
-rw-r--r--lsp.h20
-rw-r--r--main.c10
-rw-r--r--ted-internal.h11
-rw-r--r--ted.c81
-rw-r--r--ted.cfg1
-rw-r--r--ted.h8
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
@@ -576,6 +576,20 @@ typedef struct {
} 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
LSPRequest request;
@@ -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);