Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple Word Completion #198

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/default.focus-config
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ Ctrl-K delete_to_end_of_line
Ctrl-Shift-Delete delete_to_end_of_line
Ctrl-Shift-Backspace delete_to_start_of_line

Ctrl-E word_completion_next
Ctrl-Q word_completion_previous
Ctrl-R word_completion_subword

Alt-ArrowUp move_selected_lines_up
Alt-ArrowDown move_selected_lines_down

Expand Down
69 changes: 69 additions & 0 deletions src/draw.jai
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,75 @@ draw_editor :: (editor_id: s64, main_area: Rect, status_bar_height: float, ui_id
}
}

// Draw word completion
if editor_is_active && word_complete.results.count {

word_complete.max_visible_item_at_once = word_complete.DEFAULT_MAX_VISIBLE_ITEM;
max_wc_item: s64 = min(word_complete.results.count, word_complete.max_visible_item_at_once);

padding := floor(10 * dpi_scale);
item_delta_scaler := (line_height + 5) * dpi_scale;
cursor_screen_pos := get_cursor_screen_pos(text_origin, cursor_coords[0].pos);

max_char := 0;
for word_complete.results { if it.word.count > max_char max_char = it.word.count; }

rect_w, rect_h, rect_left, rect_bottom: float = 0;
calc_rect :: () #expand {
rect_w = (max_char * char_x_advance) + (padding * 2);
rect_h = max_wc_item * (item_delta_scaler) + padding;
rect_bottom = cursor_screen_pos.y - rect_h;
rect_left = cursor_screen_pos.x + padding*2;

if rect_bottom < line_number_panel.h {
rect_bottom = line_number_panel.h + line_height;
}
if rect_left + rect_w + padding > main_area.w + main_area.x {
rect_left = (main_area.w + main_area.x) - (rect_w + padding);
}
}
calc_rect();

rect_top := rect_bottom + rect_h;
if (rect_top > main_area.h) {
// Reduce the max visible results if there is no more space to expand at the top
usable_h := main_area.h - line_number_panel.h;
max_wc_item = cast(s64)(usable_h / item_delta_scaler / 2);
word_complete.max_visible_item_at_once = max_wc_item;
calc_rect();
}

rect := make_rect(rect_left, rect_bottom, rect_w, rect_h);
background_dark := Color.BACKGROUND_1;
draw_rounded_rect(rect, background_dark, radius = rounding_radius_large, set_shader = true);

i := 0;
for word_complete.scroll_top..max_wc_item + word_complete.scroll_top - 1 {
if it == word_complete.results.count then break;
item_left := rect_left + padding;
item_top := (rect_bottom + rect_h) - (item_delta_scaler * (i + 1));
item_color := Token_Type.default;
if (it % word_complete.results.count) == word_complete.selected_result_index {
item_color = Token_Type.warning;
}

Simp.prepare_text(font, word_complete.results[it].word);
Simp.draw_prepared_text(font, cast(s64) item_left, cast(s64) item_top, xx item_color);

i+=1;
}

if (word_complete.scroll_top) {
Simp.prepare_text(font_ui_small, BULLET_ICON);
Simp.draw_prepared_text(font_ui_small, xx (rect_left + (rect_w/2)), xx (rect_bottom+rect_h-line_height/2), color = xx Color.UI_DEFAULT);
}

if (word_complete.results.count > word_complete.scroll_top + max_wc_item) {
Simp.prepare_text(font_ui_small, BULLET_ICON);
Simp.draw_prepared_text(font_ui_small, xx (rect_left + (rect_w/2)), xx rect_bottom, color = xx Color.UI_DEFAULT);
}
}

// Draw search bar
if active_global_widget != .editors then search_bar.active = false;

