#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; } } // do the actual completion static void autocomplete_complete(Ted *ted, char *start, TextBuffer *buffer, char *completion) { char *str = completion + strlen(start); buffer_start_edit_chain(buffer); // don't merge with other edits buffer_insert_utf8_at_cursor(buffer, str); buffer_end_edit_chain(buffer); ted->autocomplete = false; } 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 (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]); } free(start); } } // open autocomplete, or just do the completion if there's only one suggestion static void autocomplete_open(Ted *ted) { char *start; TextBuffer *buffer; ted->cursor_error_time = 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); } } static void autocomplete_frame(Ted *ted) { 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; 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]); } } } 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; } }