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

Support command for jumping to word location based on key #10218

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::parser::Parser;
use crate::proc::job_reap;
use crate::reader::{
reader_reading_interrupted, reader_reset_interrupted, reader_schedule_prompt_repaint,
reader_set_show_overlay_state,
};
use crate::signal::signal_clear_cancel;
#[cfg(test)]
Expand Down Expand Up @@ -173,6 +174,7 @@ const INPUT_FUNCTION_METADATA: &[InputFunctionMetadata] = &[
make_md(L!("kill-selection"), ReadlineCmd::KillSelection),
make_md(L!("kill-whole-line"), ReadlineCmd::KillWholeLine),
make_md(L!("kill-word"), ReadlineCmd::KillWord),
make_md(L!("move-jump-anchor"), ReadlineCmd::MoveJumpAnchor),
make_md(L!("nextd-or-forward-word"), ReadlineCmd::NextdOrForwardWord),
make_md(L!("or"), ReadlineCmd::FuncOr),
make_md(L!("pager-toggle-search"), ReadlineCmd::PagerToggleSearch),
Expand Down Expand Up @@ -264,11 +266,19 @@ fn input_function_arity(function: ReadlineCmd) -> usize {
ReadlineCmd::ForwardJump
| ReadlineCmd::BackwardJump
| ReadlineCmd::ForwardJumpTill
| ReadlineCmd::BackwardJumpTill => 1,
| ReadlineCmd::BackwardJumpTill
| ReadlineCmd::MoveJumpAnchor => 1,
_ => 0,
}
}

fn input_function_show_overlay(function: ReadlineCmd) -> bool {
match function {
ReadlineCmd::MoveJumpAnchor => true,
_ => false,
}
}

/// Inserts an input mapping at the correct position. We sort them in descending order by length, so
/// that we test longer sequences first.
fn input_mapping_insert_sorted(ml: &mut Vec<InputMapping>, new_mapping: InputMapping) {
Expand Down Expand Up @@ -468,6 +478,9 @@ impl Inputter {
"event_storage should be empty"
);
let mut skipped = std::mem::take(&mut self.event_storage);
if arity > 0 && input_function_show_overlay(code) {
reader_set_show_overlay_state(true);
}
for _ in 0..arity {
// Skip and queue up any function codes. See issue #2357.
let arg: char;
Expand Down
1 change: 1 addition & 0 deletions src/input_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pub enum ReadlineCmd {
DisableMouseTracking,
// ncurses uses the obvious name
ClearScreenAndRepaint,
MoveJumpAnchor,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is definitely a problem we should consider addressing.

The classic use case is if you want to edit NEEDLE after pasting this command:

echo '/some/really/long/path/name with spaces/aaa/bbb/ccc' NEEDLE 'other token with spaces' a/b/c/

Today I usually fall back on Alt+e to open my editor.

Possible alternative approaches are

  • use Shift+Left and Shift+Right which are bound to forward-bigword and backward-bigword by default -- I didn't realize that until today
  • allow searching within the commandline (Ctrl-S would be the obvious Emacs-binding)
  • allow moving by full tokens (the Alt+Shift+b binding will be open once we support CSI u)
  • add commands that move by $COLUMNS/2 or similar

Your approach sounds reasonable as well and the UI is fairly well known and obvious.
Do you have an example use case?

I briefly tried it and it has some glitches on multiline commands (when using \co twice) and the contrast is bad (light theme).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for having a look.

I tried using it on multiline commands, and it does not seem to behave in any unexpected way to me, even when pressing ctrl+o twice, however i do agree that the behaviour probably is undesirable, especially replacing the newlines with jump letters, and thus temporarily altering the line breaking.

There are also some challenges with regards to the fact that it skips adding jump characters if a word is too short leading to inconsistent marking of characters depending on the location of the cursor.

I haven't really used this feature much in the context of fish yet, so I am not sure how well it fits for command line editing except for jumping in paths as i mentioned, but as I wrote i find it useful in the context of a programming text editor; for example when jumping in a line of code that consists of an an if-test with lots of brackets, quotes etc.

When it comes to the highlighting colour I agree that the contrast is poor, i just chose the first sensible colour enumeration in the theme I could find. We can change it to something else, or make a completely new color category.

// NOTE: This one has to be last.
ReverseRepeatJump,
}
Expand Down
185 changes: 184 additions & 1 deletion src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ pub struct ReaderData {
/// If these differs from the text of the command line, then we must kick off a new request.
in_flight_highlight_request: WString,
in_flight_autosuggest_request: WString,

move_locations_displayed: bool,
}

/// Read commands from \c fd until encountering EOF.
Expand Down Expand Up @@ -855,6 +857,13 @@ pub fn reader_handle_command(cmd: ReadlineCmd) {
}
}

pub fn reader_set_show_overlay_state(visible: bool) {
if let Some(data) = current_data() {
data.move_locations_displayed = visible;
data.layout_and_repaint(L!(" readline show move jump anchor"));
}
}

/// Enqueue an event to the back of the reader's input queue.
pub fn reader_queue_ch(ch: CharEvent) {
if let Some(data) = current_data() {
Expand Down Expand Up @@ -1041,6 +1050,7 @@ impl ReaderData {
last_jump_precision: JumpPrecision::To,
in_flight_highlight_request: Default::default(),
in_flight_autosuggest_request: Default::default(),
move_locations_displayed: Default::default(),
}))
}

