diff options
-rw-r--r-- | autocomplete.c | 294 | ||||
-rw-r--r-- | buffer.c | 8 | ||||
-rw-r--r-- | command.c | 2 | ||||
-rw-r--r-- | main.c | 8 | ||||
-rw-r--r-- | menu.c | 2 | ||||
-rw-r--r-- | ted.c | 2 | ||||
-rw-r--r-- | ted.h | 8 |
7 files changed, 178 insertions, 146 deletions
diff --git a/autocomplete.c b/autocomplete.c index 23523bd..ed6687a 100644 --- a/autocomplete.c +++ b/autocomplete.c @@ -1,168 +1,186 @@ +#define TAGS_MAX_COMPLETIONS 200 // max # of tag completions to scroll through +#define AUTOCOMPLETE_NCOMPLETIONS 10 // max # of completions to show at once -#define AUTOCOMPLETE_NCOMPLETIONS 10 // max # of completions to show - -// get the thing to be completed (and what buffer it's in) -// returns false if this is a read only buffer or something -// call free() on *startp when you're done with it -static Status autocomplete_get(Ted *ted, char **startp, TextBuffer **bufferp) { - TextBuffer *buffer = ted->active_buffer; - if (buffer && !buffer->view_only && !buffer->is_line_buffer) { - buffer->selection = false; - if (is_word(buffer_char_after_cursor(buffer))) - buffer_cursor_move_right_words(buffer, 1); - else - buffer_scroll_to_cursor(buffer); - char *start = str32_to_utf8_cstr(buffer_word_at_cursor(buffer)); - if (*start) { - *startp = start; - *bufferp = buffer; - return true; - } else { - // no word at cursor - free(start); - return false; - } - } else { - return false; - } +static void autocomplete_clear_completions(Ted *ted) { + arr_foreach_ptr(ted->autocompletions, Autocompletion, completion) { + free(completion->label); + free(completion->text); + } + arr_clear(ted->autocompletions); } // do the actual completion -static void autocomplete_complete(Ted *ted, char *start, TextBuffer *buffer, char *completion) { - char *str = completion + strlen(start); +static void autocomplete_complete(Ted *ted, Autocompletion *completion) { + TextBuffer *buffer = ted->active_buffer; buffer_start_edit_chain(buffer); // don't merge with other edits - buffer_insert_utf8_at_cursor(buffer, str); + if (is_word(buffer_char_before_cursor(buffer))) + buffer_backspace_words_at_cursor(buffer, 1); // delete whatever text was already typed + buffer_insert_utf8_at_cursor(buffer, completion->text); buffer_end_edit_chain(buffer); - ted->autocomplete = false; + autocomplete_close(ted); } static void autocomplete_select_cursor_completion(Ted *ted) { - char *start; TextBuffer *buffer; - if (autocomplete_get(ted, &start, &buffer)) { - char *completions[AUTOCOMPLETE_NCOMPLETIONS]; - size_t ncompletions = tags_beginning_with(ted, start, completions, arr_count(completions)); + if (ted->autocomplete) { + size_t ncompletions = arr_len(ted->autocompletions); if (ncompletions) { i64 cursor = mod_i64(ted->autocomplete_cursor, (i64)ncompletions); - autocomplete_complete(ted, start, buffer, completions[cursor]); - for (size_t i = 0; i < ncompletions; ++i) - free(completions[i]); + autocomplete_complete(ted, &ted->autocompletions[cursor]); + autocomplete_close(ted); + } + } +} + + +void autocomplete_close(Ted *ted) { + if (ted->autocomplete) { + ted->autocomplete = false; + autocomplete_clear_completions(ted); + } +} + +static void autocomplete_find_completions(Ted *ted) { + TextBuffer *buffer = ted->active_buffer; + BufferPos pos = buffer->cursor_pos; + if (buffer_pos_eq(pos, ted->autocomplete_pos)) + return; // no need to update completions. + ted->autocomplete_pos = pos; + + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + LSPRequest request = { + .type = LSP_REQUEST_COMPLETION + }; + request.data.completion = (LSPRequestCompletion) { + .position = { + .document = str_dup(buffer->filename), + .pos = buffer_pos_to_lsp(buffer, pos) + } + }; + lsp_send_request(lsp, &request); + } else { + // tag completion + autocomplete_clear_completions(ted); + + char *word_at_cursor = str32_to_utf8_cstr(buffer_word_at_cursor(buffer)); + char **completions = calloc(TAGS_MAX_COMPLETIONS, sizeof *completions); + size_t ncompletions = tags_beginning_with(ted, word_at_cursor, completions, TAGS_MAX_COMPLETIONS); + free(word_at_cursor); + + arr_set_len(ted->autocompletions, ncompletions); + + for (size_t i = 0; i < ncompletions; ++i) { + ted->autocompletions[i].label = completions[i]; + ted->autocompletions[i].text = str_dup(completions[i]); } - free(start); + free(completions); } } // open autocomplete, or just do the completion if there's only one suggestion static void autocomplete_open(Ted *ted) { - char *start; - TextBuffer *buffer; + if (!ted->active_buffer) return; + TextBuffer *buffer = ted->active_buffer; + if (!buffer->filename) return; + if (buffer->view_only) return; + ted->cursor_error_time = 0; + ted->autocomplete_pos = (BufferPos){0,0}; ted->autocomplete_cursor = 0; - if (autocomplete_get(ted, &start, &buffer)) { - char *completions[2] = {0}; - size_t ncompletions = tags_beginning_with(ted, start, completions, 2); - switch (ncompletions) { - case 0: - ted->cursor_error_time = time_get_seconds(); - break; - case 1: - autocomplete_complete(ted, start, buffer, completions[0]); - break; - case 2: - // open autocomplete selection menu - ted->autocomplete = true; - break; - default: assert(0); break; - } - free(completions[0]); - free(completions[1]); - free(start); + ted->autocompletions = NULL; + autocomplete_find_completions(ted); + switch (arr_len(ted->autocompletions)) { + case 0: + ted->cursor_error_time = time_get_seconds(); + autocomplete_close(ted); + break; + case 1: + autocomplete_complete(ted, &ted->autocompletions[0]); + // (^ this calls autocomplete_close) + break; + default: + // open autocomplete menu + ted->autocomplete = true; + break; } } static void autocomplete_frame(Ted *ted) { + TextBuffer *buffer = ted->active_buffer; + Font *font = ted->font; + float char_height = text_font_char_height(font); + Settings const *settings = buffer_settings(buffer); + u32 const *colors = settings->colors; + float const padding = settings->padding; + + autocomplete_find_completions(ted); + + size_t ncompletions = arr_len(ted->autocompletions); + if (ncompletions > AUTOCOMPLETE_NCOMPLETIONS) + ncompletions = AUTOCOMPLETE_NCOMPLETIONS; + float menu_width = 400, menu_height = (float)ncompletions * char_height + 2 * padding; - char *start; - TextBuffer *buffer; - if (autocomplete_get(ted, &start, &buffer)) { - Font *font = ted->font; - float char_height = text_font_char_height(font); - Settings const *settings = buffer_settings(buffer); - u32 const *colors = settings->colors; - float const padding = settings->padding; + if (ncompletions == 0) { + // no completions. close menu. + autocomplete_close(ted); + return; + } - char *completions[AUTOCOMPLETE_NCOMPLETIONS]; - size_t ncompletions = tags_beginning_with(ted, start, completions, arr_count(completions)); - float menu_width = 400, menu_height = (float)ncompletions * char_height + 2 * padding; - - if (ncompletions == 0) { - // no completions. close menu. - ted->autocomplete = false; - return; - } - - ted->autocomplete_cursor = (i32)mod_i64(ted->autocomplete_cursor, (i64)ncompletions); - - v2 cursor_pos = buffer_pos_to_pixels(buffer, buffer->cursor_pos); - bool open_up = cursor_pos.y > 0.5f * (buffer->y1 + buffer->y2); // should the completion menu open upwards? - bool open_left = cursor_pos.x > 0.5f * (buffer->x1 + buffer->x2); - float x = cursor_pos.x, start_y = cursor_pos.y; - if (open_left) x -= menu_width; - if (open_up) - start_y -= menu_height; - else - start_y += char_height; // put menu below cursor - { - Rect menu_rect = rect(V2(x, start_y), V2(menu_width, menu_height)); - gl_geometry_rect(menu_rect, colors[COLOR_MENU_BG]); - //gl_geometry_rect_border(menu_rect, 1, colors[COLOR_BORDER]); - ted->autocomplete_rect = menu_rect; - } - - // vertical padding - start_y += padding; - menu_height -= 2 * padding; - - u16 cursor_entry = (u16)((ted->mouse_pos.y - start_y) / char_height); - if (cursor_entry < ncompletions) { - // highlight moused over entry - Rect r = rect(V2(x, start_y + cursor_entry * char_height), V2(menu_width, char_height)); - gl_geometry_rect(r, colors[COLOR_MENU_HL]); - ted->cursor = ted->cursor_hand; - } - { // highlight cursor entry - Rect r = rect(V2(x, start_y + (float)ted->autocomplete_cursor * char_height), V2(menu_width, char_height)); - gl_geometry_rect(r, colors[COLOR_MENU_HL]); - } - - for (uint i = 0; i < ted->nmouse_clicks[SDL_BUTTON_LEFT]; ++i) { - v2 click = ted->mouse_clicks[SDL_BUTTON_LEFT][i]; - if (rect_contains_point(ted->autocomplete_rect, click)) { - u16 entry = (u16)((click.y - start_y) / char_height); - if (entry < ncompletions) { - // entry was clicked on! use this completion. - autocomplete_complete(ted, start, buffer, completions[entry]); - } + ted->autocomplete_cursor = (i32)mod_i64(ted->autocomplete_cursor, (i64)ncompletions); + + v2 cursor_pos = buffer_pos_to_pixels(buffer, buffer->cursor_pos); + bool open_up = cursor_pos.y > 0.5f * (buffer->y1 + buffer->y2); // should the completion menu open upwards? + bool open_left = cursor_pos.x > 0.5f * (buffer->x1 + buffer->x2); + float x = cursor_pos.x, start_y = cursor_pos.y; + if (open_left) x -= menu_width; + if (open_up) + start_y -= menu_height; + else + start_y += char_height; // put menu below cursor + { + Rect menu_rect = rect(V2(x, start_y), V2(menu_width, menu_height)); + gl_geometry_rect(menu_rect, colors[COLOR_MENU_BG]); + //gl_geometry_rect_border(menu_rect, 1, colors[COLOR_BORDER]); + ted->autocomplete_rect = menu_rect; + } + + // vertical padding + start_y += padding; + menu_height -= 2 * padding; + + u16 cursor_entry = (u16)((ted->mouse_pos.y - start_y) / char_height); + if (cursor_entry < ncompletions) { + // highlight moused over entry + Rect r = rect(V2(x, start_y + cursor_entry * char_height), V2(menu_width, char_height)); + gl_geometry_rect(r, colors[COLOR_MENU_HL]); + ted->cursor = ted->cursor_hand; + } + { // highlight cursor entry + Rect r = rect(V2(x, start_y + (float)ted->autocomplete_cursor * char_height), V2(menu_width, char_height)); + gl_geometry_rect(r, colors[COLOR_MENU_HL]); + } + + for (uint i = 0; i < ted->nmouse_clicks[SDL_BUTTON_LEFT]; ++i) { + v2 click = ted->mouse_clicks[SDL_BUTTON_LEFT][i]; + if (rect_contains_point(ted->autocomplete_rect, click)) { + u16 entry = (u16)((click.y - start_y) / char_height); + if (entry < ncompletions) { + // entry was clicked on! use this completion. + autocomplete_complete(ted, &ted->autocompletions[entry]); } } - - float y = start_y; - TextRenderState state = text_render_state_default; - state.min_x = x + padding; state.min_y = y; state.max_x = x + menu_width - padding; state.max_y = y + menu_height; - rgba_u32_to_floats(colors[COLOR_TEXT], state.color); - for (size_t i = 0; i < ncompletions; ++i) { - state.x = x + padding; state.y = y; - text_utf8_with_state(font, &state, completions[i]); - y += char_height; - } - - gl_geometry_draw(); - text_render(font); - - for (size_t i = 0; i < ncompletions; ++i) - free(completions[i]); + } - free(start); - } else { - ted->autocomplete = false; + float y = start_y; + TextRenderState state = text_render_state_default; + state.min_x = x + padding; state.min_y = y; state.max_x = x + menu_width - padding; state.max_y = y + menu_height; + rgba_u32_to_floats(colors[COLOR_TEXT], state.color); + for (size_t i = 0; i < ncompletions; ++i) { + state.x = x + padding; state.y = y; + text_utf8_with_state(font, &state, ted->autocompletions[i].label); + y += char_height; } + + gl_geometry_draw(); + text_render(font); } @@ -287,6 +287,8 @@ Language buffer_language(TextBuffer *buffer) { } LSP *buffer_lsp(TextBuffer *buffer) { + if (!buffer_is_named_file(buffer)) + return NULL; return ted_get_lsp(buffer->ted, buffer_language(buffer)); } @@ -1386,7 +1388,7 @@ static Status buffer_insert_lines(TextBuffer *buffer, u32 where, u32 number) { } // LSP uses UTF-16 indices because Microsoft fucking loves UTF-16 and won't let it die -static LSPPosition buffer_pos_to_lsp(TextBuffer *buffer, BufferPos pos) { +LSPPosition buffer_pos_to_lsp(TextBuffer *buffer, BufferPos pos) { LSPPosition lsp_pos = { .line = pos.line }; @@ -2111,7 +2113,7 @@ Status buffer_load_file(TextBuffer *buffer, char const *filename) { buffer_seterr(buffer, "File too big (size: %zu).", file_size); success = false; } else { - u8 *file_contents = buffer_calloc(buffer, 1, file_size); + u8 *file_contents = buffer_calloc(buffer, 1, file_size + 1); lines_capacity = 4; lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines nlines = 1; @@ -2345,7 +2347,7 @@ bool buffer_handle_click(Ted *ted, TextBuffer *buffer, v2 click, u8 times) { if (rect_contains_point(ted->autocomplete_rect, click)) return false; // don't look at clicks in the autocomplete menu else - ted->autocomplete = false; // close autocomplete menu if user clicks outside of it + autocomplete_close(ted); // close autocomplete menu if user clicks outside of it } if (buffer_pixels_to_pos(buffer, click, &buffer_pos)) { // user clicked on buffer @@ -377,7 +377,7 @@ void command_execute(Ted *ted, Command c, i64 argument) { // dismiss error box *ted->error_shown = '\0'; } else if (ted->autocomplete) { - ted->autocomplete = false; + autocomplete_close(ted); } else if (ted->menu) { menu_escape(ted); } else { @@ -1,10 +1,14 @@ -/* o +/* @TODO: +- make sure autocomplete is closed when you switch buffers +- scroll through completions +- figure out under which circumstances backspace should close completions - rename buffer->filename to buffer->path - rust-analyzer bug reports: - bad json can give "Unexpected error: client exited without proper shutdown sequence" - rust-analyzer should wait until cargo metadata/check is done before sending initialize response FUTURE FEATURES: +- robust find (results shouldn't move around when you type things) - configurable max buffer size + max view-only buffer size - :set-build-command, don't let ../Cargo.toml override ./Makefile - add numlock as a key modifier @@ -977,7 +981,7 @@ int main(int argc, char **argv) { autocomplete_frame(ted); } } else { - ted->autocomplete = false; + autocomplete_close(ted); text_utf8_anchored(font, "Press Ctrl+O to open a file or Ctrl+N to create a new one.", window_width * 0.5f, window_height * 0.5f, ted_color(ted, COLOR_TEXT_SECONDARY), ANCHOR_MIDDLE); text_render(font); @@ -50,7 +50,7 @@ void menu_open(Ted *ted, Menu menu) { if (ted->menu) menu_close_with_next(ted, menu); if (ted->find) find_close(ted); - ted->autocomplete = false; + autocomplete_close(ted); ted->menu = menu; TextBuffer *prev_buf = ted->prev_active_buffer = ted->active_buffer; if (prev_buf) @@ -139,7 +139,7 @@ static void ted_load_fonts(Ted *ted) { void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer) { TextBuffer *search_buffer = find_search_buffer(ted); ted->active_buffer = buffer; - ted->autocomplete = false; + autocomplete_close(ted); if (buffer != search_buffer) { if (ted->find) find_update(ted, true); // make sure find results are for this file @@ -352,6 +352,11 @@ typedef struct { u32 build_output_line; // which line in the build output corresponds to this error } BuildError; +typedef struct { + char *label; + char *text; +} Autocompletion; + typedef struct Ted { struct LSP *test_lsp; // @TODO: something better @@ -405,6 +410,8 @@ typedef struct Ted { bool building; // is the build process running? bool autocomplete; // is the autocomplete window open? + Autocompletion *autocompletions; // dynamic array of suggestions + BufferPos autocomplete_pos; // position of cursor last time completions were generated. if this changes, we need to recompute completions. i32 autocomplete_cursor; // which completion is currently selected Rect autocomplete_rect; // rectangle where the autocomplete menu is (needed to avoid interpreting autocomplete clicks as other clicks) @@ -468,6 +475,7 @@ typedef struct Ted { char error_shown[512]; // error display in box on screen } Ted; +void autocomplete_close(Ted *ted); void command_execute(Ted *ted, Command c, i64 argument); void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer); // the settings of the active buffer, or the default settings if there is no active buffer |