diff --git a/src/bin/edit/draw_filepicker.rs b/src/bin/edit/draw_filepicker.rs index ffd30f12ebb..ca91e6ce262 100644 --- a/src/bin/edit/draw_filepicker.rs +++ b/src/bin/edit/draw_filepicker.rs @@ -58,8 +58,72 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) { ctx.inherit_focus(); ctx.label("name-label", loc(LocId::SaveAsDialogNameLabel)); - ctx.editline("name", &mut state.file_picker_pending_name); + + let name_changed = ctx.editline("name", &mut state.file_picker_pending_name); ctx.inherit_focus(); + + if ctx.contains_focus() { + if name_changed && ctx.is_focused() { + update_autocomplete_suggestions(state); + } + } else if !state.file_picker_autocomplete.is_empty() { + state.file_picker_autocomplete.clear(); + } + + if !state.file_picker_autocomplete.is_empty() { + let bg = ctx.indexed_alpha(IndexedColor::Background, 3, 4); + let fg = ctx.contrasted(bg); + let focus_list_beg = ctx.is_focused() && ctx.consume_shortcut(vk::DOWN); + let focus_list_end = ctx.is_focused() && ctx.consume_shortcut(vk::UP); + let mut autocomplete_done = ctx.consume_shortcut(vk::ESCAPE); + + ctx.list_begin("suggestions"); + ctx.attr_float(FloatSpec { + anchor: Anchor::Last, + gravity_x: 0.0, + gravity_y: 0.0, + offset_x: 0.0, + offset_y: 1.0, + }); + ctx.attr_border(); + ctx.attr_background_rgba(bg); + ctx.attr_foreground_rgba(fg); + { + for (idx, suggestion) in state.file_picker_autocomplete.iter().enumerate() { + let sel = ctx.list_item(false, suggestion.as_str()); + if sel != ListSelection::Unchanged { + state.file_picker_pending_name = suggestion.as_path().into(); + } + if sel == ListSelection::Activated { + autocomplete_done = true; + } + + let is_first = idx == 0; + let is_last = idx == state.file_picker_autocomplete.len() - 1; + if (is_first && focus_list_beg) || (is_last && focus_list_end) { + ctx.list_item_steal_focus(); + } else if ctx.is_focused() + && ((is_first && ctx.consume_shortcut(vk::UP)) + || (is_last && ctx.consume_shortcut(vk::DOWN))) + { + ctx.toss_focus_up(); + } + } + } + ctx.list_end(); + + // If the user typed something, we want to put focus back into the editline. + // TODO: The input should be processed by the editline and not simply get swallowed. + if ctx.keyboard_input().is_some() { + ctx.set_input_consumed(); + autocomplete_done = true; + } + + if autocomplete_done { + state.file_picker_autocomplete.clear(); + } + } + if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) { activated = true; } @@ -191,6 +255,7 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) { state.file_picker_pending_name = Default::default(); state.file_picker_entries = Default::default(); state.file_picker_overwrite_warning = Default::default(); + state.file_picker_autocomplete = Default::default(); } } @@ -281,3 +346,34 @@ fn draw_dialog_saveas_refresh_files(state: &mut State) { state.file_picker_entries = Some(files); } + +fn update_autocomplete_suggestions(state: &mut State) { + state.file_picker_autocomplete.clear(); + + if state.file_picker_pending_name.as_os_str().is_empty() { + return; + } + + let needle = state.file_picker_pending_name.as_os_str().as_encoded_bytes(); + let mut matches = Vec::new(); + + if let Some(entries) = &state.file_picker_entries { + // Remove the first entry, which is always "..". + for entry in &entries[1.min(entries.len())..] { + let haystack = entry.as_bytes(); + // We only want items that are longer than the needle, + // because we're looking for suggestions, not for matches. + if haystack.len() > needle.len() + && let haystack = &haystack[..needle.len()] + && icu::compare_strings(haystack, needle) == Ordering::Equal + { + matches.push(entry.clone()); + if matches.len() >= 5 { + break; // Limit to 5 suggestions + } + } + } + } + + state.file_picker_autocomplete = matches; +} diff --git a/src/bin/edit/state.rs b/src/bin/edit/state.rs index 90689e59a6d..354077392c6 100644 --- a/src/bin/edit/state.rs +++ b/src/bin/edit/state.rs @@ -137,6 +137,7 @@ pub struct State { pub file_picker_pending_name: PathBuf, pub file_picker_entries: Option>, pub file_picker_overwrite_warning: Option, // The path the warning is about. + pub file_picker_autocomplete: Vec, pub wants_search: StateSearch, pub search_needle: String, @@ -182,6 +183,7 @@ impl State { file_picker_pending_name: Default::default(), file_picker_entries: None, file_picker_overwrite_warning: None, + file_picker_autocomplete: Vec::new(), wants_search: StateSearch { kind: StateSearchKind::Hidden, focus: false }, search_needle: Default::default(), diff --git a/src/tui.rs b/src/tui.rs index 87d2b4a28c6..f9454b291f4 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2511,6 +2511,9 @@ impl<'a> Context<'a, '_> { } } vk::UP => { + if single_line { + return false; + } match modifiers { kbmod::NONE => { let mut x = tc.preferred_column; @@ -2568,57 +2571,62 @@ impl<'a> Context<'a, '_> { tb.cursor_move_delta(granularity, 1); } } - vk::DOWN => match modifiers { - kbmod::NONE => { - let mut x = tc.preferred_column; - let mut y = tb.cursor_visual_pos().y + 1; - - // If there's a selection we put the cursor below it. - if let Some((_, end)) = tb.selection_range() { - x = end.visual_pos.x; - y = end.visual_pos.y + 1; - tc.preferred_column = x; - } + vk::DOWN => { + if single_line { + return false; + } + match modifiers { + kbmod::NONE => { + let mut x = tc.preferred_column; + let mut y = tb.cursor_visual_pos().y + 1; - // If the cursor was already on the last line, - // move it to the end of the buffer. - if y >= tb.visual_line_count() { - x = CoordType::MAX; - } + // If there's a selection we put the cursor below it. + if let Some((_, end)) = tb.selection_range() { + x = end.visual_pos.x; + y = end.visual_pos.y + 1; + tc.preferred_column = x; + } + + // If the cursor was already on the last line, + // move it to the end of the buffer. + if y >= tb.visual_line_count() { + x = CoordType::MAX; + } - tb.cursor_move_to_visual(Point { x, y }); + tb.cursor_move_to_visual(Point { x, y }); - // If we fell into the `if y >= tb.get_visual_line_count()` above, we wanted to - // update the `preferred_column` but didn't know yet what it was. Now we know! - if x == CoordType::MAX { - tc.preferred_column = tb.cursor_visual_pos().x; + // If we fell into the `if y >= tb.get_visual_line_count()` above, we wanted to + // update the `preferred_column` but didn't know yet what it was. Now we know! + if x == CoordType::MAX { + tc.preferred_column = tb.cursor_visual_pos().x; + } } - } - kbmod::CTRL => { - tc.scroll_offset.y += 1; - make_cursor_visible = false; - } - kbmod::SHIFT => { - // If the cursor was already on the last line, - // move it to the end of the buffer. - if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 { - tc.preferred_column = CoordType::MAX; + kbmod::CTRL => { + tc.scroll_offset.y += 1; + make_cursor_visible = false; } + kbmod::SHIFT => { + // If the cursor was already on the last line, + // move it to the end of the buffer. + if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 { + tc.preferred_column = CoordType::MAX; + } - tb.selection_update_visual(Point { - x: tc.preferred_column, - y: tb.cursor_visual_pos().y + 1, - }); + tb.selection_update_visual(Point { + x: tc.preferred_column, + y: tb.cursor_visual_pos().y + 1, + }); - if tc.preferred_column == CoordType::MAX { - tc.preferred_column = tb.cursor_visual_pos().x; + if tc.preferred_column == CoordType::MAX { + tc.preferred_column = tb.cursor_visual_pos().x; + } } + kbmod::CTRL_ALT => { + // TODO: Add cursor above + } + _ => return false, } - kbmod::CTRL_ALT => { - // TODO: Add cursor above - } - _ => return false, - }, + } vk::INSERT => match modifiers { kbmod::SHIFT => { write = &self.tui.clipboard; @@ -2968,6 +2976,23 @@ impl<'a> Context<'a, '_> { } } + /// [`Context::steal_focus`], but for a list view. + /// + /// This exists, because didn't want to figure out how to get + /// [`Context::styled_list_item_end`] to recognize a regular, + /// programmatic focus steal. + pub fn list_item_steal_focus(&mut self) { + self.steal_focus(); + + match &mut self.tree.current_node.borrow_mut().content { + NodeContent::List(content) => { + content.selected = self.tree.last_node.borrow().id; + content.selected_node = Some(self.tree.last_node); + } + _ => unreachable!(), + } + } + /// Ends the current list block. pub fn list_end(&mut self) { self.block_end();