diff options
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | buffer.c | 20 | ||||
-rw-r--r-- | colors.c | 1 | ||||
-rw-r--r-- | colors.h | 1 | ||||
-rw-r--r-- | command.c | 17 | ||||
-rw-r--r-- | command.h | 3 | ||||
-rw-r--r-- | config.c | 1 | ||||
-rw-r--r-- | ide-code-action.c | 260 | ||||
-rw-r--r-- | ide-document-link.c | 13 | ||||
-rw-r--r-- | ide-rename-symbol.c | 80 | ||||
-rw-r--r-- | lib/stb_truetype.h | 14 | ||||
-rw-r--r-- | lsp-json.c | 2 | ||||
-rw-r--r-- | lsp-parse.c | 87 | ||||
-rw-r--r-- | lsp-write.c | 99 | ||||
-rw-r--r-- | lsp.c | 42 | ||||
-rw-r--r-- | lsp.h | 45 | ||||
-rw-r--r-- | main.c | 13 | ||||
-rwxr-xr-x | pre-commit.sh | 2 | ||||
-rw-r--r-- | syntax.c | 2 | ||||
-rw-r--r-- | ted-internal.h | 30 | ||||
-rw-r--r-- | ted.c | 90 | ||||
-rw-r--r-- | ted.cfg | 9 | ||||
-rw-r--r-- | ted.h | 16 | ||||
-rw-r--r-- | ui.c | 2 | ||||
-rw-r--r-- | windows_installer/ted/ted.vdproj | 6 |
26 files changed, 729 insertions, 131 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 99c539e..1896751 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.5...3.25) project(ted) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(SOURCES buffer.c build.c colors.c command.c config.c find.c gl.c ide-autocomplete.c - ide-document-link.c ide-definitions.c ide-format.c ide-highlights.c ide-hover.c + ide-document-link.c ide-definitions.c ide-format.c ide-highlights.c ide-hover.c ide-code-action.c ide-signature-help.c ide-usages.c ide-rename-symbol.c lsp.c lsp-json.c lsp-parse.c lsp-write.c main.c menu.c node.c os.c session.c stb_truetype.c syntax.c tags.c ted.c text.c ui.c util.c macro.c) @@ -237,7 +237,7 @@ The Go team's `go-pls` is enabled by default. You can download it ### Java Eclipse's `jdtls` is enabled by default. -You can download it [here](download.eclipse.org/jdtls/milestones/?d). +You can download it [here](https://download.eclipse.org/jdtls/milestones/?d). ### JavaScript/TypeScript @@ -368,6 +368,7 @@ Then run `make.bat release`. <tr><td>2.8.2</td> <td>Fix syntax highlighting bug</td> <td>2025 Jun 27</td></tr> <tr><td>2.8.3</td> <td>Fix annoying auto-indent behaviour</td> <td>2025 Sep 1</td></tr> <tr><td>2.8.4</td> <td>Keep cursor pos on reload, other small improvements</td> <td>2025 Sep 28</td></tr> +<tr><td>2.9.0</td> <td>LSP code actions, bug fixes</td> <td>2025 Sep 30</td></tr> </table> ## License @@ -39,14 +39,6 @@ struct BufferEdit { double time; // time at start of edit (i.e. the time just before the edit), in seconds since epoch }; -typedef struct { - MessageType severity; - BufferPos pos; - char *message; - // may be NULL - char *url; -} Diagnostic; - struct TextBuffer { /// NULL if this buffer is untitled or doesn't correspond to a file (e.g. line buffers) char *path; @@ -1004,6 +996,7 @@ static void buffer_line_free(Line *line) { static void diagnostic_free(Diagnostic *diagnostic) { free(diagnostic->message); free(diagnostic->url); + free(diagnostic->raw); memset(diagnostic, 0, sizeof *diagnostic); } @@ -2667,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. @@ -3536,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) { @@ -4245,7 +4243,9 @@ void buffer_publish_diagnostics(TextBuffer *buffer, const LSPRequest *request, L arr_foreach_ptr(diagnostics, const LSPDiagnostic, diagnostic) { Diagnostic *d = arr_addp(buffer->diagnostics); d->pos = buffer_pos_from_lsp(buffer, diagnostic->range.start); + d->end = buffer_pos_from_lsp(buffer, diagnostic->range.end); d->severity = diagnostic_severity(diagnostic); + d->raw = str_dup(lsp_request_string(request, diagnostic->raw)); char message[280]; const char *code = lsp_request_string(request, diagnostic->code); if (*code) { @@ -4287,3 +4287,7 @@ void buffer_set_inotify_modified(TextBuffer *buffer) { buffer->inotify_modified = true; } #endif + +const Diagnostic *buffer_diagnostics(TextBuffer *buffer) { + return buffer->diagnostics; +} @@ -10,7 +10,6 @@ typedef struct { static ColorName color_names[] = { {COLOR_UNKNOWN, "unknown"}, {COLOR_TEXT, "text"}, - {COLOR_TEXT_SECONDARY, "text-secondary"}, {COLOR_BG, "bg"}, {COLOR_CURSOR, "cursor"}, {COLOR_CURSOR_ERROR, "cursor-error"}, @@ -12,7 +12,6 @@ typedef enum { /// main text color COLOR_TEXT, - COLOR_TEXT_SECONDARY, COLOR_BG, COLOR_CURSOR, COLOR_CURSOR_ERROR, @@ -111,6 +111,8 @@ static CommandName command_names[] = { {"indent-with-tabs", CMD_INDENT_WITH_TABS}, {"set-tab-width", CMD_SET_TAB_WIDTH}, {"debug-print-undo-history", CMD_DEBUG_PRINT_UNDO_HISTORY}, + {"code-action", CMD_CODE_ACTION}, + {"code-action-prev", CMD_CODE_ACTION_PREV}, }; static_assert_if_possible(arr_count(command_names) == CMD_COUNT) @@ -344,6 +346,8 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *full_argumen buffer_select_all(buffer); } else if (autocomplete_is_open(ted) || autocomplete_has_phantom(ted)) { autocomplete_select_completion(ted); + } else if (code_action_is_open(ted)) { + code_action_select(ted); } else if (buffer) { if (buffer_has_selection(buffer)) buffer_indent_selection(buffer); @@ -377,6 +381,8 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *full_argumen } else if (buffer) { buffer_newline(buffer); } + } else if (code_action_is_open(ted)) { + code_action_select(ted); } else if (buffer) { buffer_newline(buffer); } @@ -640,6 +646,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 { @@ -740,5 +748,14 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *full_argumen case CMD_DEBUG_PRINT_UNDO_HISTORY: buffer_print_undo_history(buffer); break; + case CMD_CODE_ACTION: + if (code_action_is_open(ted)) + code_action_next(ted); + else + code_action_open(ted); + break; + case CMD_CODE_ACTION_PREV: + code_action_prev(ted); + break; } } @@ -107,6 +107,9 @@ typedef enum { CMD_AUTOCOMPLETE_BACK, CMD_FIND_USAGES, CMD_RENAME_SYMBOL, + CMD_CODE_ACTION, + CMD_CODE_ACTION_PREV, + /// "go to definition of..." menu CMD_GOTO_DEFINITION, CMD_GOTO_DEFINITION_AT_CURSOR, @@ -1538,6 +1538,7 @@ void config_read(Ted *ted, const char *path, ConfigFormat format) { break; case CONFIG_TED_CFG: config_read_ted_cfg(ted, source_rc, &include_stack); + arr_free(include_stack); // force recompute default settings strcpy(ted->default_settings_cwd, "//"); break; diff --git a/ide-code-action.c b/ide-code-action.c new file mode 100644 index 0000000..c855843 --- /dev/null +++ b/ide-code-action.c @@ -0,0 +1,260 @@ +#include "ted-internal.h" + +typedef struct { + const char *name; + const LSPCodeAction *lsp; + int relevancy; +} Action; + +struct CodeAction { + LSPServerRequestID last_request; + // which buffer code action is open for + LSPDocumentID which_buffer; + // cursor position when code action was opened + BufferPos cursor_pos; + LSPResponse response; + Action *actions; + u32 cursor; +}; + +static bool ranges_touch(BufferPos p1, BufferPos p2, BufferPos q1, BufferPos q2) { + int cmp21 = buffer_pos_cmp(p2, q1); + if (cmp21 < 0) { + // p is entirely before q + return false; + } + int cmp12 = buffer_pos_cmp(p1, q2); + if (cmp12 > 0) { + // p is entirely after q + return false; + } + return true; +} + +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); + c->cursor = 0; + c->which_buffer = buffer_lsp_document_id(buffer); + c->cursor_pos = buffer_cursor_pos(buffer); + BufferPos range_start = {0}, range_end = {0}; + LSPRange range = {0}; + if (buffer_selection_pos(buffer, &range_start)) { + range = buffer_selection_as_lsp_range(buffer); + range_end = buffer_cursor_pos(buffer); + if (buffer_pos_cmp(range_start, range_end) > 0) { + BufferPos tmp = range_start; + range_start = range_end; + range_end = tmp; + } + } else { + range_start = range_end = buffer_cursor_pos(buffer); + range.start = range.end = buffer_cursor_pos_as_lsp_position(buffer); + } + LSPRequest req = { + .type = LSP_REQUEST_CODE_ACTION, + }; + LSPRequestCodeAction *code_action_req = &req.data.code_action; + code_action_req->document = buffer_lsp_document_id(buffer); + code_action_req->range = range; + arr_foreach_ptr(buffer_diagnostics(buffer), const Diagnostic, d) { + if (ranges_touch(range_start, range_end, d->pos, d->end)) { + LSPString raw = lsp_request_add_string(&req, d->raw); + arr_add(code_action_req->raw_diagnostics, raw); + } + } + c->last_request = lsp_send_request(lsp, &req); +} + +void code_action_next(Ted *ted) { + CodeAction *c = ted->code_action; + u32 actions_count = arr_len(c->actions); + if (actions_count) + c->cursor = (c->cursor + 1) % actions_count; +} + +void code_action_prev(Ted *ted) { + CodeAction *c = ted->code_action; + u32 actions_count = arr_len(c->actions); + if (actions_count) + c->cursor = c->cursor ? c->cursor - 1 : actions_count - 1; +} + +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); + arr_free(c->actions); + memset(&c->response, 0, sizeof c->response); +} + +static int action_qsort_cmp_relevancy(const void *av, const void *bv) { + const Action *a = av, *b = bv; + return b->relevancy - a->relevancy; +} + +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); // free old response + c->response = *response; + arr_free(c->actions); + // we want to figure out which actions should be "preferred" (ordered first) + // currently we: + // first, prefer actions with the LSP isPreferred property set to true. + // then, prefer 'quickfix' to other kinds of actions. + // then, prefer whichever action comes first. + arr_foreach_ptr(response->data.code_action.actions, const LSPCodeAction, action) { + Action *action_out = arr_addp(c->actions); + action_out->lsp = action; + action_out->name = lsp_response_string(response, action->name); + int score = 0; + if (action->is_preferred) + score += 10; + if (action->kind == LSP_CODE_ACTION_QUICKFIX) + score += 1; + action_out->relevancy = score; + } + arr_qsort(c->actions, action_qsort_cmp_relevancy); + 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); + switch (action->command.kind) { + case LSP_COMMAND_NONE: break; + case LSP_COMMAND_WORKSPACE_EDIT: + ted_perform_workspace_edit(ted, lsp, response, &action->command.data.edit); + break; + } + code_action_close(ted); +} + +void code_action_select(Ted *ted) { + CodeAction *c = ted->code_action; + if (c->cursor < arr_len(c->actions)) + code_action_perform(ted, c->actions[c->cursor].lsp); +} + +void code_action_frame(Ted *ted) { + CodeAction *c = ted->code_action; + if (arr_len(c->actions) == 0) + return; + TextBuffer *buffer = ted_active_buffer(ted); + if (!buffer) { + code_action_close(ted); + return; + } + LSP *lsp = buffer_lsp(buffer); + if (!lsp || lsp_get_id(lsp) != c->last_request.lsp) { + // LSP or active buffer changed + code_action_close(ted); + return; + } + if (buffer_lsp_document_id(buffer) != c->which_buffer) { + // buffer changed + code_action_close(ted); + return; + } + if (!buffer_pos_eq(buffer_cursor_pos(buffer), c->cursor_pos)) { + // cursor moved + code_action_close(ted); + return; + } + const Settings *settings = ted_active_settings(ted); + Font *font = ted->font; + float char_height = text_font_char_height(font); + float padding = settings->padding; + float border_thickness = settings->border_thickness; + + // display code action panel + vec2 cursor_pos = buffer_pos_to_pixels(buffer, buffer_cursor_pos(buffer)); + float x = cursor_pos.x, y = cursor_pos.y; + float panel_width = 0, panel_height = (char_height + border_thickness) * (float)arr_len(c->actions); + arr_foreach_ptr(c->actions, const Action, action) { + float row_width = text_get_size_vec2(font, action->name).x + + char_height * 6 + padding * 2; + if (row_width > panel_width) + panel_width = row_width; + } + // ensure panel doesn't go offscreen + if (x > ted->window_width - panel_width) + x = ted->window_width - panel_width; + if (y > ted->window_height / 2) { + // open panel upwards + y -= panel_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 Action *selected_action = NULL; + arr_foreach_ptr(c->actions, const Action, action) { + Rect entry_rect = {{x, y}, {panel_width, char_height}}; + if (rect_contains_point(entry_rect, ted->mouse_pos) + || c->cursor == (u32)(action - c->actions)) { + // hovering/cursoring over this entry + 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; // clicked on this entry + if (action != c->actions) { + // border between entries + Rect border = {{x, y}, {panel_width, border_thickness}}; + gl_geometry_rect(border, settings_color(settings, COLOR_AUTOCOMPLETE_BORDER)); + y += border_thickness; + }; + text_utf8(font, action->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->lsp); + } else { + arr_foreach_ptr(ted->mouse_clicks[SDL_BUTTON_LEFT], const MouseClick, click) { + if (!rect_contains_point(panel_rect, click->pos)) { + // clicked outside of code action panel; close it. + code_action_close(ted); + } + } + } +} diff --git a/ide-document-link.c b/ide-document-link.c index 754cecd..7448c83 100644 --- a/ide-document-link.c +++ b/ide-document-link.c @@ -81,7 +81,6 @@ void document_link_frame(Ted *ted) { document_link_clear(ted); return; } - TextBuffer *buffer = ted->active_buffer; if (!buffer) return; @@ -89,20 +88,24 @@ void document_link_frame(Ted *ted) { LSP *lsp = buffer_lsp(buffer); if (!lsp) return; - - if (!dl->last_request.id) { + LSPDocumentID document = buffer_lsp_document_id(buffer); + if (document != dl->requested_document // don't spam requests + && !dl->last_request.id) { // send the request LSPRequest request = {.type = LSP_REQUEST_DOCUMENT_LINK}; LSPRequestDocumentLink *lnk = &request.data.document_link; - lnk->document = buffer_lsp_document_id(buffer); + lnk->document = document; dl->last_request = lsp_send_request(lsp, &request); dl->requested_document = lnk->document; } - + arr_foreach_ptr(dl->links, DocumentLink, l) { Rect r = document_link_get_rect(ted, l); if (ted_mouse_in_rect(ted, r)) { ted->cursor = ted->cursor_hand; + r.pos.y += r.size.y - 1; + r.size.y = 1; + gl_geometry_rect(r, settings_color(settings, COLOR_TEXT)); } } } 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/lib/stb_truetype.h b/lib/stb_truetype.h index bbf2284..90a5c2e 100644 --- a/lib/stb_truetype.h +++ b/lib/stb_truetype.h @@ -54,7 +54,7 @@ // Hou Qiming Derek Vinyard // Rob Loach Cort Stratton // Kenney Phillis Jr. Brian Costabile -// Ken Voskuil (kaesve) +// Ken Voskuil (kaesve) Yakov Galka // // VERSION HISTORY // @@ -4604,6 +4604,8 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc scale_y = -scale_y; { + // distance from singular values (in the same units as the pixel grid) + const float eps = 1./1024, eps2 = eps*eps; int x,y,i,j; float *precompute; stbtt_vertex *verts; @@ -4616,15 +4618,15 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; float x1 = verts[j].x*scale_x, y1 = verts[j].y*scale_y; float dist = (float) STBTT_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); - precompute[i] = (dist == 0) ? 0.0f : 1.0f / dist; + precompute[i] = (dist < eps) ? 0.0f : 1.0f / dist; } else if (verts[i].type == STBTT_vcurve) { float x2 = verts[j].x *scale_x, y2 = verts[j].y *scale_y; float x1 = verts[i].cx*scale_x, y1 = verts[i].cy*scale_y; float x0 = verts[i].x *scale_x, y0 = verts[i].y *scale_y; float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; float len2 = bx*bx + by*by; - if (len2 != 0.0f) - precompute[i] = 1.0f / (bx*bx + by*by); + if (len2 >= eps2) + precompute[i] = 1.0f / len2; else precompute[i] = 0.0f; } else @@ -4689,8 +4691,8 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc float a = 3*(ax*bx + ay*by); float b = 2*(ax*ax + ay*ay) + (mx*bx+my*by); float c = mx*ax+my*ay; - if (a == 0.0) { // if a is 0, it's linear - if (b != 0.0) { + if (STBTT_fabs(a) < eps2) { // if a is 0, it's linear + if (STBTT_fabs(b) >= eps2) { res[num++] = -c/b; } } else { @@ -577,7 +577,7 @@ void json_string_get(const JSON *json, JSONString string, char *buf, size_t buf_ hex[0] = text[i++]; hex[1] = text[i++]; hex[2] = text[i++]; - hex[3] = text[i++]; + hex[3] = text[i]; unsigned code_point=0; sscanf(hex, "%04x", &code_point); // technically this won't deal with people writing out UTF-16 surrogate halves diff --git a/lsp-parse.c b/lsp-parse.c index 63d5605..5881ec3 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -280,6 +280,11 @@ static void parse_capabilities(LSP *lsp, const JSON *json, JSONObject capabiliti cap->range_formatting_support = true; } + JSONValue code_action_value = json_object_get(json, capabilities, "codeActionProvider"); + if (code_action_value.type == JSON_OBJECT || code_action_value.type == JSON_TRUE) { + cap->code_action_support = true; + } + JSONObject workspace = json_object_get_object(json, capabilities, "workspace"); // check WorkspaceFoldersServerCapabilities JSONObject workspace_folders = json_object_get_object(json, workspace, "workspaceFolders"); @@ -795,10 +800,14 @@ static bool parse_publish_diagnostics(LSP *lsp, const JSON *json, LSPRequest *re return false; JSONArray diagnostics = json_object_get_array(json, params, "diagnostics"); for (u32 i = 0; i < diagnostics.len; ++i) { - JSONObject diagnostic_in = json_array_get_object(json, diagnostics, i); + JSONValue diagnostic_val = json_array_get(json, diagnostics, i); + JSONObject diagnostic_in = json_force_object(diagnostic_val); LSPDiagnostic *diagnostic_out = arr_addp(pub->diagnostics); if (!parse_diagnostic(lsp, request, json, diagnostic_in, diagnostic_out)) return false; + char *raw = json_reserialize(json, diagnostic_val); + diagnostic_out->raw = lsp_request_add_string(request, raw); + free(raw); } return true; } @@ -1081,6 +1090,79 @@ static bool parse_formatting_response(LSP *lsp, const JSON *json, LSPResponse *r return true; } +static bool parse_command(LSP *lsp, LSPResponse *response, const JSON *json, JSONObject command_in, LSPCommand *command_out) { + JSONString command_str = json_object_get_string(json, command_in, "command"); + JSONArray arguments = json_object_get_array(json, command_in, "arguments"); + char command[64]; + json_string_get(json, command_str, command, sizeof command); + if (strcmp(command, "java.apply.workspaceEdit") == 0) { + // why does this exist?? + if (arguments.len != 1) { + lsp_set_error(lsp, "Expected 1 argument to %s; got %" PRIu32, command, arguments.len); + return false; + } + JSONObject edit = json_array_get_object(json, arguments, 0); + if (!parse_workspace_edit(lsp, response, json, edit, &command_out->data.edit)) { + lsp_workspace_edit_free(&command_out->data.edit); + return false; + } + command_out->kind = LSP_COMMAND_WORKSPACE_EDIT; + return true; + } + eprint("Unrecognized command: %s\n", command); + (void)command_out; + return false; +} + +static LSPCodeActionKind parse_code_action_kind(const char *kind) { + if (streq(kind, "quickfix")) + return LSP_CODE_ACTION_QUICKFIX; + return LSP_CODE_ACTION_OTHER; +} + +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); + action_out.is_preferred = json_object_get_bool(json, action, "isPreferred", false); + JSONString kind_str = json_object_get_string(json, action, "kind"); + char kind[32]; + json_string_get(json, kind_str, kind, sizeof kind); + action_out.kind = parse_code_action_kind(kind); + bool understood = true; + if (command_val.type == JSON_STRING) { + // this action is a Command + understood &= parse_command(lsp, response, 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, response, 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 @@ -1164,6 +1246,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-write.c b/lsp-write.c index c1c846a..6c6d5a6 100644 --- a/lsp-write.c +++ b/lsp-write.c @@ -250,6 +250,68 @@ static void write_document_position(JSONWriter *o, LSPDocumentPosition pos) { write_key_position(o, "position", pos.pos); } + +static void json_reserialize_to(const JSON *json, JSONWriter *o, JSONValue val) { + switch (val.type) { + case JSON_UNDEFINED: + assert(false); + break; + case JSON_TRUE: + write_bool(o, true); + break; + case JSON_FALSE: + write_bool(o, false); + break; + case JSON_NULL: + write_null(o); + break; + case JSON_NUMBER: + write_number(o, val.val.number); + break; + case JSON_STRING: { + char *s = json_string_get_alloc(json, val.val.string); + write_string(o, s); + free(s); + } break; + case JSON_ARRAY: { + JSONArray array = val.val.array; + write_arr_start(o); + for (u32 i = 0; i < array.len; i++) { + JSONValue elem = json_array_get(json, array, i); + write_arr_elem(o); + json_reserialize_to(json, o, elem); + } + write_arr_end(o); + } break; + case JSON_OBJECT: { + JSONObject object = val.val.object; + write_obj_start(o); + JSONValue *keys = &json->values[object.items]; + JSONValue *values = keys + object.len; + for (u32 i = 0; i < object.len; i++) { + JSONValue key_val = keys[i]; + assert(key_val.type == JSON_STRING); + JSONString key = key_val.val.string; + JSONValue value = values[i]; + char *key_str = json_string_get_alloc(json, key); + write_key(o, key_str); + free(key_str); + json_reserialize_to(json, o, value); + } + write_obj_end(o); + } break; + } +} + +char *json_reserialize(const JSON *json, JSONValue value) { + StrBuilder builder = str_builder_new(); + JSONWriter writer = json_writer_new(NULL, &builder); + json_reserialize_to(json, &writer, value); + char *str = str_dup(builder.str); + str_builder_free(&builder); + return str; +} + static const char *lsp_request_method(LSPRequest *request) { switch (request->type) { case LSP_REQUEST_NONE: break; @@ -311,6 +373,8 @@ static const char *lsp_request_method(LSPRequest *request) { return "textDocument/rangeFormatting"; case LSP_REQUEST_FORMATTING: return "textDocument/formatting"; + case LSP_REQUEST_CODE_ACTION: + return "textDocument/codeAction"; } assert(0); return "$/ignore"; @@ -468,6 +532,24 @@ void write_request(LSP *lsp, LSPRequest *request, StrBuilder *builder) { write_key_obj_start(o, "publishDiagnostics"); write_key_bool(o, "codeDescriptionSupport", true); write_obj_end(o); + + write_key_obj_start(o, "codeAction"); + write_key_bool(o, "isPreferredSupport", true); + write_key_obj_start(o, "codeActionLiteralSupport"); + write_key_obj_start(o, "codeActionKind"); + write_key_arr_start(o, "valueSet"); + write_arr_elem_string(o, "quickfix"); + write_arr_elem_string(o, "refactor"); + write_arr_elem_string(o, "refactor.extract"); + write_arr_elem_string(o, "refactor.inline"); + write_arr_elem_string(o, "refactor.rewrite"); + write_arr_elem_string(o, "source"); + write_arr_elem_string(o, "source.organizeImports"); + write_arr_elem_string(o, "source.fixAll"); + write_arr_end(o); + write_obj_end(o); + write_obj_end(o); + write_obj_end(o); write_obj_end(o); write_key_obj_start(o, "workspace"); write_key_bool(o, "workspaceFolders", true); @@ -670,6 +752,23 @@ void write_request(LSP *lsp, LSPRequest *request, StrBuilder *builder) { } write_obj_end(o); } break; + case LSP_REQUEST_CODE_ACTION: { + LSPRequestCodeAction *code_action = &request->data.code_action; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_file_uri(o, "uri", code_action->document); + write_obj_end(o); + write_key_range(o, "range", code_action->range); + write_key_obj_start(o, "context"); + write_key_arr_start(o, "diagnostics"); + arr_foreach_ptr(code_action->raw_diagnostics, const LSPString, raw_diagnostic) { + write_arr_elem(o); + str_builder_append(o->builder, lsp_request_string(request, *raw_diagnostic)); + } + write_arr_end(o); + write_obj_end(o); + write_obj_end(o); + } break; } write_obj_end(o); @@ -136,6 +136,10 @@ void lsp_request_free(LSPRequest *r) { case LSP_REQUEST_PREPARE_RENAME: case LSP_REQUEST_WORKSPACE_SYMBOLS: break; + case LSP_REQUEST_CODE_ACTION: { + LSPRequestCodeAction *c = &r->data.code_action; + arr_free(c->raw_diagnostics); + } break; case LSP_REQUEST_DID_CHANGE: { LSPRequestDidChange *c = &r->data.change; arr_free(c->changes); @@ -149,6 +153,15 @@ void lsp_request_free(LSPRequest *r) { memset(r, 0, sizeof *r); } +void lsp_workspace_edit_free(LSPWorkspaceEdit *edit) { + arr_foreach_ptr(edit->changes, LSPWorkspaceChange, c) { + if (c->type == LSP_CHANGE_EDITS) { + arr_free(c->data.edit.edits); + } + } + arr_free(edit->changes); +} + void lsp_response_free(LSPResponse *r) { lsp_message_base_free(&r->base); switch (r->request.type) { @@ -166,12 +179,7 @@ void lsp_response_free(LSPResponse *r) { break; case LSP_REQUEST_RENAME: { LSPResponseRename *rename = &r->data.rename; - arr_foreach_ptr(rename->changes, LSPWorkspaceChange, c) { - if (c->type == LSP_CHANGE_EDITS) { - arr_free(c->data.edit.edits); - } - } - arr_free(r->data.rename.changes); + lsp_workspace_edit_free(rename); } break; case LSP_REQUEST_HIGHLIGHT: arr_free(r->data.highlight.highlights); @@ -185,6 +193,19 @@ 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) { + switch (action->command.kind) { + case LSP_COMMAND_WORKSPACE_EDIT: + lsp_workspace_edit_free(&action->command.data.edit); + break; + default: break; + } + lsp_workspace_edit_free(&action->edit); + } + arr_free(c->actions); + } break; default: break; } @@ -237,6 +258,8 @@ static bool lsp_supports_request(LSP *lsp, const LSPRequest *request) { return cap->open_close_support; case LSP_REQUEST_DID_CHANGE: return cap->sync_support; + case LSP_REQUEST_CODE_ACTION: + return cap->code_action_support; case LSP_REQUEST_INITIALIZE: case LSP_REQUEST_INITIALIZED: case LSP_REQUEST_CANCEL: @@ -320,6 +343,7 @@ static bool request_type_is_notification(LSPRequestType type) { case LSP_REQUEST_DOCUMENT_LINK: case LSP_REQUEST_FORMATTING: case LSP_REQUEST_RANGE_FORMATTING: + case LSP_REQUEST_CODE_ACTION: return false; } assert(0); @@ -447,7 +471,7 @@ static bool lsp_receive(LSP *lsp, size_t max_size) { arr_hdr_(lsp->received_data)->len = (u32)received_so_far; lsp->received_data[received_so_far] = '\0';// null terminate #if LSP_SHOW_S2C - const int limit = 1000; + const int limit = 10000; debug_println("%s%.*s%s%s",term_italics(stdout),limit,lsp->received_data, strlen(lsp->received_data) > (size_t)limit ? "..." : "", term_clear(stdout)); @@ -474,10 +498,6 @@ static bool lsp_receive(LSP *lsp, size_t max_size) { json_free(&json); } size_t leftover_data_len = arr_len(lsp->received_data) - (response_offset + response_size); - - //printf("arr_cap = %u response_offset = %u, response_size = %zu, leftover len = %u\n", - // arr_hdr_(lsp->received_data)->cap, - // response_offset, response_size, leftover_data_len); memmove(lsp->received_data, lsp->received_data + response_offset + response_size, leftover_data_len); arr_set_len(lsp->received_data, leftover_data_len); @@ -97,6 +97,7 @@ typedef enum { LSP_REQUEST_LOG_MESSAGE, //< window/logMessage LSP_REQUEST_WORKSPACE_FOLDERS, //< workspace/workspaceFolders - NOTE: this is handled directly in lsp-parse.c (because it only needs information from the LSP struct) LSP_REQUEST_PUBLISH_DIAGNOSTICS, //< textDocument/publishDiagnostics + LSP_REQUEST_CODE_ACTION, //< textDocument/codeAction } LSPRequestType; typedef enum { @@ -170,6 +171,8 @@ typedef struct { /// URI to description of code /// e.g. for Rust's E0621, this would be https://doc.rust-lang.org/error_codes/E0621.html LSPString code_description_uri; + // raw diagnostic object, to be used with textDocument/codeAction + LSPString raw; } LSPDiagnostic; typedef struct { @@ -259,6 +262,12 @@ typedef struct { } LSPRequestFormatting; typedef struct { + LSPDocumentID document; + LSPRange range; + LSPString *raw_diagnostics; +} LSPRequestCodeAction; + +typedef struct { LSPMessageType type; /// LSP requests/responses tend to have a lot of strings. /// to avoid doing a ton of allocations+frees, @@ -296,6 +305,7 @@ typedef struct { LSPRequestPublishDiagnostics publish_diagnostics; // LSP_REQUEST_FORMATTING and LSP_REQUEST_RANGE_FORMATTING LSPRequestFormatting formatting; + LSPRequestCodeAction code_action; } data; } LSPRequest; @@ -565,6 +575,35 @@ typedef struct { LSPTextEdit *edits; } LSPResponseFormatting; +typedef enum { + LSP_COMMAND_NONE, + LSP_COMMAND_WORKSPACE_EDIT, +} LSPCommandKind; + +typedef struct { + LSPCommandKind kind; + union { + LSPWorkspaceEdit edit; + } data; +} LSPCommand; + +typedef enum { + LSP_CODE_ACTION_OTHER, + LSP_CODE_ACTION_QUICKFIX, +} LSPCodeActionKind; + +typedef struct { + LSPString name; + LSPWorkspaceEdit edit; + LSPCodeActionKind kind; + bool is_preferred; + LSPCommand command; +} LSPCodeAction; + +typedef struct { + LSPCodeAction *actions; +} LSPResponseCodeAction; + typedef struct { LSPMessageBase base; /// the request which this is a response to @@ -586,6 +625,7 @@ typedef struct { LSPResponseDocumentLink document_link; /// `LSP_REQUEST_FORMATTING` or `LSP_REQUEST_RANGE_FORMATTING` LSPResponseFormatting formatting; + LSPResponseCodeAction code_action; } data; } LSPResponse; @@ -629,6 +669,7 @@ typedef struct { bool document_link_support; bool formatting_support; bool range_formatting_support; + bool code_action_support; } LSPCapabilities; typedef struct LSP LSP; @@ -669,6 +710,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. @@ -935,8 +977,11 @@ size_t json_escape_to(char *out, size_t out_sz, const char *in); char *json_escape(const char *str); LSPString lsp_response_add_json_string(LSPResponse *response, const JSON *json, JSONString string); LSPString lsp_request_add_json_string(LSPRequest *request, const JSON *json, JSONString string); +void lsp_workspace_edit_free(LSPWorkspaceEdit *edit); /// free resources used by lsp-write.c void lsp_write_quit(void); +// convert JSON value back into string +char *json_reserialize(const JSON *json, JSONValue value); /// print server-to-client communication #define LSP_SHOW_S2C 0 @@ -1,6 +1,5 @@ /* FUTURE FEATURES: -- LSP "actions" - wrap-text command - path-specific extensions - more tests @@ -95,6 +94,7 @@ the first character can be interpreted specially if it is one of the following: #include "menu.c" #include "ide-autocomplete.c" #include "ide-signature-help.c" +#include "ide-code-action.c" #include "ide-rename-symbol.c" #include "ide-hover.c" #include "ide-definitions.c" @@ -587,6 +587,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); @@ -968,6 +969,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; @@ -1005,10 +1007,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); } } @@ -1101,6 +1108,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); @@ -1293,6 +1301,7 @@ int main(int argc, char **argv) { signature_help_quit(ted); autocomplete_quit(ted); format_quit(ted); + code_action_quit(ted); highlights_quit(ted); usages_quit(ted); session_write(ted); diff --git a/pre-commit.sh b/pre-commit.sh index 3553140..ccf1943 100755 --- a/pre-commit.sh +++ b/pre-commit.sh @@ -6,5 +6,5 @@ # Check for trailing whitespace git grep -In ' $' && { echo 'Fix trailing whitespace!'; exit 1; } -git grep -In '\w'"$(printf '\t')"'$' && { echo 'Fix trailing whitespace!'; exit 1; } +git grep -In '\S'"$(printf '\t')" && { echo 'Fix trailing whitespace!'; exit 1; } exit 0 @@ -2139,7 +2139,7 @@ static void syntax_highlight_css(SyntaxState *state_ptr, const char32_t *line, u dealt_with = true; if (!in_braces && char_types) { goto handle_pseudo; - } else { + } else { --i; // we'll increment i from the for loop } break; diff --git a/ted-internal.h b/ted-internal.h index a9de122..6875793 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; @@ -448,8 +452,25 @@ struct Ted { u64 edit_notify_id; EditNotifyInfo *edit_notifys; +#if HAS_INOTIFY + // 16384 = default inotify queue size + char inotify_event_buf[16384 * sizeof(struct inotify_event)]; +#endif }; +typedef struct { + MessageType severity; + // start position + BufferPos pos; + // end position + BufferPos end; + char *message; + // may be NULL + char *url; + // raw JSON object (for textDocument/codeAction) + char *raw; +} Diagnostic; + // === buffer.c === /// create a new empty buffer with no file name TextBuffer *buffer_new(Ted *ted); @@ -508,6 +529,7 @@ int buffer_inotify_watch(TextBuffer *buffer); /// inform buffer that its watch on the Ted's inotify was modified void buffer_set_inotify_modified(TextBuffer *buffer); #endif +const Diagnostic *buffer_diagnostics(TextBuffer *buffer); // === build.c === void build_frame(Ted *ted, float x1, float y1, float x2, float y2); @@ -634,6 +656,12 @@ void autocomplete_quit(Ted *ted); void autocomplete_frame(Ted *ted); void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *response); +// === ide-code-action.c === +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); /// go to the definition of `name`. @@ -771,5 +799,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_ @@ -982,16 +982,96 @@ 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 if (ted->inotify_fd == -1) return; int *watches_modified = NULL; - char events[16384 * sizeof(struct inotify_event)]; - ssize_t bytes = read(ted->inotify_fd, events, sizeof events); + ssize_t bytes = read(ted->inotify_fd, ted->inotify_event_buf, sizeof ted->inotify_event_buf); for (int i = 0; i + (int)sizeof(struct inotify_event) <= bytes; i += (int)sizeof(struct inotify_event)) { struct inotify_event event; - memcpy(&event, &events[i * (int)sizeof event], sizeof event); + memcpy(&event, &ted->inotify_event_buf[i], sizeof event); if (event.mask & INOTIFY_MASK) { bool new = true; arr_foreach_ptr(watches_modified, int, w) { @@ -1002,12 +1082,10 @@ void ted_check_inotify(Ted *ted) { } if (new) arr_add(watches_modified, event.wd); } - i += (int)event.len; // should always be 0 in theory... + i += (int)event.len; // should always be 0 in theory given our event types... } // ideally we'd read more events in a loop here but then we'd hang if someone is constantly // changing the file. probably no good way of dealing with that though. - // for what it's worth, 16384 is the default max inotify queue size, so we should always - // read all the events with that one read call. arr_foreach_ptr(ted->buffers, TextBufferPtr, pbuffer) { int watch = buffer_inotify_watch(*pbuffer); arr_foreach_ptr(watches_modified, int, w) { @@ -321,7 +321,6 @@ Ctrl+Shift+r = :reload-all # to set the language of the current buffer to Rust, for example. This might be useful if # one file extension could be multiple different languages. # Here are the numbers corresponding to each language we have syntax highlighting for: -# 0 Plain text (no syntax highlighting) # 1 C # 2 C++ # 3 Rust @@ -338,6 +337,10 @@ Ctrl+Shift+r = :reload-all # 14 JSON # 15 XML # 16 GLSL +# 17 Plain text +# 18 CSS +# 19 GdScript (Godot) +# 20 C# # -1 Guess from the file extension (default) # IDE features @@ -346,6 +349,10 @@ Ctrl+Space = :autocomplete Ctrl+Shift+Space = :autocomplete-back Ctrl+u = :find-usages Ctrl+r = :rename-symbol +# Show "code action" suggestions (refactoring, quick fixes, etc.) / move code action cursor forwards +Alt+Space = :code-action +# Move code action cursor backwards +Alt+Shift+Space = :code-action-prev Ctrl+z = :undo Ctrl+Shift+z = :redo @@ -22,7 +22,7 @@ extern "C" { #include "command.h" /// Version number -#define TED_VERSION "2.8.4" +#define TED_VERSION "2.9.0" /// Maximum path size ted handles. #define TED_PATH_MAX 1024 /// Config filename @@ -928,6 +928,20 @@ 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); +/// Select current code action +void code_action_select(Ted *ted); +/// Move code action cursor forwards +void code_action_next(Ted *ted); +/// Move code action cursor backwards +void code_action_prev(Ted *ted); + // === ide-definitions.c === /// cancel the last go-to-definition / find symbols request. void definition_cancel_lookup(Ted *ted); @@ -648,7 +648,7 @@ void file_selector_render(Ted *ted, FileSelector *fs) { { const char *cwd = fs->cwd; const float text_width = text_get_size_vec2(font, cwd).x; - TextRenderState state = text_render_state_default; + TextRenderState state = text_render_state_default; state.x = bounds.pos.x; if (text_width > bounds.size.x) { // very long cwd diff --git a/windows_installer/ted/ted.vdproj b/windows_installer/ted/ted.vdproj index 9bf631c..b9e5a67 100644 --- a/windows_installer/ted/ted.vdproj +++ b/windows_installer/ted/ted.vdproj @@ -620,15 +620,15 @@ { "Name" = "8:Microsoft Visual Studio" "ProductName" = "8:ted" - "ProductCode" = "8:{C36CA9E0-6E99-42B9-9F94-8869D5DFD9D9}" - "PackageCode" = "8:{4EF1D7F9-DEA9-44C7-9CDA-9B26348DE375}" + "ProductCode" = "8:{943A950E-7D69-4097-B889-3015A528059E}" + "PackageCode" = "8:{F551F5C0-E284-4DC1-B334-88F6AAFE255B}" "UpgradeCode" = "8:{844F6C2B-DF3B-4A81-9BD5-603401BBA651}" "AspNetVersion" = "8:2.0.50727.0" "RestartWWWService" = "11:FALSE" "RemovePreviousVersions" = "11:TRUE" "DetectNewerInstalledVersion" = "11:FALSE" "InstallAllUsers" = "11:FALSE" - "ProductVersion" = "8:25.09.2800" + "ProductVersion" = "8:25.09.3000" "Manufacturer" = "8:ted" "ARPHELPTELEPHONE" = "8:" "ARPHELPLINK" = "8:" |