Skip to content
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