Expand Down
208 changes: 208 additions & 0 deletions src/editors.jai
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ active_editor_handle_event :: (editor: *Editor, buffer: *Buffer, event: Input.Ev
keep_selection := false;
enabled_whole_words := false;

word_complete.should_abort = true;
defer { if word_complete.should_abort && word_complete.active then word_complete_abort(); }

handled := true;

if action == {
Expand Down Expand Up @@ -287,6 +290,10 @@ active_editor_handle_event :: (editor: *Editor, buffer: *Buffer, event: Input.Ev
case .delete_to_start_of_line; delete_to_start_of_line (editor, buffer);
case .delete_to_end_of_line; delete_to_end_of_line (editor, buffer);

case .word_completion_next; word_completion();
case .word_completion_previous; word_completion(forwards = false);
case .word_completion_subword; word_completion(subwords = true);

case; return false;
}
}
Expand Down Expand Up @@ -3417,8 +3424,209 @@ copy_current_line_info :: (using editor: *Editor, buffer: Buffer) {
add_success_message("String '%' copied to clipboard", line_info, dismiss_in_seconds = 5);
}

word_completion :: (forwards := true, subwords := false) {
using word_complete;
should_abort = false;

if !active {
search_for_subwords = subwords;

context.underscore_is_part_of_word = !subwords;
word_complete_search();
context.underscore_is_part_of_word = true;

if results.count == 0 then return;
active = true;
}

if forwards word_completion_next();
else word_completion_previous();
}

word_completion_next :: () {
using word_complete;

selected_result_index = (selected_result_index + 1) % results.count;

start := scroll_top;
end := start + max_visible_item_at_once;

if selected_result_index+1 > end {
scroll_top += 1;
} else if selected_result_index == 0 {
scroll_top = 0;
}

word_complete_apply();
}

word_completion_previous :: () {
using word_complete;

if selected_result_index - 1 < 0 {
selected_result_index = results.count-1;
if selected_result_index - (max_visible_item_at_once-1) > -1 {
scroll_top = selected_result_index - (max_visible_item_at_once-1);
}
} else {
selected_result_index -= 1;
if selected_result_index < scroll_top {
scroll_top -= 1;
}
}

word_complete_apply();
}

word_complete_abort :: () {
using word_complete;
active = false;
scroll_top = 0;
if !results.count return;
for results { free(it.word); }
array_reset(*results);
selected_result_index = -1;
}

word_complete_search :: () {
// hmm...
UNDERSCORE_PENALTY :: 1000000000;
CASE_SENSITIVE_PENALTY :: 100000000;

editor, buffer := get_active_editor_and_buffer();

cursor := *editor.cursors[0];
right := cursor.pos;
left := scan_through_similar_chars_on_the_left(buffer, cursor.pos, Char_Type.word, skip_one_space = false);
start_subword := get_range_as_string(buffer, Offset_Range.{left, right});
if start_subword.count == 0 then return;

buffer_str := cast(string)buffer.bytes;

add_new_result_or_update_delta :: inline (new_result: string, delta: s64) {
if word_complete.search_for_subwords {
for 0..new_result.count-1 {
if new_result[it] == #char "_" {
delta += UNDERSCORE_PENALTY;
break;
}
}
}

for * word_complete.results {
if it.word == new_result {
// found a closer one
if delta < it.cursor_delta then it.cursor_delta = xx delta;
return;
}
}

result := Word_Result.{word = new_result, cursor_delta = xx delta};
array_add(*word_complete.results, result);
}

pos := 0;
while pos != buffer_str.count-1 {
pos = find_index_from_left_nocase(buffer_str, start_subword, pos);
if pos == -1 break;

defer pos += start_subword.count;
if pos == left || (pos > 0 && is_word_char(buffer_str[pos-1])) {
// Exclude results that are part of another word. For instance, if your subword is 'ine' and you have a word like 'wine' elsewhere, it will not include the result 'ine' from 'w[ine]'.
continue;
}

for i: pos..buffer_str.count-1 {
c := buffer_str[i];
if !is_word_char(c) || i == buffer_str.count-1 {
is_subword := c == #char "_";

s := advance(buffer_str, pos);
s.count = i - pos;
if s != start_subword {
// Now, we're going left to right, so the distance between the word and the cursor will
// always be closer than in right-to-left searching; thus, we need to apply some corrections to it.
cursor_delta := cursor.pos - pos;
if pos >= right then cursor_delta = i - cursor.pos;
if is_upper(start_subword[0]) != is_upper(buffer_str[pos]) then cursor_delta += CASE_SENSITIVE_PENALTY;
add_new_result_or_update_delta(s, cursor_delta);
}

if !is_subword then break;
}
}

}

if !word_complete.results.count then return;

bubble_sort(word_complete.results, (a, b) => cast(s64) ((a.cursor_delta) - (b.cursor_delta)));

add_new_result_or_update_delta(start_subword, delta = 0);
for * word_complete.results { it.word = copy_string(it.word); }

return;
}

