From e3eb333ae2b07467e14dd9e2f845889f75f01a16 Mon Sep 17 00:00:00 2001 From: Leo Tenenbaum Date: Wed, 3 Mar 2021 19:17:48 -0500 Subject: start autocomplete --- buffer.c | 35 ++++++++++++++++++--------- colors.h | 6 ++++- command.c | 35 +++++++++++++++++++++++++++ command.h | 5 +++- math.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tags.c | 13 +++++++--- ted.cfg | 6 +++++ ted.h | 4 ++- 8 files changed, 169 insertions(+), 18 deletions(-) diff --git a/buffer.c b/buffer.c index 21922ee..7c5c9db 100644 --- a/buffer.c +++ b/buffer.c @@ -2437,17 +2437,6 @@ void buffer_render(TextBuffer *buffer, Rect r) { text_render(font); if (ted->active_buffer == buffer) { - // render cursor - float time_on = settings->cursor_blink_time_on; - float time_off = settings->cursor_blink_time_off; - bool is_on = true; - if (time_off > 0) { - double absolute_time = time_get_seconds(); - float period = time_on + time_off; - // time in period - double t = fmod(absolute_time, period); - is_on = t < time_on; // are we in the "on" part of the period? - } // highlight matching brackets char32_t cursor_bracket = buffer_char_before_cursor(buffer); @@ -2477,11 +2466,33 @@ void buffer_render(TextBuffer *buffer, Rect r) { } } + // render cursor + float time_on = settings->cursor_blink_time_on; + float time_off = settings->cursor_blink_time_off; + double error_animation_duration = 1.0; + double error_animation_dt = time_get_seconds() - ted->cursor_error_time; + bool error_animation = ted->cursor_error_time > 0 && error_animation_dt < error_animation_duration; + + bool is_on = true; + if (!error_animation && time_off > 0) { + double absolute_time = time_get_seconds(); + float period = time_on + time_off; + // time in period + double t = fmod(absolute_time, period); + is_on = t < time_on; // are we in the "on" part of the period? + } if (is_on) { if (buffer_clip_rect(buffer, &cursor_rect)) { // draw cursor - gl_geometry_rect(cursor_rect, colors[buffer->view_only ? COLOR_VIEW_ONLY_CURSOR : COLOR_CURSOR]); + u32 color = colors[COLOR_CURSOR]; + if (buffer->view_only) + color = colors[COLOR_VIEW_ONLY_CURSOR]; + if (error_animation) { + color = color_interpolate(maxf(0, 2 * ((float)(error_animation_dt / error_animation_duration) - 0.5f)), colors[COLOR_CURSOR_ERROR], colors[COLOR_CURSOR]); + } + + gl_geometry_rect(cursor_rect, color); } } gl_geometry_draw(); diff --git a/colors.h b/colors.h index e1f033c..f45ed01 100644 --- a/colors.h +++ b/colors.h @@ -6,6 +6,7 @@ ENUM_U16 { COLOR_BG, COLOR_HL, COLOR_CURSOR, + COLOR_CURSOR_ERROR, COLOR_CURSOR_LINE_BG, COLOR_SELECTION_BG, COLOR_VIEW_ONLY_CURSOR, @@ -49,13 +50,14 @@ typedef struct { char const *name; } ColorName; -static ColorName const color_names[COLOR_COUNT] = { +static ColorName const color_names[] = { {COLOR_UNKNOWN, "unknown"}, {COLOR_TEXT, "text"}, {COLOR_TEXT_SECONDARY, "text-secondary"}, {COLOR_BG, "bg"}, {COLOR_HL, "hl"}, {COLOR_CURSOR, "cursor"}, + {COLOR_CURSOR_ERROR, "cursor-error"}, {COLOR_CURSOR_LINE_BG, "cursor-line-bg"}, {COLOR_VIEW_ONLY_CURSOR, "view-only-cursor"}, {COLOR_VIEW_ONLY_SELECTION_BG, "view-only-selection-bg"}, @@ -88,6 +90,8 @@ static ColorName const color_names[COLOR_COUNT] = { {COLOR_LINE_NUMBERS_SEPARATOR, "line-numbers-separator"}, }; +static_assert_if_possible(arr_count(color_names) == COLOR_COUNT) + static ColorSetting color_setting_from_str(char const *str) { // @OPTIMIZE: sort color_names, binary search for (int i = 0; i < COLOR_COUNT; ++i) { diff --git a/command.c b/command.c index 727b2c6..f0dd1ec 100644 --- a/command.c +++ b/command.c @@ -204,6 +204,39 @@ void command_execute(Ted *ted, Command c, i64 argument) { } } break; + + case CMD_AUTOCOMPLETE: { + 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)); + 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: { + char *str = completions[0] + 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); + } break; + case 2: + // open autocomplete selection menu + ted->autocomplete = true; + break; + default: assert(0); break; + } + free(completions[0]); + free(completions[1]); + free(start); + } + } break; + case CMD_UNDO: if (buffer) buffer_undo(buffer, argument); break; @@ -301,6 +334,8 @@ void command_execute(Ted *ted, Command c, i64 argument) { if (*ted->error_shown) { // dismiss error box *ted->error_shown = '\0'; + } else if (ted->autocomplete) { + ted->autocomplete = false; } else if (ted->menu) { menu_escape(ted); } else if (ted->find) { diff --git a/command.h b/command.h index 41bab31..1c9be20 100644 --- a/command.h +++ b/command.h @@ -50,7 +50,9 @@ ENUM_U16 { CMD_COMMAND_SELECTOR, CMD_OPEN_CONFIG, CMD_QUIT, - + + CMD_AUTOCOMPLETE, + CMD_COPY, CMD_CUT, CMD_PASTE, @@ -138,6 +140,7 @@ static CommandName const command_names[] = { {"copy", CMD_COPY}, {"cut", CMD_CUT}, {"paste", CMD_PASTE}, + {"autocomplete", CMD_AUTOCOMPLETE}, {"find", CMD_FIND}, {"find-replace", CMD_FIND_REPLACE}, {"tab-close", CMD_TAB_CLOSE}, diff --git a/math.c b/math.c index 7d16a52..75ec497 100644 --- a/math.c +++ b/math.c @@ -639,6 +639,13 @@ static v4 rgba_u32_to_v4(u32 rgba) { return V4(c[0], c[1], c[2], c[3]); } +static u32 rgba_v4_to_u32(v4 color) { + return (u32)(color.x * 255) << 24 + | (u32)(color.y * 255) << 16 + | (u32)(color.z * 255) << 8 + | (u32)(color.w * 255); +} + // returns average of red green and blue components of color static float rgba_brightness(u32 color) { u8 r = (u8)(color >> 24), g = (u8)(color >> 16), b = (u8)(color >> 8); @@ -740,3 +747,79 @@ static Rect rect_shrink(Rect r, float amount) { r.size.y = maxf(r.size.y, 0); return r; } + +static v4 color_rgba_to_hsva(v4 rgba) { + float R = rgba.x, G = rgba.y, B = rgba.z, A = rgba.w; + float M = maxf(R, maxf(G, B)); + float m = minf(R, minf(G, B)); + float C = M - m; + float H = 0; + if (C == 0) + H = 0; + if (M == R) + H = fmodf((G - B) / C, 6); + else if (M == G) + H = (B - R) / C + 2; + else if (M == B) + H = (R - G) / C + 4; + H *= 60; + float V = M; + float S = V == 0 ? 0 : C / V; + return V4(H, S, V, A); +} + +static v4 color_hsva_to_rgba(v4 hsva) { + float H = hsva.x, S = hsva.y, V = hsva.z, A = hsva.w; + H /= 60; + float C = S * V; + float X = C * (1 - fabsf(fmodf(H, 2) - 1)); + float R, G, B; + if (H <= 1) + R=C, G=X, B=0; + else if (H <= 2) + R=X, G=C, B=0; + else if (H <= 3) + R=0, G=C, B=X; + else if (H <= 4) + R=0, G=X, B=C; + else if (H <= 5) + R=X, G=0, B=C; + else + R=C, G=0, B=X; + + float m = V-C; + R += m; + G += m; + B += m; + return V4(R, G, B, A); +} + +static u32 color_interpolate(float x, u32 color1, u32 color2) { + x = x * x * (3 - 2*x); // hermite interpolation + + v4 c1 = rgba_u32_to_v4(color1), c2 = rgba_u32_to_v4(color2); + // to make it interpolate more nicely, convert to hsv, interpolate in that space, then convert back + c1 = color_rgba_to_hsva(c1); + c2 = color_rgba_to_hsva(c2); + float h1 = c1.x, s1 = c1.y, v1 = c1.z, a1 = c1.w; + float h2 = c2.x, s2 = c2.y, v2 = c2.z, a2 = c2.w; + + float s_out = lerpf(x, s1, s2); + float v_out = lerpf(x, v1, v2); + float a_out = lerpf(x, a1, a2); + + float h_out; + // because hue is on a circle, we need to make sure we take the shorter route around the circle + if (fabsf(h1 - h2) < 180) { + h_out = lerpf(x, h1, h2); + } else if (h1 > h2) { + h_out = lerpf(x, h1, h2 + 360); + } else { + h_out = lerpf(x, h1 + 360, h2); + } + h_out = fmodf(h_out, 360); + + v4 c_out = V4(h_out, s_out, v_out, a_out); + c_out = color_hsva_to_rgba(c_out); + return rgba_v4_to_u32(c_out); +} diff --git a/tags.c b/tags.c index a7b87e8..ad95d4c 100644 --- a/tags.c +++ b/tags.c @@ -42,6 +42,7 @@ static int tag_try(FILE *fp, char const *tag) { } // finds all tags beginning with the given prefix, returning them into *out, writing at most out_size entries. +// you may pass NULL for out, in which case just the number of matching tags is returned (still maxing out at out_size) // each element in out should be freed when you're done with them size_t tags_beginning_with(Ted *ted, char const *prefix, char **out, size_t out_size) { assert(out_size); @@ -77,14 +78,20 @@ size_t tags_beginning_with(Ted *ted, char const *prefix, char **out, size_t out_ size_t nmatches = 0; size_t prefix_len = strlen(prefix); bool done = false; + char prev_match[1024]; + while (!done && fgets(line, sizeof line, file)) { switch (strncmp(line, prefix, prefix_len)) { case 0: { char *tag = strn_dup(line, strcspn(line, "\t")); - if (nmatches == 0 || !streq(tag, out[nmatches - 1])) // don't include duplicate tags - out[nmatches++] = tag; - else + if (nmatches == 0 || !streq(tag, prev_match)) { // don't include duplicate tags + strbuf_cpy(prev_match, tag); + if (out) out[nmatches] = tag; + else free(tag); + ++nmatches; + } else { free(tag); + } if (nmatches >= out_size) done = true; } break; case +1: diff --git a/ted.cfg b/ted.cfg index d824b92..ff8ff87 100644 --- a/ted.cfg +++ b/ted.cfg @@ -86,6 +86,9 @@ Ctrl+s = :save Ctrl+Alt+Shift+s = :save-all Ctrl+Shift+s = :save-as Ctrl+q = :quit + +Ctrl+Space = :autocomplete + Ctrl+z = :undo Ctrl+Shift+z = :redo Ctrl+f = :find @@ -129,6 +132,7 @@ Ctrl+/ = :split-horizontal Ctrl+Shift+/ = :split-vertical # unsplit Ctrl+j = :split-join +# switch to the other side of a split Ctrl+tab = :split-swap Escape = :escape @@ -140,6 +144,8 @@ active-tab-hl = #a77a selected-tab-hl = #7777 cursor-line-bg = #fff2 cursor = #3ff +# used as cursor color when you do autocomplete and there are no suggestions +cursor-error = #f00 # color to highlight matching brackets with matching-bracket-hl = #fda8 selection-bg = #36aa diff --git a/ted.h b/ted.h index ea1afdd..7b0af48 100644 --- a/ted.h +++ b/ted.h @@ -263,6 +263,7 @@ typedef struct Ted { TextBuffer build_buffer; // buffer for build output (view only) TextBuffer argument_buffer; // used for command selector double error_time; // time error box was opened (in seconds -- see time_get_seconds) + double cursor_error_time; // time which the cursor error animation started (cursor turns red, e.g. when there's no autocomplete suggestion) KeyAction key_actions[KEY_COMBO_COUNT]; bool search_cwd; // should the working directory be searched for files? set to true if the executable isn't "installed" bool quit; // if set to true, the window will close next frame. NOTE: this doesn't check for unsaved changes!! @@ -277,7 +278,8 @@ typedef struct Ted { Command warn_unsaved; // if non-zero, the user is trying to execute this command, but there are unsaved changes bool build_shown; // are we showing the build output? bool building; // is the build process running? - + bool autocomplete; // is the autocomplete window open? + FILE *log; BuildError *build_errors; // dynamic array of build errors -- cgit v1.2.3