Expand Down Expand Up @@ -1219,6 +1229,71 @@ pub fn combine_command_and_autosuggestion(cmdline: &wstr, autosuggestion: &wstr)
full_line
}

fn line_move_locations_inner<F, I>(
base: &wstr,
char_iter: I,
jump_anchors: &wstr,
mut record_location: F,
) where
F: FnMut(usize, char),
I: Iterator<Item = char>,
{
let mut alphanumeric_state = base.char_at(0).is_alphanumeric();
let mut anchor_idx = 0;
let mut jump_before_iter = jump_anchors.chars();
let mut last_location: Option<usize> = None;
for (cidx, char) in char_iter.enumerate() {
if char.is_alphanumeric() != alphanumeric_state {
alphanumeric_state = char.is_alphanumeric();
//log::info!("State change x:{}", cidx);
/* Skip adjacent locations, is there a way to do this without
being unstable when moving the cursor */
if last_location
.map(|ll| cidx.saturating_sub(ll) > 1)
.unwrap_or(true)
&& cidx > 0
{
/* Show jump anchor at every transition between alphanumeric an non alphanumeric text */
if jump_anchors.len() > anchor_idx {
if let Some(jump_anchor) = jump_before_iter.next() {
last_location = Some(cidx);
// render char instead
record_location(cidx, jump_anchor);
anchor_idx += 1;
}
}
}
}
}
}
pub fn line_move_locations<F>(
base: &wstr,
jump_anchors: &wstr,
cursor: usize,
after: bool,
record_location: F,
) where
F: FnMut(usize, char),
{
if jump_anchors.is_empty() {
return;
}
/* TODO: Ideally we should use grapheme based iteration.
Currently only unicode codepoint based iteration is used */
/*let (before_cursor, after_cursor) =
base.split_at((cursor + if after { 0 } else { 1 }).min(base.len()));*/
if after {
if cursor < base.len() {
let after_cursor = &base[cursor..];
let char_iter = after_cursor.chars();
line_move_locations_inner(base, char_iter, jump_anchors, record_location)
}
} else {
let before_cursor = &base[0..(cursor + 1).min(base.len())];
let char_iter = before_cursor.chars().rev();
line_move_locations_inner(base, char_iter, jump_anchors, record_location)
};
}
impl ReaderData {
/// \return true if the command line has changed and repainting is needed. If \p colors is not
/// null, then also return true if the colors have changed.
Expand Down Expand Up @@ -1300,6 +1375,99 @@ impl ReaderData {
self.rendered_layout = self.make_layout_data();
self.paint_layout(reason);
}
fn move_location_perfom(&mut self, target: char, elt: EditableLineTag) {
let jump_anchors_before = self
.vars()
.get(L!("fish_jump_anchors_before"))
.map(|ev| ev.as_string())
.unwrap_or_default();
let jump_anchors_after = self
.vars()
.get(L!("fish_jump_anchors_after"))
.map(|ev| ev.as_string())
.unwrap_or_default();

let basis_text = self.command_line.text();
let current_position = self.command_line.position();

let mut target_pos = None;
// TODO: Adjust target pos depending on if to or till mode is enabled
if jump_anchors_before.contains(target) {
line_move_locations(
&basis_text,
&jump_anchors_before,
current_position,
false,
|pos, jump_char| {
let abs_pos = current_position.saturating_sub(pos);
if jump_char == target {
target_pos = Some(abs_pos);
}
},
);
} else if jump_anchors_after.contains(target) {
line_move_locations(
&basis_text,
&jump_anchors_after,
current_position,
true,
|pos, jump_char| {
let abs_pos = current_position + pos;
if jump_char == target {
target_pos = Some(abs_pos);
}
},
);
} else {
/* Target character not found, do nothing */
}
if let Some(target_pos) = target_pos {
self.update_buff_pos(elt, Some(target_pos));
}
self.move_locations_displayed = false;
}
fn apply_overlays(&self, basis: WString, highlight: &mut Vec<HighlightSpec>) -> WString {
let current_position = self.command_line.position();
// TODO: Figure out a way to do string manipulation in an unicode safe manner
let mut overlayed = basis.clone().into_vec();
let jump_anchors_before = self
.vars()
.get(L!("fish_jump_anchors_before"))
.map(|ev| ev.as_string())
.unwrap_or_default();
let jump_anchors_after = self
.vars()
.get(L!("fish_jump_anchors_after"))
.map(|ev| ev.as_string())
.unwrap_or_default();
line_move_locations(
&basis,
&jump_anchors_before,
current_position,
false,
|pos, jump_char| {
let abs_pos = current_position.saturating_sub(pos);
overlayed.get_mut(abs_pos).map(|c| *c = jump_char as u32);
highlight
.get_mut(abs_pos)
.map(|h| h.background = HighlightRole::search_match);
},
);
line_move_locations(
&basis,
&jump_anchors_after,
current_position,
true,
|pos, jump_char| {
let abs_pos = current_position + pos;
overlayed.get_mut(abs_pos).map(|c| *c = jump_char as u32);
highlight
.get_mut(abs_pos)
.map(|h| h.background = HighlightRole::search_match);
},
);
WString::from_vec(overlayed).unwrap()
}

/// Paint the last rendered layout.
/// \p reason is used in FLOG to explain why.
Expand All @@ -1308,15 +1476,24 @@ impl ReaderData {
let data = &self.rendered_layout;
let cmd_line = &self.command_line;

let mut colors = data.colors.clone();

let full_line = if self.conf.in_silent_mode {
wstr::from_char_slice(&[get_obfuscation_read_char()]).repeat(cmd_line.len())
} else if self.move_locations_displayed {
let mut command_line_string = WString::new();
command_line_string.push_utfstr(cmd_line.text());
// Combine the command and autosuggestion into one string.
combine_command_and_autosuggestion(
&self.apply_overlays(command_line_string, &mut colors),
&self.autosuggestion.text,
)
} else {
// Combine the command and autosuggestion into one string.
combine_command_and_autosuggestion(cmd_line.text(), &self.autosuggestion.text)
};

// Copy the colors and extend them with autosuggestion color.
let mut colors = data.colors.clone();

// Highlight any history search.
if !self.conf.in_silent_mode && data.history_search_range.is_some() {
Expand Down Expand Up @@ -2989,6 +3166,12 @@ impl ReaderData {
rl::SelfInsert | rl::SelfInsertNotFirst | rl::FuncAnd | rl::FuncOr => {
panic!("should have been handled by inputter_t::readch");
}
ReadlineCmd::MoveJumpAnchor => {
let target = self.inputter.function_pop_arg();
self.move_locations_displayed = false;
let (elt, _el) = self.active_edit_line();
self.move_location_perfom(target, elt);
}
}
}
}
Expand Down