word_complete_apply :: () {
using word_complete;

if !results || !open_editors[editors.active].cursors.count return;
editor, buffer := get_active_editor_and_buffer();

result := results[selected_result_index];
word := result.word;

offset_delta: s32 = 0;
buf_len := cast(s32) buffer.bytes.count;

for *cursor: editor.cursors {
left := scan_through_similar_chars_on_the_left(buffer, cursor.pos, Char_Type.word, skip_one_space = false);
cursor.sel = left;
}

for *cursor: editor.cursors {
new_len := cast(s32) buffer.bytes.count;
if new_len != buf_len {
offset_delta += new_len - buf_len;
cursor.pos += offset_delta;
cursor.sel += offset_delta;
buf_len = new_len;
}

range := get_selection(cursor);
replace_range(buffer, range, word);

end := cast(s32)(range.start+word.count);
add_paste_animation(editor, Offset_Range.{range.start, end});

cursor.pos = end;
cursor.sel = end;
}
}

#scope_export

Word_Completion :: struct {
DEFAULT_MAX_VISIBLE_ITEM :: 3;
MAX_TOTAL_RESULT :: 30;

active := false;
should_abort := false;
selected_result_index := -1;
search_for_subwords := false;
results: [..] Word_Result;
scroll_top := 0;
max_visible_item_at_once := DEFAULT_MAX_VISIBLE_ITEM;
}

Word_Result :: struct {
word: string;
cursor_delta: s32;
}

word_complete: Word_Completion;

Editor :: struct {
buffer_id: s64;

Expand Down
4 changes: 4 additions & 0 deletions src/keymap.jai
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ ACTIONS_EDITORS :: #run arrays_concat(ACTIONS_COMMON, string.[
"toggle_line_wrap",
"toggle_line_numbers",
"copy_current_line_info",

"word_completion_next",
"word_completion_previous",
"word_completion_subword",
]);

ACTIONS_OPEN_FILE_DIALOG :: #run arrays_concat(ACTIONS_COMMON, string.[
Expand Down
22 changes: 22 additions & 0 deletions src/utils.jai
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,23 @@ find_index_from_left_nocase :: (s: string, substring: string, start_index := 0)
return -1;
}


find_index_from_right_nocase :: (s: string, substring: string, start_index := 0) -> s64 {
if !substring return -1;
x := s;
if start_index {
if start_index > s.count return -1; // assert?
x.count = start_index;
}

for < i: x.count-substring.count..0 {
t := slice(x, i, substring.count);
if equal_nocase(t, substring) return i;
}

return -1;
}

match_whole_word :: (s: string, offset: int, count: int) -> bool {
word_ends_on_the_left := offset <= 0 || !is_word_char(s[offset - 1]) || !is_word_char(s[offset]);
word_ends_on_the_right := offset + count >= s.count || !is_word_char(s[offset + count]) || !is_word_char(s[offset + count - 1]);
Expand Down Expand Up @@ -362,6 +379,11 @@ is_all_whitespace :: (s: string) -> bool {
return true;
}

is_upper :: inline (byte: u8) -> bool {
if byte >= #char "A" && byte <= #char "Z" return true;
return false;
}

count_whitespace :: (bytes: [] u8, start_offset: s64, max_offset: s64, spaces := " \t") -> count: s32 {
subarray := array_view(bytes, start_offset, max_offset - start_offset);
for subarray {
Expand Down