mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-22 13:37:27 +08:00
Port pager.cpp
This commit is contained in:
parent
f7b5ebc79f
commit
43e2d7b48c
|
@ -121,7 +121,6 @@ set(FISH_SRCS
|
|||
src/input_common.cpp
|
||||
src/input.cpp
|
||||
src/output.cpp
|
||||
src/pager.cpp
|
||||
src/parse_util.cpp
|
||||
src/path.cpp
|
||||
src/reader.cpp
|
||||
|
|
|
@ -91,6 +91,7 @@ fn main() {
|
|||
"fish-rust/src/kill.rs",
|
||||
"fish-rust/src/operation_context.rs",
|
||||
"fish-rust/src/output.rs",
|
||||
"fish-rust/src/pager.rs",
|
||||
"fish-rust/src/parse_constants.rs",
|
||||
"fish-rust/src/parser.rs",
|
||||
"fish-rust/src/parse_tree.rs",
|
||||
|
|
|
@ -2639,6 +2639,10 @@ unsafe impl cxx::ExternType for CompletionListFfi {
|
|||
type Id = cxx::type_id!("CompletionListFfi");
|
||||
type Kind = cxx::kind::Opaque;
|
||||
}
|
||||
unsafe impl cxx::ExternType for Completion {
|
||||
type Id = cxx::type_id!("Completion");
|
||||
type Kind = cxx::kind::Opaque;
|
||||
}
|
||||
|
||||
fn new_completion() -> Box<Completion> {
|
||||
Box::new(Completion::new(
|
||||
|
|
|
@ -105,11 +105,6 @@ include_cpp! {
|
|||
generate!("commandline_get_state_text_ffi")
|
||||
generate!("completion_apply_to_command_line")
|
||||
|
||||
generate!("pager_t")
|
||||
generate!("page_rendering_t")
|
||||
generate!("pager_set_term_size_ffi")
|
||||
generate!("pager_update_rendering_ffi")
|
||||
|
||||
generate!("get_history_variable_text_ffi")
|
||||
|
||||
generate_pod!("escape_string_style_t")
|
||||
|
@ -173,8 +168,6 @@ impl Repin for IoStreams<'_> {}
|
|||
impl Repin for wcstring_list_ffi_t {}
|
||||
impl Repin for rgb_color_t {}
|
||||
impl Repin for OutputStreamFfi<'_> {}
|
||||
impl Repin for pager_t {}
|
||||
impl Repin for page_rendering_t {}
|
||||
|
||||
pub use autocxx::c_int;
|
||||
pub use ffi::*;
|
||||
|
|
|
@ -1635,6 +1635,7 @@ impl Default for HighlightSpec {
|
|||
mod highlight_ffi {
|
||||
/// Describes the role of a span of text.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum HighlightRole {
|
||||
normal, // normal text
|
||||
error, // error
|
||||
|
|
|
@ -74,6 +74,7 @@ mod nix;
|
|||
mod null_terminated_array;
|
||||
mod operation_context;
|
||||
mod output;
|
||||
mod pager;
|
||||
mod parse_constants;
|
||||
mod parse_execution;
|
||||
mod parse_tree;
|
||||
|
|
1366
fish-rust/src/pager.rs
Normal file
1366
fish-rust/src/pager.rs
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -7,9 +7,11 @@
|
|||
//! The current implementation is less smart than ncurses allows and can not for example move blocks
|
||||
//! of text around to handle text insertion.
|
||||
|
||||
use crate::pager::{PageRendering, Pager};
|
||||
use std::collections::LinkedList;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
|
@ -24,7 +26,6 @@ use crate::common::{
|
|||
use crate::curses::{term, tparm0, tparm1};
|
||||
use crate::env::{EnvStackRef, Environment, TERM_HAS_XN};
|
||||
use crate::fallback::fish_wcwidth;
|
||||
use crate::ffi::{self, Repin};
|
||||
use crate::flog::FLOGF;
|
||||
use crate::future::IsSomeAnd;
|
||||
use crate::global_safety::RelaxedAtomicBool;
|
||||
|
@ -35,7 +36,6 @@ use crate::wchar::prelude::*;
|
|||
use crate::wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI};
|
||||
use crate::wcstringutil::string_prefixes_string;
|
||||
use crate::{highlight::HighlightSpec, wcstringutil::fish_wcwidth_visible};
|
||||
use std::pin::Pin;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HighlightedChar {
|
||||
|
@ -53,7 +53,7 @@ pub struct Line {
|
|||
}
|
||||
|
||||
impl Line {
|
||||
fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
|
@ -148,10 +148,11 @@ impl ScreenData {
|
|||
self.line_datas.resize(size, Default::default())
|
||||
}
|
||||
|
||||
pub fn create_line(&mut self, idx: usize) {
|
||||
pub fn create_line(&mut self, idx: usize) -> &mut Line {
|
||||
if idx >= self.line_datas.len() {
|
||||
self.line_datas.resize(idx + 1, Default::default())
|
||||
}
|
||||
self.line_mut(idx)
|
||||
}
|
||||
|
||||
pub fn insert_line_at_index(&mut self, idx: usize) -> &mut Line {
|
||||
|
@ -256,8 +257,8 @@ impl Screen {
|
|||
indent: &[usize],
|
||||
cursor_pos: usize,
|
||||
vars: &dyn Environment,
|
||||
pager: Pin<&mut ffi::pager_t>,
|
||||
page_rendering: Pin<&mut ffi::page_rendering_t>,
|
||||
pager: &mut Pager,
|
||||
page_rendering: &mut PageRendering,
|
||||
cursor_is_within_pager: bool,
|
||||
) {
|
||||
let curr_termsize = termsize_last();
|
||||
|
@ -362,25 +363,19 @@ impl Screen {
|
|||
|
||||
// Re-render our completions page if necessary. Limit the term size of the pager to the true
|
||||
// term size, minus the number of lines consumed by our string.
|
||||
let pager = pager.unpin();
|
||||
let page_rendering = page_rendering.unpin();
|
||||
crate::ffi::pager_set_term_size_ffi(
|
||||
pager.pin(),
|
||||
&Termsize::new(
|
||||
std::cmp::max(1, curr_termsize.width),
|
||||
std::cmp::max(
|
||||
1,
|
||||
curr_termsize
|
||||
.height
|
||||
.saturating_sub_unsigned(full_line_count),
|
||||
),
|
||||
) as *const Termsize as *const autocxx::c_void,
|
||||
);
|
||||
pager.set_term_size(&Termsize::new(
|
||||
std::cmp::max(1, curr_termsize.width),
|
||||
std::cmp::max(
|
||||
1,
|
||||
curr_termsize
|
||||
.height
|
||||
.saturating_sub_unsigned(full_line_count),
|
||||
),
|
||||
));
|
||||
|
||||
crate::ffi::pager_update_rendering_ffi(pager.pin(), page_rendering.pin());
|
||||
pager.update_rendering(page_rendering);
|
||||
// Append pager_data (none if empty).
|
||||
self.desired
|
||||
.append_lines(unsafe { &*(page_rendering.screen_data_ffi() as *const ScreenData) });
|
||||
self.desired.append_lines(&page_rendering.screen_data);
|
||||
|
||||
self.update(&layout.left_prompt, &layout.right_prompt, vars);
|
||||
self.save_status();
|
||||
|
@ -1848,11 +1843,14 @@ fn compute_layout(
|
|||
mod screen_ffi {
|
||||
extern "C++" {
|
||||
include!("screen.h");
|
||||
include!("pager.h");
|
||||
include!("highlight.h");
|
||||
pub type HighlightSpec = crate::highlight::HighlightSpec;
|
||||
pub type HighlightSpecListFFI = crate::highlight::HighlightSpecListFFI;
|
||||
pub type pager_t = crate::ffi::pager_t;
|
||||
pub type page_rendering_t = crate::ffi::page_rendering_t;
|
||||
// pub type pager_t = crate::ffi::pager_t;
|
||||
// pub type page_rendering_t = crate::ffi::page_rendering_t;
|
||||
pub type Pager = crate::pager::Pager;
|
||||
pub type PageRendering = crate::pager::PageRendering;
|
||||
pub type highlight_spec_t = crate::ffi::highlight_spec_t;
|
||||
}
|
||||
extern "Rust" {
|
||||
|
@ -1891,8 +1889,8 @@ mod screen_ffi {
|
|||
indent: &CxxVector<i32>,
|
||||
cursor_pos: usize,
|
||||
vars: *mut u8,
|
||||
pager: Pin<&mut pager_t>,
|
||||
page_rendering: Pin<&mut page_rendering_t>,
|
||||
pager: Pin<&mut Pager>,
|
||||
page_rendering: Pin<&mut PageRendering>,
|
||||
cursor_is_within_pager: bool,
|
||||
);
|
||||
fn reset_abandoning_line(&mut self, screen_width: usize);
|
||||
|
@ -1956,8 +1954,8 @@ impl Screen {
|
|||
indent: &CxxVector<i32>,
|
||||
cursor_pos: usize,
|
||||
vars: *mut u8,
|
||||
pager: Pin<&mut ffi::pager_t>,
|
||||
page_rendering: Pin<&mut ffi::page_rendering_t>,
|
||||
pager: Pin<&mut Pager>,
|
||||
page_rendering: Pin<&mut PageRendering>,
|
||||
cursor_is_within_pager: bool,
|
||||
) {
|
||||
let vars = unsafe { Box::from_raw(vars as *mut EnvStackRef) };
|
||||
|
@ -1974,8 +1972,8 @@ impl Screen {
|
|||
&my_indent,
|
||||
cursor_pos,
|
||||
vars.as_ref().as_ref().get_ref(),
|
||||
pager,
|
||||
page_rendering,
|
||||
pager.get_mut(),
|
||||
page_rendering.get_mut(),
|
||||
cursor_is_within_pager,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ mod expand;
|
|||
mod fd_monitor;
|
||||
mod highlight;
|
||||
mod history;
|
||||
mod pager;
|
||||
mod parser;
|
||||
#[cfg(test)]
|
||||
mod redirection;
|
||||
|
|
192
fish-rust/src/tests/pager.rs
Normal file
192
fish-rust/src/tests/pager.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
use crate::common::get_ellipsis_char;
|
||||
use crate::complete::{CompleteFlags, Completion};
|
||||
use crate::ffi_tests::add_test;
|
||||
use crate::pager::{Pager, SelectionMotion};
|
||||
use crate::termsize::Termsize;
|
||||
use crate::wchar::prelude::*;
|
||||
use crate::wchar_ext::WExt;
|
||||
use crate::wcstringutil::StringFuzzyMatch;
|
||||
|
||||
add_test!("test_pager_navigation", || {
|
||||
// Generate 19 strings of width 10. There's 2 spaces between completions, and our term size is
|
||||
// 80; these can therefore fit into 6 columns (6 * 12 - 2 = 70) or 5 columns (58) but not 7
|
||||
// columns (7 * 12 - 2 = 82).
|
||||
//
|
||||
// You can simulate this test by creating 19 files named "file00.txt" through "file_18.txt".
|
||||
let mut completions = vec![];
|
||||
for _ in 0..19 {
|
||||
completions.push(Completion::new(
|
||||
L!("abcdefghij").to_owned(),
|
||||
"".into(),
|
||||
StringFuzzyMatch::exact_match(),
|
||||
CompleteFlags::default(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut pager = Pager::default();
|
||||
pager.set_completions(&completions);
|
||||
pager.set_term_size(&Termsize::defaults());
|
||||
let mut render = pager.render();
|
||||
|
||||
assert_eq!(render.term_width, Some(80));
|
||||
assert_eq!(render.term_height, Some(24));
|
||||
|
||||
let rows = 4;
|
||||
let cols = 5;
|
||||
|
||||
// We have 19 completions. We can fit into 6 columns with 4 rows or 5 columns with 4 rows; the
|
||||
// second one is better and so is what we ought to have picked.
|
||||
assert_eq!(render.rows, rows);
|
||||
assert_eq!(render.cols, cols);
|
||||
|
||||
// Initially expect to have no completion index.
|
||||
assert!(render.selected_completion_idx.is_none());
|
||||
|
||||
// Here are navigation directions and where we expect the selection to be.
|
||||
macro_rules! validate {
|
||||
($pager:ident, $render:ident, $dir:expr, $sel:expr) => {
|
||||
$pager.select_next_completion_in_direction($dir, &$render);
|
||||
$pager.update_rendering(&mut $render);
|
||||
assert_eq!(
|
||||
Some($sel),
|
||||
$render.selected_completion_idx,
|
||||
"For command {:?}",
|
||||
$dir
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Tab completion to get into the list.
|
||||
validate!(pager, render, SelectionMotion::Next, 0);
|
||||
// Westward motion in upper left goes to the last filled column in the last row.
|
||||
validate!(pager, render, SelectionMotion::West, 15);
|
||||
// East goes back.
|
||||
validate!(pager, render, SelectionMotion::East, 0);
|
||||
validate!(pager, render, SelectionMotion::West, 15);
|
||||
validate!(pager, render, SelectionMotion::West, 11);
|
||||
validate!(pager, render, SelectionMotion::East, 15);
|
||||
validate!(pager, render, SelectionMotion::East, 0);
|
||||
// "Next" motion goes down the column.
|
||||
validate!(pager, render, SelectionMotion::Next, 1);
|
||||
validate!(pager, render, SelectionMotion::Next, 2);
|
||||
validate!(pager, render, SelectionMotion::West, 17);
|
||||
validate!(pager, render, SelectionMotion::East, 2);
|
||||
validate!(pager, render, SelectionMotion::East, 6);
|
||||
validate!(pager, render, SelectionMotion::East, 10);
|
||||
validate!(pager, render, SelectionMotion::East, 14);
|
||||
validate!(pager, render, SelectionMotion::East, 18);
|
||||
validate!(pager, render, SelectionMotion::West, 14);
|
||||
validate!(pager, render, SelectionMotion::East, 18);
|
||||
// Eastward motion wraps back to the upper left, westward goes to the prior column.
|
||||
validate!(pager, render, SelectionMotion::East, 3);
|
||||
validate!(pager, render, SelectionMotion::East, 7);
|
||||
validate!(pager, render, SelectionMotion::East, 11);
|
||||
validate!(pager, render, SelectionMotion::East, 15);
|
||||
// Pages.
|
||||
validate!(pager, render, SelectionMotion::PageNorth, 12);
|
||||
validate!(pager, render, SelectionMotion::PageSouth, 15);
|
||||
validate!(pager, render, SelectionMotion::PageNorth, 12);
|
||||
validate!(pager, render, SelectionMotion::East, 16);
|
||||
validate!(pager, render, SelectionMotion::PageSouth, 18);
|
||||
validate!(pager, render, SelectionMotion::East, 3);
|
||||
validate!(pager, render, SelectionMotion::North, 2);
|
||||
validate!(pager, render, SelectionMotion::PageNorth, 0);
|
||||
validate!(pager, render, SelectionMotion::PageSouth, 3);
|
||||
});
|
||||
|
||||
add_test!("test_pager_layout", || {
|
||||
// These tests are woefully incomplete
|
||||
// They only test the truncation logic for a single completion
|
||||
|
||||
let rendered_line = |pager: &mut Pager, width: isize| {
|
||||
pager.set_term_size(&Termsize::new(width, 24));
|
||||
let rendering = pager.render();
|
||||
let sd = &rendering.screen_data;
|
||||
assert_eq!(sd.line_count(), 1);
|
||||
let line = sd.line(0);
|
||||
WString::from(Vec::from_iter((0..line.len()).map(|i| line.char_at(i))))
|
||||
};
|
||||
let compute_expected = |expected: &wstr| {
|
||||
let ellipsis_char = get_ellipsis_char();
|
||||
if ellipsis_char != '\u{2026}' {
|
||||
// hack: handle the case where ellipsis is not L'\x2026'
|
||||
expected.replace(L!("\u{2026}"), wstr::from_char_slice(&[ellipsis_char]))
|
||||
} else {
|
||||
expected.to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
macro_rules! validate {
|
||||
($pager:expr, $width:expr, $expected:expr) => {
|
||||
assert_eq!(
|
||||
rendered_line($pager, $width),
|
||||
compute_expected($expected),
|
||||
"width {}",
|
||||
$width
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
let mut pager = Pager::default();
|
||||
|
||||
// These test cases have equal completions and descriptions
|
||||
let c1s = vec![Completion::new(
|
||||
L!("abcdefghij").to_owned(),
|
||||
L!("1234567890").to_owned(),
|
||||
StringFuzzyMatch::exact_match(),
|
||||
CompleteFlags::default(),
|
||||
)];
|
||||
pager.set_completions(&c1s);
|
||||
|
||||
validate!(&mut pager, 26, L!("abcdefghij (1234567890)"));
|
||||
validate!(&mut pager, 25, L!("abcdefghij (1234567890)"));
|
||||
validate!(&mut pager, 24, L!("abcdefghij (1234567890)"));
|
||||
validate!(&mut pager, 23, L!("abcdefghij (12345678…)"));
|
||||
validate!(&mut pager, 22, L!("abcdefghij (1234567…)"));
|
||||
validate!(&mut pager, 21, L!("abcdefghij (123456…)"));
|
||||
validate!(&mut pager, 20, L!("abcdefghij (12345…)"));
|
||||
validate!(&mut pager, 19, L!("abcdefghij (1234…)"));
|
||||
validate!(&mut pager, 18, L!("abcdefgh… (1234…)"));
|
||||
validate!(&mut pager, 17, L!("abcdefg… (1234…)"));
|
||||
validate!(&mut pager, 16, L!("abcdefg… (123…)"));
|
||||
|
||||
// These test cases have heavyweight completions
|
||||
let c2s = vec![Completion::new(
|
||||
L!("abcdefghijklmnopqrs").to_owned(),
|
||||
L!("1").to_owned(),
|
||||
StringFuzzyMatch::exact_match(),
|
||||
CompleteFlags::default(),
|
||||
)];
|
||||
pager.set_completions(&c2s);
|
||||
validate!(&mut pager, 26, L!("abcdefghijklmnopqrs (1)"));
|
||||
validate!(&mut pager, 25, L!("abcdefghijklmnopqrs (1)"));
|
||||
validate!(&mut pager, 24, L!("abcdefghijklmnopqrs (1)"));
|
||||
validate!(&mut pager, 23, L!("abcdefghijklmnopq… (1)"));
|
||||
validate!(&mut pager, 22, L!("abcdefghijklmnop… (1)"));
|
||||
validate!(&mut pager, 21, L!("abcdefghijklmno… (1)"));
|
||||
validate!(&mut pager, 20, L!("abcdefghijklmn… (1)"));
|
||||
validate!(&mut pager, 19, L!("abcdefghijklm… (1)"));
|
||||
validate!(&mut pager, 18, L!("abcdefghijkl… (1)"));
|
||||
validate!(&mut pager, 17, L!("abcdefghijk… (1)"));
|
||||
validate!(&mut pager, 16, L!("abcdefghij… (1)"));
|
||||
|
||||
// These test cases have no descriptions
|
||||
let c3s = vec![Completion::new(
|
||||
L!("abcdefghijklmnopqrst").to_owned(),
|
||||
L!("").to_owned(),
|
||||
StringFuzzyMatch::exact_match(),
|
||||
CompleteFlags::default(),
|
||||
)];
|
||||
pager.set_completions(&c3s);
|
||||
validate!(&mut pager, 26, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 25, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 24, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 23, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 22, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 21, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 20, L!("abcdefghijklmnopqrst"));
|
||||
validate!(&mut pager, 19, L!("abcdefghijklmnopqr…"));
|
||||
validate!(&mut pager, 18, L!("abcdefghijklmnopq…"));
|
||||
validate!(&mut pager, 17, L!("abcdefghijklmnop…"));
|
||||
validate!(&mut pager, 16, L!("abcdefghijklmno…"));
|
||||
});
|
|
@ -1,6 +1,9 @@
|
|||
use std::{iter, slice};
|
||||
|
||||
use crate::wchar::{wstr, WString};
|
||||
use crate::{
|
||||
common::subslice_position,
|
||||
wchar::{wstr, WString},
|
||||
};
|
||||
use widestring::utfstr::CharsUtf32;
|
||||
|
||||
/// Helpers to convert things to widestring.
|
||||
|
@ -237,6 +240,19 @@ pub trait WExt {
|
|||
inner(self.as_char_slice(), search.as_ref())
|
||||
}
|
||||
|
||||
/// Replaces all matches of a pattern with another string.
|
||||
fn replace(&self, from: impl AsRef<[char]>, to: &wstr) -> WString {
|
||||
let from = from.as_ref();
|
||||
let mut s = self.as_char_slice().to_vec();
|
||||
let mut offset = 0;
|
||||
while let Some(relpos) = subslice_position(&s[offset..], from) {
|
||||
offset += relpos;
|
||||
s.splice(offset..(offset + from.len()), to.chars());
|
||||
offset += to.len();
|
||||
}
|
||||
WString::from_chars(s)
|
||||
}
|
||||
|
||||
/// \return the index of the first occurrence of the given char, or None.
|
||||
fn find_char(&self, c: char) -> Option<usize> {
|
||||
self.as_char_slice().iter().position(|&x| x == c)
|
||||
|
|
|
@ -249,7 +249,7 @@ impl StringFuzzyMatch {
|
|||
pub fn string_fuzzy_match_string(
|
||||
string: &wstr,
|
||||
match_against: &wstr,
|
||||
anchor_start: bool,
|
||||
anchor_start: bool, /* = false */
|
||||
) -> Option<StringFuzzyMatch> {
|
||||
StringFuzzyMatch::try_create(string, match_against, anchor_start)
|
||||
}
|
||||
|
|
|
@ -1176,195 +1176,6 @@ static void test_abbreviations() {
|
|||
}
|
||||
}
|
||||
|
||||
// todo!("port this")
|
||||
static void test_pager_navigation() {
|
||||
say(L"Testing pager navigation");
|
||||
|
||||
// Generate 19 strings of width 10. There's 2 spaces between completions, and our term size is
|
||||
// 80; these can therefore fit into 6 columns (6 * 12 - 2 = 70) or 5 columns (58) but not 7
|
||||
// columns (7 * 12 - 2 = 82).
|
||||
//
|
||||
// You can simulate this test by creating 19 files named "file00.txt" through "file_18.txt".
|
||||
auto completions = new_completion_list();
|
||||
for (size_t i = 0; i < 19; i++) {
|
||||
append_completion(*completions, L"abcdefghij");
|
||||
}
|
||||
|
||||
pager_t pager;
|
||||
pager.set_completions(*completions);
|
||||
pager.set_term_size(termsize_default());
|
||||
page_rendering_t render = pager.render();
|
||||
|
||||
if (render.term_width != 80) err(L"Wrong term width");
|
||||
if (render.term_height != 24) err(L"Wrong term height");
|
||||
|
||||
size_t rows = 4, cols = 5;
|
||||
|
||||
// We have 19 completions. We can fit into 6 columns with 4 rows or 5 columns with 4 rows; the
|
||||
// second one is better and so is what we ought to have picked.
|
||||
if (render.rows != rows) err(L"Wrong row count");
|
||||
if (render.cols != cols) err(L"Wrong column count");
|
||||
|
||||
// Initially expect to have no completion index.
|
||||
if (render.selected_completion_idx != (size_t)(-1)) {
|
||||
err(L"Wrong initial selection");
|
||||
}
|
||||
|
||||
// Here are navigation directions and where we expect the selection to be.
|
||||
const struct {
|
||||
selection_motion_t dir;
|
||||
size_t sel;
|
||||
} cmds[] = {
|
||||
// Tab completion to get into the list.
|
||||
{selection_motion_t::next, 0},
|
||||
|
||||
// Westward motion in upper left goes to the last filled column in the last row.
|
||||
{selection_motion_t::west, 15},
|
||||
// East goes back.
|
||||
{selection_motion_t::east, 0},
|
||||
|
||||
{selection_motion_t::west, 15},
|
||||
{selection_motion_t::west, 11},
|
||||
{selection_motion_t::east, 15},
|
||||
{selection_motion_t::east, 0},
|
||||
|
||||
// "Next" motion goes down the column.
|
||||
{selection_motion_t::next, 1},
|
||||
{selection_motion_t::next, 2},
|
||||
|
||||
{selection_motion_t::west, 17},
|
||||
{selection_motion_t::east, 2},
|
||||
{selection_motion_t::east, 6},
|
||||
{selection_motion_t::east, 10},
|
||||
{selection_motion_t::east, 14},
|
||||
{selection_motion_t::east, 18},
|
||||
|
||||
{selection_motion_t::west, 14},
|
||||
{selection_motion_t::east, 18},
|
||||
|
||||
// Eastward motion wraps back to the upper left, westward goes to the prior column.
|
||||
{selection_motion_t::east, 3},
|
||||
{selection_motion_t::east, 7},
|
||||
{selection_motion_t::east, 11},
|
||||
{selection_motion_t::east, 15},
|
||||
|
||||
// Pages.
|
||||
{selection_motion_t::page_north, 12},
|
||||
{selection_motion_t::page_south, 15},
|
||||
{selection_motion_t::page_north, 12},
|
||||
{selection_motion_t::east, 16},
|
||||
{selection_motion_t::page_south, 18},
|
||||
{selection_motion_t::east, 3},
|
||||
{selection_motion_t::north, 2},
|
||||
{selection_motion_t::page_north, 0},
|
||||
{selection_motion_t::page_south, 3},
|
||||
|
||||
};
|
||||
for (size_t i = 0; i < sizeof cmds / sizeof *cmds; i++) {
|
||||
pager.select_next_completion_in_direction(cmds[i].dir, render);
|
||||
pager.update_rendering(&render);
|
||||
if (cmds[i].sel != render.selected_completion_idx) {
|
||||
err(L"For command %lu, expected selection %lu, but found instead %lu\n", i, cmds[i].sel,
|
||||
render.selected_completion_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct pager_layout_testcase_t {
|
||||
int width;
|
||||
const wchar_t *expected;
|
||||
|
||||
// Run ourselves as a test case.
|
||||
// Set our data on the pager, and then check the rendering.
|
||||
// We should have one line, and it should have our expected text.
|
||||
void run(pager_t &pager) const {
|
||||
pager.set_term_size(termsize_t{this->width, 24});
|
||||
page_rendering_t rendering = pager.render();
|
||||
const screen_data_t &sd = *rendering.screen_data;
|
||||
do_test(sd.line_count() == 1);
|
||||
if (sd.line_count() > 0) {
|
||||
wcstring expected = this->expected;
|
||||
|
||||
// hack: handle the case where ellipsis is not L'\x2026'
|
||||
wchar_t ellipsis_char = get_ellipsis_char();
|
||||
if (ellipsis_char != L'\x2026') {
|
||||
std::replace(expected.begin(), expected.end(), L'\x2026', ellipsis_char);
|
||||
}
|
||||
|
||||
wcstring text = *(sd.line_ffi(0)->text_characters_ffi());
|
||||
if (text != expected) {
|
||||
std::fwprintf(stderr, L"width %d got %zu<%ls>, expected %zu<%ls>\n", this->width,
|
||||
text.length(), text.c_str(), expected.length(), expected.c_str());
|
||||
for (size_t i = 0; i < std::max(text.length(), expected.length()); i++) {
|
||||
std::fwprintf(stderr, L"i %zu got <%lx> expected <%lx>\n", i,
|
||||
i >= text.length() ? 0xffff : text[i],
|
||||
i >= expected.length() ? 0xffff : expected[i]);
|
||||
}
|
||||
}
|
||||
do_test(text == expected);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// todo!("port this")
|
||||
static void test_pager_layout() {
|
||||
// These tests are woefully incomplete
|
||||
// They only test the truncation logic for a single completion
|
||||
say(L"Testing pager layout");
|
||||
pager_t pager;
|
||||
|
||||
// These test cases have equal completions and descriptions
|
||||
auto c1 = new_completion_with(L"abcdefghij", L"1234567890", 0);
|
||||
auto c1s = new_completion_list();
|
||||
c1s->push_back(*c1);
|
||||
pager.set_completions(*c1s);
|
||||
const pager_layout_testcase_t testcases1[] = {
|
||||
{26, L"abcdefghij (1234567890)"}, {25, L"abcdefghij (1234567890)"},
|
||||
{24, L"abcdefghij (1234567890)"}, {23, L"abcdefghij (12345678…)"},
|
||||
{22, L"abcdefghij (1234567…)"}, {21, L"abcdefghij (123456…)"},
|
||||
{20, L"abcdefghij (12345…)"}, {19, L"abcdefghij (1234…)"},
|
||||
{18, L"abcdefgh… (1234…)"}, {17, L"abcdefg… (1234…)"},
|
||||
{16, L"abcdefg… (123…)"}, {0, nullptr} // sentinel terminator
|
||||
};
|
||||
for (size_t i = 0; testcases1[i].expected != nullptr; i++) {
|
||||
testcases1[i].run(pager);
|
||||
}
|
||||
|
||||
// These test cases have heavyweight completions
|
||||
auto c2 = new_completion_with(L"abcdefghijklmnopqrs", L"1", 0);
|
||||
auto c2s = new_completion_list();
|
||||
c2s->push_back(*c2);
|
||||
pager.set_completions(*c2s);
|
||||
const pager_layout_testcase_t testcases2[] = {
|
||||
{26, L"abcdefghijklmnopqrs (1)"}, {25, L"abcdefghijklmnopqrs (1)"},
|
||||
{24, L"abcdefghijklmnopqrs (1)"}, {23, L"abcdefghijklmnopq… (1)"},
|
||||
{22, L"abcdefghijklmnop… (1)"}, {21, L"abcdefghijklmno… (1)"},
|
||||
{20, L"abcdefghijklmn… (1)"}, {19, L"abcdefghijklm… (1)"},
|
||||
{18, L"abcdefghijkl… (1)"}, {17, L"abcdefghijk… (1)"},
|
||||
{16, L"abcdefghij… (1)"}, {0, nullptr} // sentinel terminator
|
||||
};
|
||||
for (size_t i = 0; testcases2[i].expected != nullptr; i++) {
|
||||
testcases2[i].run(pager);
|
||||
}
|
||||
|
||||
// These test cases have no descriptions
|
||||
auto c3 = new_completion_with(L"abcdefghijklmnopqrst", L"", 0);
|
||||
auto c3s = new_completion_list();
|
||||
c3s->push_back(*c3);
|
||||
pager.set_completions(*c3s);
|
||||
const pager_layout_testcase_t testcases3[] = {
|
||||
{26, L"abcdefghijklmnopqrst"}, {25, L"abcdefghijklmnopqrst"},
|
||||
{24, L"abcdefghijklmnopqrst"}, {23, L"abcdefghijklmnopqrst"},
|
||||
{22, L"abcdefghijklmnopqrst"}, {21, L"abcdefghijklmnopqrst"},
|
||||
{20, L"abcdefghijklmnopqrst"}, {19, L"abcdefghijklmnopqr…"},
|
||||
{18, L"abcdefghijklmnopq…"}, {17, L"abcdefghijklmnop…"},
|
||||
{16, L"abcdefghijklmno…"}, {0, nullptr} // sentinel terminator
|
||||
};
|
||||
for (size_t i = 0; testcases3[i].expected != nullptr; i++) {
|
||||
testcases3[i].run(pager);
|
||||
}
|
||||
}
|
||||
|
||||
// todo!("port this")
|
||||
enum word_motion_t { word_motion_left, word_motion_right };
|
||||
static void test_1_word_motion(word_motion_t motion, move_word_style_t style,
|
||||
|
@ -2465,8 +2276,6 @@ static const test_t s_tests[]{
|
|||
{TEST_GROUP("parser"), test_parser},
|
||||
{TEST_GROUP("lru"), test_lru},
|
||||
{TEST_GROUP("wcstod"), test_wcstod},
|
||||
{TEST_GROUP("pager_navigation"), test_pager_navigation},
|
||||
{TEST_GROUP("pager_layout"), test_pager_layout},
|
||||
{TEST_GROUP("word_motion"), test_word_motion},
|
||||
{TEST_GROUP("colors"), test_colors},
|
||||
{TEST_GROUP("input"), test_input},
|
||||
|
|
970
src/pager.cpp
970
src/pager.cpp
|
@ -1,970 +0,0 @@
|
|||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
#include "pager.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <wctype.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cwchar>
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "complete.h"
|
||||
#include "editable_line.rs.h"
|
||||
#include "fallback.h"
|
||||
#include "highlight.h"
|
||||
#include "maybe.h"
|
||||
#include "operation_context.h"
|
||||
#include "reader.h"
|
||||
#include "screen.h"
|
||||
#include "termsize.h"
|
||||
#include "wcstringutil.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
|
||||
using comp_t = pager_t::comp_t;
|
||||
using comp_info_list_t = std::vector<comp_t>;
|
||||
|
||||
comp_t &comp_t::operator=(const comp_t &other) {
|
||||
if (this == &other) return *this;
|
||||
comp = other.comp;
|
||||
desc = other.desc;
|
||||
representative = other.representative->clone();
|
||||
colors = other.colors;
|
||||
comp_width = other.comp_width;
|
||||
desc_width = other.desc_width;
|
||||
return *this;
|
||||
}
|
||||
|
||||
comp_t::comp_t(const comp_t &other) { *this = other; }
|
||||
|
||||
/// The minimum width (in characters) the terminal must to show completions at all.
|
||||
#define PAGER_MIN_WIDTH 16
|
||||
|
||||
/// Minimum height to show completions
|
||||
#define PAGER_MIN_HEIGHT 4
|
||||
|
||||
/// The maximum number of columns of completion to attempt to fit onto the screen.
|
||||
#define PAGER_MAX_COLS 6
|
||||
|
||||
/// Width of the search field.
|
||||
#define PAGER_SEARCH_FIELD_WIDTH 12
|
||||
|
||||
/// Text we use for the search field.
|
||||
#define SEARCH_FIELD_PROMPT _(L"search: ")
|
||||
|
||||
static inline bool selection_direction_is_cardinal(selection_motion_t dir) {
|
||||
switch (dir) {
|
||||
case selection_motion_t::north:
|
||||
case selection_motion_t::east:
|
||||
case selection_motion_t::south:
|
||||
case selection_motion_t::west:
|
||||
case selection_motion_t::page_north:
|
||||
case selection_motion_t::page_south: {
|
||||
return true;
|
||||
}
|
||||
case selection_motion_t::next:
|
||||
case selection_motion_t::prev:
|
||||
case selection_motion_t::deselect: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
DIE("unreachable");
|
||||
}
|
||||
|
||||
/// Returns numer / denom, rounding up. As a "courtesy" 0/0 is 0.
|
||||
static size_t divide_round_up(size_t numer, size_t denom) {
|
||||
if (numer == 0) return 0;
|
||||
assert(denom > 0);
|
||||
bool has_rem = (numer % denom) != 0;
|
||||
return numer / denom + (has_rem ? 1 : 0);
|
||||
}
|
||||
|
||||
/// Print the specified string, but use at most the specified amount of space. If the whole string
|
||||
/// can't be fitted, ellipsize it.
|
||||
///
|
||||
/// \param str the string to print
|
||||
/// \param color the color to apply to every printed character
|
||||
/// \param max the maximum space that may be used for printing
|
||||
/// \param has_more if this flag is true, this is not the entire string, and the string should be
|
||||
/// ellipsized even if the string fits but takes up the whole space.
|
||||
template <typename Func>
|
||||
static typename std::enable_if<
|
||||
std::is_convertible<Func, std::function<highlight_spec_t(size_t)>>::value, size_t>::type
|
||||
print_max(const wcstring &str, Func color, size_t max, bool has_more, line_t *line) {
|
||||
size_t remaining = max;
|
||||
for (size_t i = 0; i < str.size(); i++) {
|
||||
wchar_t c = str.at(i);
|
||||
int iwidth_c = fish_wcwidth(c);
|
||||
if (iwidth_c < 0) {
|
||||
// skip non-printable characters
|
||||
continue;
|
||||
}
|
||||
auto width_c = size_t(iwidth_c);
|
||||
|
||||
if (width_c > remaining) break;
|
||||
|
||||
wchar_t ellipsis = get_ellipsis_char();
|
||||
if ((width_c == remaining) && (has_more || i + 1 < str.size())) {
|
||||
line->append(ellipsis, color(i));
|
||||
int ellipsis_width = fish_wcwidth(ellipsis);
|
||||
remaining -= std::min(remaining, size_t(ellipsis_width));
|
||||
break;
|
||||
}
|
||||
|
||||
line->append(c, color(i));
|
||||
assert(remaining >= width_c);
|
||||
remaining -= width_c;
|
||||
}
|
||||
|
||||
// return how much we consumed
|
||||
assert(remaining <= max);
|
||||
return max - remaining;
|
||||
}
|
||||
|
||||
static size_t print_max(const wcstring &str, highlight_spec_t color, size_t max, bool has_more,
|
||||
line_t *line) {
|
||||
return print_max(
|
||||
str, [=](size_t) -> highlight_spec_t { return color; }, max, has_more, line);
|
||||
}
|
||||
|
||||
/// Print the specified item using at the specified amount of space.
|
||||
rust::Box<Line> pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, size_t row,
|
||||
size_t column, size_t width, bool secondary,
|
||||
bool selected, page_rendering_t *rendering) const {
|
||||
UNUSED(column);
|
||||
UNUSED(row);
|
||||
UNUSED(rendering);
|
||||
size_t comp_width;
|
||||
rust::Box<Line> line_data_box = new_line();
|
||||
auto &line_data = *line_data_box;
|
||||
|
||||
if (c->preferred_width() <= width) {
|
||||
// The entry fits, we give it as much space as it wants.
|
||||
comp_width = c->comp_width;
|
||||
} else {
|
||||
// The completion and description won't fit on the allocated space. Give a maximum of 2/3 of
|
||||
// the space to the completion, and whatever is left to the description
|
||||
// This expression is an overflow-safe way of calculating (width-4)*2/3
|
||||
size_t width_minus_spacer = width - std::min(width, size_t(4));
|
||||
size_t two_thirds_width = (width_minus_spacer / 3) * 2 + ((width_minus_spacer % 3) * 2) / 3;
|
||||
comp_width = std::min(c->comp_width, two_thirds_width);
|
||||
|
||||
// If the description is short, give the completion the remaining space
|
||||
size_t desc_punct_width = c->description_punctuated_width();
|
||||
if (width > desc_punct_width) {
|
||||
comp_width = std::max(comp_width, width - desc_punct_width);
|
||||
}
|
||||
|
||||
// The description gets what's left
|
||||
assert(comp_width <= width);
|
||||
}
|
||||
|
||||
auto modify_role = [=](highlight_role_t role) -> highlight_role_t {
|
||||
using uint_t = typename std::underlying_type<highlight_role_t>::type;
|
||||
auto base = static_cast<uint_t>(role);
|
||||
if (selected) {
|
||||
base += static_cast<uint_t>(highlight_role_t::pager_selected_background) -
|
||||
static_cast<uint_t>(highlight_role_t::pager_background);
|
||||
} else if (secondary) {
|
||||
base += static_cast<uint_t>(highlight_role_t::pager_secondary_background) -
|
||||
static_cast<uint_t>(highlight_role_t::pager_background);
|
||||
}
|
||||
return static_cast<highlight_role_t>(base);
|
||||
};
|
||||
|
||||
highlight_role_t bg_role = modify_role(highlight_role_t::pager_background);
|
||||
highlight_spec_t bg = {highlight_role_t::normal, bg_role};
|
||||
highlight_spec_t prefix_col = {
|
||||
modify_role(highlight_prefix ? highlight_role_t::pager_prefix
|
||||
: highlight_role_t::pager_completion),
|
||||
bg_role};
|
||||
highlight_spec_t comp_col = {modify_role(highlight_role_t::pager_completion), bg_role};
|
||||
highlight_spec_t desc_col = {modify_role(highlight_role_t::pager_description), bg_role};
|
||||
|
||||
// Print the completion part
|
||||
size_t comp_remaining = comp_width;
|
||||
for (size_t i = 0; i < c->comp.size(); i++) {
|
||||
const wcstring &comp = c->comp.at(i);
|
||||
|
||||
if (i > 0) {
|
||||
comp_remaining -=
|
||||
print_max(PAGER_SPACER_STRING, bg, comp_remaining, true /* has_more */, &line_data);
|
||||
}
|
||||
|
||||
comp_remaining -= print_max(prefix, prefix_col, comp_remaining, !comp.empty(), &line_data);
|
||||
comp_remaining -= print_max(
|
||||
comp,
|
||||
[&](size_t i) -> highlight_spec_t {
|
||||
if (c->colors.empty()) return comp_col; // Not a shell command.
|
||||
if (selected) return comp_col; // Rendered in reverse video, so avoid highlighting.
|
||||
return i < c->colors.size() ? c->colors[i] : c->colors.back();
|
||||
},
|
||||
comp_remaining, i + 1 < c->comp.size(), &line_data);
|
||||
}
|
||||
|
||||
size_t desc_remaining = width - comp_width + comp_remaining;
|
||||
if (c->desc_width > 0 && desc_remaining > 4) {
|
||||
// always have at least two spaces to separate completion and description
|
||||
desc_remaining -= print_max(L" ", bg, 2, false, &line_data);
|
||||
|
||||
// right-justify the description by adding spaces
|
||||
// the 2 here refers to the parenthesis below
|
||||
while (desc_remaining > c->desc_width + 2) {
|
||||
desc_remaining -= print_max(L" ", bg, 1, false, &line_data);
|
||||
}
|
||||
|
||||
assert(desc_remaining >= 2);
|
||||
highlight_spec_t paren_col = {highlight_role_t::pager_completion, bg_role};
|
||||
desc_remaining -= print_max(L"(", paren_col, 1, false, &line_data);
|
||||
desc_remaining -= print_max(c->desc, desc_col, desc_remaining - 1, false, &line_data);
|
||||
desc_remaining -= print_max(L")", paren_col, 1, false, &line_data);
|
||||
} else {
|
||||
// No description, or it won't fit. Just add spaces.
|
||||
print_max(wcstring(desc_remaining, L' '), bg, desc_remaining, false, &line_data);
|
||||
}
|
||||
|
||||
return line_data_box;
|
||||
}
|
||||
|
||||
/// Print the specified part of the completion list, using the specified column offsets and quoting
|
||||
/// style.
|
||||
///
|
||||
/// \param cols number of columns to print in
|
||||
/// \param width_by_column An array specifying the width of each column
|
||||
/// \param row_start The first row to print
|
||||
/// \param row_stop the row after the last row to print
|
||||
/// \param prefix The string to print before each completion
|
||||
/// \param lst The list of completions to print
|
||||
void pager_t::completion_print(size_t cols, const size_t *width_by_column, size_t row_start,
|
||||
size_t row_stop, const wcstring &prefix, const comp_info_list_t &lst,
|
||||
page_rendering_t *rendering) const {
|
||||
// Teach the rendering about the rows it printed.
|
||||
assert(row_stop >= row_start);
|
||||
rendering->row_start = row_start;
|
||||
rendering->row_end = row_stop;
|
||||
|
||||
size_t rows = divide_round_up(lst.size(), cols);
|
||||
|
||||
size_t effective_selected_idx = this->visual_selected_completion_index(rows, cols);
|
||||
|
||||
for (size_t row = row_start; row < row_stop; row++) {
|
||||
for (size_t col = 0; col < cols; col++) {
|
||||
if (lst.size() <= col * rows + row) continue;
|
||||
|
||||
size_t idx = col * rows + row;
|
||||
const comp_t *el = &lst.at(idx);
|
||||
bool is_selected = (idx == effective_selected_idx);
|
||||
|
||||
// Print this completion on its own "line".
|
||||
auto line = completion_print_item(prefix, el, row, col, width_by_column[col], row % 2,
|
||||
is_selected, rendering);
|
||||
|
||||
// If there's more to come, append two spaces.
|
||||
if (col + 1 < cols) {
|
||||
line->append_str(PAGER_SPACER_STRING, highlight_spec_t{});
|
||||
}
|
||||
|
||||
// Append this to the real line.
|
||||
rendering->screen_data->create_line(row - row_start)->append_line(*line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim leading and trailing whitespace, and compress other whitespace runs into a single space.
|
||||
static void mangle_1_completion_description(wcstring *str) {
|
||||
size_t leading = 0, trailing = 0, len = str->size();
|
||||
|
||||
// Skip leading spaces.
|
||||
for (; leading < len; leading++) {
|
||||
if (!iswspace(str->at(leading))) break;
|
||||
}
|
||||
|
||||
// Compress runs of spaces to a single space.
|
||||
bool was_space = false;
|
||||
for (; leading < len; leading++) {
|
||||
wchar_t wc = str->at(leading);
|
||||
bool is_space = iswspace(wc);
|
||||
if (!is_space) { // normal character
|
||||
str->at(trailing++) = wc;
|
||||
} else if (!was_space) { // initial space in a run
|
||||
str->at(trailing++) = L' ';
|
||||
} // else non-initial space in a run, do nothing
|
||||
was_space = is_space;
|
||||
}
|
||||
|
||||
// leading is now at len, trailing is the new length of the string. Delete trailing spaces.
|
||||
while (trailing > 0 && iswspace(str->at(trailing - 1))) {
|
||||
trailing--;
|
||||
}
|
||||
|
||||
str->resize(trailing);
|
||||
}
|
||||
|
||||
static void join_completions(comp_info_list_t *comps) {
|
||||
// A map from description to index in the completion list of the element with that description.
|
||||
// The indexes are stored +1.
|
||||
std::unordered_map<wcstring, size_t> desc_table;
|
||||
|
||||
// Note that we mutate the completion list as we go, so the size changes.
|
||||
for (size_t i = 0; i < comps->size(); i++) {
|
||||
const comp_t &new_comp = comps->at(i);
|
||||
const wcstring &desc = new_comp.desc;
|
||||
if (desc.empty()) continue;
|
||||
|
||||
// See if it's in the table.
|
||||
size_t prev_idx_plus_one = desc_table[desc];
|
||||
if (prev_idx_plus_one == 0) {
|
||||
// We're the first with this description.
|
||||
desc_table[desc] = i + 1;
|
||||
} else {
|
||||
// There's a prior completion with this description. Append the new ones to it.
|
||||
comp_t *prior_comp = &comps->at(prev_idx_plus_one - 1);
|
||||
prior_comp->comp.insert(prior_comp->comp.end(), new_comp.comp.begin(),
|
||||
new_comp.comp.end());
|
||||
|
||||
// Erase the element at this index, and decrement the index to reflect that fact.
|
||||
comps->erase(comps->begin() + i);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a list of comp_t structures from a list of completions.
|
||||
static comp_info_list_t process_completions_into_infos(const completion_list_t &lst) {
|
||||
const size_t lst_size = lst.size();
|
||||
|
||||
// Make the list of the correct size up-front.
|
||||
comp_info_list_t result(lst_size);
|
||||
for (size_t i = 0; i < lst_size; i++) {
|
||||
const completion_t &comp = lst.at(i);
|
||||
comp_t *comp_info = &result.at(i);
|
||||
|
||||
// Append the single completion string. We may later merge these into multiple.
|
||||
comp_info->comp.push_back(escape_string(
|
||||
*comp.completion(), ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED | ESCAPE_SYMBOLIC));
|
||||
if (comp.replaces_commandline()
|
||||
// HACK We want to render a full shell command, with syntax highlighting. Above we
|
||||
// escape nonprintables, which might make the rendered command longer than the original
|
||||
// completion. In that case we get wrong colors. However this should only happen in
|
||||
// contrived cases, since our symbolic escaping uses a single character to represent
|
||||
// newline and tab characters; other nonprintables are extremely rare in a command
|
||||
// line. It will only be common for single-byte locales where we don't
|
||||
// use Unicode characters for escaping, so just disable those here.
|
||||
// We should probably fix this by first highlighting the original completion, and
|
||||
// then writing a variant of escape_string() that adjusts highlighting according
|
||||
// so it matches the escaped string.
|
||||
&& MB_CUR_MAX > 1) {
|
||||
highlight_shell(*comp.completion(), comp_info->colors, *empty_operation_context());
|
||||
assert(comp_info->comp.back().size() >= comp_info->colors.size());
|
||||
}
|
||||
|
||||
// Append the mangled description.
|
||||
comp_info->desc = std::move(*comp.description());
|
||||
mangle_1_completion_description(&comp_info->desc);
|
||||
|
||||
// Set the representative completion.
|
||||
comp_info->representative = comp.clone();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void pager_t::measure_completion_infos(comp_info_list_t *infos, const wcstring &prefix) const {
|
||||
size_t prefix_len = fish_wcswidth(prefix);
|
||||
for (auto &info : *infos) {
|
||||
comp_t *comp = &info;
|
||||
const std::vector<wcstring> &comp_strings = comp->comp;
|
||||
|
||||
for (size_t j = 0; j < comp_strings.size(); j++) {
|
||||
// If there's more than one, append the length of ', '.
|
||||
if (j >= 1) comp->comp_width += 2;
|
||||
|
||||
// fish_wcswidth() can return -1 if it can't calculate the width. So be cautious.
|
||||
int comp_width = fish_wcswidth(comp_strings.at(j));
|
||||
if (comp_width >= 0) comp->comp_width += prefix_len + comp_width;
|
||||
}
|
||||
|
||||
// fish_wcswidth() can return -1 if it can't calculate the width. So be cautious.
|
||||
int desc_width = fish_wcswidth(comp->desc);
|
||||
comp->desc_width = desc_width > 0 ? desc_width : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Indicates if the given completion info passes any filtering we have.
|
||||
bool pager_t::completion_info_passes_filter(const comp_t &info) const {
|
||||
// If we have no filter, everything passes.
|
||||
if (!search_field_shown || this->search_field_line.empty()) return true;
|
||||
|
||||
const wcstring needle = *this->search_field_line.text();
|
||||
|
||||
// Match against the description.
|
||||
if (string_fuzzy_match_string(needle, info.desc)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Match against the completion strings.
|
||||
for (const auto &i : info.comp) {
|
||||
if (string_fuzzy_match_string(needle, prefix + i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // no match
|
||||
}
|
||||
|
||||
// Update completion_infos from unfiltered_completion_infos, to reflect the filter.
|
||||
void pager_t::refilter_completions() {
|
||||
this->completion_infos.clear();
|
||||
for (const auto &info : this->unfiltered_completion_infos) {
|
||||
if (this->completion_info_passes_filter(info)) {
|
||||
this->completion_infos.push_back(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pager_t::set_completions(const completion_list_t &raw_completions) {
|
||||
selected_completion_idx = PAGER_SELECTION_NONE;
|
||||
// Get completion infos out of it.
|
||||
unfiltered_completion_infos = process_completions_into_infos(raw_completions);
|
||||
|
||||
// Maybe join them.
|
||||
if (prefix == L"-") join_completions(&unfiltered_completion_infos);
|
||||
|
||||
// Compute their various widths.
|
||||
measure_completion_infos(&unfiltered_completion_infos, prefix);
|
||||
|
||||
// Refilter them.
|
||||
this->refilter_completions();
|
||||
have_unrendered_completions = true;
|
||||
}
|
||||
|
||||
void pager_t::set_prefix(const wcstring &pref, bool highlight) {
|
||||
prefix = pref;
|
||||
highlight_prefix = highlight;
|
||||
}
|
||||
|
||||
void pager_t::set_term_size(const termsize_t &ts) {
|
||||
available_term_width = ts.width > 0 ? ts.width : 0;
|
||||
available_term_height = ts.height > 0 ? ts.height : 0;
|
||||
}
|
||||
|
||||
void pager_set_term_size_ffi(pager_t &pager, const void *ts) {
|
||||
pager.set_term_size(*reinterpret_cast<const termsize_t *>(ts));
|
||||
}
|
||||
|
||||
void pager_update_rendering_ffi(pager_t &pager, page_rendering_t &rendering) {
|
||||
pager.update_rendering(&rendering);
|
||||
}
|
||||
|
||||
/// Try to print the list of completions lst with the prefix prefix using cols as the number of
|
||||
/// columns. Return true if the completion list was printed, false if the terminal is too narrow for
|
||||
/// the specified number of columns. Always succeeds if cols is 1.
|
||||
bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const comp_info_list_t &lst,
|
||||
page_rendering_t *rendering, size_t suggested_start_row) const {
|
||||
assert(cols > 0);
|
||||
// The calculated preferred width of each column.
|
||||
size_t width_by_column[PAGER_MAX_COLS] = {0};
|
||||
|
||||
// Skip completions on tiny terminals.
|
||||
if (this->available_term_width < PAGER_MIN_WIDTH ||
|
||||
this->available_term_height < PAGER_MIN_HEIGHT)
|
||||
return true;
|
||||
|
||||
// Compute the effective term width and term height, accounting for disclosure.
|
||||
size_t term_width = this->available_term_width;
|
||||
size_t term_height =
|
||||
this->available_term_height - 1 -
|
||||
(search_field_shown ? 1 : 0); // we always subtract 1 to make room for a comment row
|
||||
if (!this->fully_disclosed) {
|
||||
// We disclose between half and the entirety of the terminal height,
|
||||
// but at least 4 rows.
|
||||
//
|
||||
// We do this so we show a useful amount but don't force fish to
|
||||
// THE VERY TOP, which is jarring.
|
||||
term_height =
|
||||
std::min(term_height,
|
||||
std::max(term_height / 2, static_cast<size_t>(PAGER_UNDISCLOSED_MAX_ROWS)));
|
||||
}
|
||||
|
||||
size_t row_count = divide_round_up(lst.size(), cols);
|
||||
|
||||
// We have more to disclose if we are not fully disclosed and there's more rows than we have in
|
||||
// our term height.
|
||||
if (!this->fully_disclosed && row_count > term_height) {
|
||||
rendering->remaining_to_disclose = row_count - term_height;
|
||||
} else {
|
||||
rendering->remaining_to_disclose = 0;
|
||||
}
|
||||
|
||||
// If we have only one row remaining to disclose, then squelch the comment row. This prevents us
|
||||
// from consuming a line to show "...and 1 more row".
|
||||
if (rendering->remaining_to_disclose == 1) {
|
||||
term_height += 1;
|
||||
rendering->remaining_to_disclose = 0;
|
||||
}
|
||||
|
||||
// Calculate how wide the list would be.
|
||||
for (size_t col = 0; col < cols; col++) {
|
||||
for (size_t row = 0; row < row_count; row++) {
|
||||
const size_t comp_idx = col * row_count + row;
|
||||
if (comp_idx >= lst.size()) continue;
|
||||
const comp_t &c = lst.at(comp_idx);
|
||||
width_by_column[col] = std::max(width_by_column[col], c.preferred_width());
|
||||
}
|
||||
}
|
||||
|
||||
bool print;
|
||||
// Force fit if one column.
|
||||
if (cols == 1) {
|
||||
width_by_column[0] = std::min(width_by_column[0], term_width);
|
||||
print = true;
|
||||
} else {
|
||||
// Compute total preferred width, plus spacing
|
||||
size_t total_width_needed = std::accumulate(width_by_column, width_by_column + cols, 0);
|
||||
total_width_needed += (cols - 1) * PAGER_SPACER_STRING_WIDTH;
|
||||
print = (total_width_needed <= term_width);
|
||||
}
|
||||
if (!print) {
|
||||
return false; // no need to continue
|
||||
}
|
||||
|
||||
// Determine the starting and stop row.
|
||||
size_t start_row = 0, stop_row = 0;
|
||||
if (row_count <= term_height) {
|
||||
// Easy, we can show everything.
|
||||
start_row = 0;
|
||||
stop_row = row_count;
|
||||
} else {
|
||||
// We can only show part of the full list. Determine which part based on the
|
||||
// suggested_start_row.
|
||||
assert(row_count > term_height);
|
||||
size_t last_starting_row = row_count - term_height;
|
||||
start_row = std::min(suggested_start_row, last_starting_row);
|
||||
stop_row = start_row + term_height;
|
||||
assert(start_row <= last_starting_row);
|
||||
}
|
||||
|
||||
assert(stop_row >= start_row);
|
||||
assert(stop_row <= row_count);
|
||||
assert(stop_row - start_row <= term_height);
|
||||
completion_print(cols, width_by_column, start_row, stop_row, prefix, lst, rendering);
|
||||
|
||||
// Add the progress line. It's a "more to disclose" line if necessary, or a row listing if
|
||||
// it's scrollable; otherwise ignore it.
|
||||
// We should never have one row remaining to disclose (else we would have just disclosed it)
|
||||
wcstring progress_text;
|
||||
assert(rendering->remaining_to_disclose != 1);
|
||||
if (rendering->remaining_to_disclose > 1) {
|
||||
progress_text = format_string(_(L"%lsand %lu more rows"), get_ellipsis_str(),
|
||||
static_cast<unsigned long>(rendering->remaining_to_disclose));
|
||||
} else if (start_row > 0 || stop_row < row_count) {
|
||||
// We have a scrollable interface. The +1 here is because we are zero indexed, but want
|
||||
// to present things as 1-indexed. We do not add 1 to stop_row or row_count because
|
||||
// these are the "past the last value".
|
||||
progress_text =
|
||||
format_string(_(L"rows %lu to %lu of %lu"), start_row + 1, stop_row, row_count);
|
||||
} else if (search_field_shown && completion_infos.empty()) {
|
||||
// Everything is filtered.
|
||||
progress_text = _(L"(no matches)");
|
||||
}
|
||||
if (!extra_progress_text.empty()) {
|
||||
if (!progress_text.empty()) {
|
||||
progress_text += L". ";
|
||||
}
|
||||
progress_text += extra_progress_text;
|
||||
}
|
||||
|
||||
if (!progress_text.empty()) {
|
||||
line_t &line = rendering->screen_data->add_line();
|
||||
highlight_spec_t spec = {highlight_role_t::pager_progress,
|
||||
highlight_role_t::pager_progress};
|
||||
print_max(progress_text, spec, term_width, true /* has_more */, &line);
|
||||
}
|
||||
|
||||
if (!search_field_shown) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add the search field.
|
||||
wcstring search_field_text = *search_field_line.text();
|
||||
// Append spaces to make it at least the required width.
|
||||
if (search_field_text.size() < PAGER_SEARCH_FIELD_WIDTH) {
|
||||
search_field_text.append(PAGER_SEARCH_FIELD_WIDTH - search_field_text.size(), L' ');
|
||||
}
|
||||
line_t *search_field = &rendering->screen_data->insert_line_at_index(0);
|
||||
|
||||
// We limit the width to term_width - 1.
|
||||
highlight_spec_t underline{};
|
||||
underline->force_underline = true;
|
||||
|
||||
size_t search_field_remaining = term_width - 1;
|
||||
search_field_remaining -= print_max(SEARCH_FIELD_PROMPT, highlight_role_t::normal,
|
||||
search_field_remaining, false, search_field);
|
||||
search_field_remaining -=
|
||||
print_max(search_field_text, underline, search_field_remaining, false, search_field);
|
||||
return true;
|
||||
}
|
||||
|
||||
page_rendering_t pager_t::render() const {
|
||||
/// Try to print the completions. Start by trying to print the list in PAGER_MAX_COLS columns,
|
||||
/// if the completions won't fit, reduce the number of columns by one. Printing a single column
|
||||
/// never fails.
|
||||
page_rendering_t rendering;
|
||||
rendering.term_width = this->available_term_width;
|
||||
rendering.term_height = this->available_term_height;
|
||||
rendering.search_field_shown = this->search_field_shown;
|
||||
rendering.search_field_line = this->search_field_line.clone();
|
||||
|
||||
for (size_t cols = PAGER_MAX_COLS; cols > 0; cols--) {
|
||||
// Initially empty rendering.
|
||||
rendering.screen_data->resize(0);
|
||||
|
||||
// Determine how many rows we would need if we had 'cols' columns. Then determine how many
|
||||
// columns we want from that. For example, say we had 19 completions. We can fit them into 6
|
||||
// columns, 4 rows, with the last row containing only 1 entry. Or we can fit them into 5
|
||||
// columns, 4 rows, the last row containing 4 entries. Since fewer columns with the same
|
||||
// number of rows is better, skip cases where we know we can do better.
|
||||
size_t min_rows_required_for_cols = divide_round_up(completion_infos.size(), cols);
|
||||
size_t min_cols_required_for_rows =
|
||||
divide_round_up(completion_infos.size(), min_rows_required_for_cols);
|
||||
|
||||
assert(min_cols_required_for_rows <= cols);
|
||||
if (cols > 1 && min_cols_required_for_rows < cols) {
|
||||
// Next iteration will be better, so skip this one.
|
||||
continue;
|
||||
}
|
||||
|
||||
rendering.cols = cols;
|
||||
rendering.rows = min_rows_required_for_cols;
|
||||
rendering.selected_completion_idx =
|
||||
this->visual_selected_completion_index(rendering.rows, rendering.cols);
|
||||
|
||||
if (completion_try_print(cols, prefix, completion_infos, &rendering, suggested_row_start)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rendering;
|
||||
}
|
||||
|
||||
bool pager_t::rendering_needs_update(const page_rendering_t &rendering) const {
|
||||
if (have_unrendered_completions) return true;
|
||||
// Common case is no pager.
|
||||
if (this->empty() && rendering.screen_data->empty()) return false;
|
||||
|
||||
return (this->empty() && !rendering.screen_data->empty()) || // Do update after clear().
|
||||
rendering.term_width != this->available_term_width || //
|
||||
rendering.term_height != this->available_term_height || //
|
||||
rendering.selected_completion_idx !=
|
||||
this->visual_selected_completion_index(rendering.rows, rendering.cols) || //
|
||||
rendering.search_field_shown != this->search_field_shown || //
|
||||
*rendering.search_field_line->text() != *this->search_field_line.text() || //
|
||||
rendering.search_field_line->position() != this->search_field_line.position() || //
|
||||
(rendering.remaining_to_disclose > 0 && this->fully_disclosed);
|
||||
}
|
||||
|
||||
void pager_t::update_rendering(page_rendering_t *rendering) {
|
||||
if (rendering_needs_update(*rendering)) {
|
||||
*rendering = this->render();
|
||||
have_unrendered_completions = false;
|
||||
}
|
||||
}
|
||||
|
||||
pager_t::pager_t() : search_field_line_box(new_editable_line()) {}
|
||||
pager_t::~pager_t() = default;
|
||||
|
||||
bool pager_t::empty() const { return unfiltered_completion_infos.empty(); }
|
||||
|
||||
bool pager_t::select_next_completion_in_direction(selection_motion_t direction,
|
||||
const page_rendering_t &rendering) {
|
||||
// Must have something to select.
|
||||
if (this->completion_infos.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selected_completion_idx == PAGER_SELECTION_NONE) {
|
||||
// Handle the case of nothing selected yet.
|
||||
switch (direction) {
|
||||
case selection_motion_t::south:
|
||||
case selection_motion_t::page_south:
|
||||
case selection_motion_t::next:
|
||||
case selection_motion_t::north:
|
||||
case selection_motion_t::prev: {
|
||||
// These directions do something sane.
|
||||
if (direction == selection_motion_t::prev ||
|
||||
direction == selection_motion_t::north) {
|
||||
selected_completion_idx = completion_infos.size() - 1;
|
||||
} else {
|
||||
selected_completion_idx = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case selection_motion_t::page_north:
|
||||
case selection_motion_t::east:
|
||||
case selection_motion_t::west:
|
||||
case selection_motion_t::deselect: {
|
||||
// These do nothing.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ok, we had something selected already. Select something different.
|
||||
size_t new_selected_completion_idx;
|
||||
if (!selection_direction_is_cardinal(direction)) {
|
||||
// Next, previous, or deselect, all easy.
|
||||
if (direction == selection_motion_t::deselect) {
|
||||
new_selected_completion_idx = PAGER_SELECTION_NONE;
|
||||
} else if (direction == selection_motion_t::next) {
|
||||
new_selected_completion_idx = selected_completion_idx + 1;
|
||||
if (new_selected_completion_idx >= completion_infos.size()) {
|
||||
new_selected_completion_idx = 0;
|
||||
}
|
||||
} else if (direction == selection_motion_t::prev) {
|
||||
if (selected_completion_idx == 0) {
|
||||
new_selected_completion_idx = completion_infos.size() - 1;
|
||||
} else {
|
||||
new_selected_completion_idx = selected_completion_idx - 1;
|
||||
}
|
||||
} else {
|
||||
DIE("unknown non-cardinal direction");
|
||||
}
|
||||
} else {
|
||||
// Cardinal directions. We have a completion index; we wish to compute its row and
|
||||
// column.
|
||||
size_t current_row = this->get_selected_row(rendering);
|
||||
size_t current_col = this->get_selected_column(rendering);
|
||||
size_t page_height = std::max(rendering.term_height - 1, static_cast<size_t>(1));
|
||||
|
||||
switch (direction) {
|
||||
case selection_motion_t::page_north: {
|
||||
if (current_row > page_height) {
|
||||
current_row = current_row - page_height;
|
||||
} else {
|
||||
current_row = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case selection_motion_t::north: {
|
||||
// Go up a whole row. If we cycle, go to the previous column.
|
||||
if (current_row > 0) {
|
||||
current_row--;
|
||||
} else {
|
||||
current_row = rendering.rows - 1;
|
||||
if (current_col > 0) {
|
||||
current_col--;
|
||||
} else {
|
||||
current_col = rendering.cols - 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case selection_motion_t::page_south: {
|
||||
if (current_row + page_height < rendering.rows) {
|
||||
current_row += page_height;
|
||||
} else {
|
||||
current_row = rendering.rows - 1;
|
||||
if (current_col * rendering.rows + current_row >= completion_infos.size()) {
|
||||
current_row = (completion_infos.size() - 1) % rendering.rows;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case selection_motion_t::south: {
|
||||
// Go down, unless we are in the last row.
|
||||
// If we go over the last element, wrap to the first.
|
||||
if (current_row + 1 < rendering.rows &&
|
||||
current_col * rendering.rows + current_row + 1 < completion_infos.size()) {
|
||||
current_row++;
|
||||
} else {
|
||||
current_row = 0;
|
||||
current_col = (current_col + 1) % rendering.cols;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case selection_motion_t::east: {
|
||||
// Go east, wrapping to the next row. There is no "row memory," so if we run off
|
||||
// the end, wrap.
|
||||
if (current_col + 1 < rendering.cols &&
|
||||
(current_col + 1) * rendering.rows + current_row <
|
||||
completion_infos.size()) {
|
||||
current_col++;
|
||||
} else {
|
||||
current_col = 0;
|
||||
current_row = (current_row + 1) % rendering.rows;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case selection_motion_t::west: {
|
||||
// Go west, wrapping to the previous row.
|
||||
if (current_col > 0) {
|
||||
current_col--;
|
||||
} else {
|
||||
current_col = rendering.cols - 1;
|
||||
if (current_row > 0) {
|
||||
current_row--;
|
||||
} else {
|
||||
current_row = rendering.rows - 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
DIE("unknown cardinal direction");
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the new index based on the changed row.
|
||||
new_selected_completion_idx = current_col * rendering.rows + current_row;
|
||||
}
|
||||
|
||||
if (selected_completion_idx == new_selected_completion_idx) {
|
||||
return false;
|
||||
}
|
||||
selected_completion_idx = new_selected_completion_idx;
|
||||
}
|
||||
|
||||
// Update suggested_row_start to ensure the selection is visible. suggested_row_start *
|
||||
// rendering.cols is the first suggested visible completion; add the visible completion
|
||||
// count to that to get the last one.
|
||||
size_t visible_row_count = rendering.row_end - rendering.row_start;
|
||||
if (visible_row_count == 0) {
|
||||
return true; // this happens if there was no room to draw the pager
|
||||
}
|
||||
if (selected_completion_idx == PAGER_SELECTION_NONE) {
|
||||
return true; // this should never happen but be paranoid
|
||||
}
|
||||
|
||||
// Ensure our suggested row start is not past the selected row.
|
||||
size_t row_containing_selection = this->get_selected_row(rendering.rows);
|
||||
if (suggested_row_start > row_containing_selection) {
|
||||
suggested_row_start = row_containing_selection;
|
||||
}
|
||||
|
||||
// Ensure our suggested row start is not too early before it.
|
||||
if (suggested_row_start + visible_row_count <= row_containing_selection) {
|
||||
// The user moved south past the bottom completion.
|
||||
if (!fully_disclosed && rendering.remaining_to_disclose > 0) {
|
||||
fully_disclosed = true; // perform disclosure
|
||||
} else {
|
||||
// Scroll
|
||||
suggested_row_start = row_containing_selection - visible_row_count + 1;
|
||||
// Ensure fully_disclosed is set. I think we can hit this case if the user
|
||||
// resizes the window - we don't want to drop back to the disclosed style.
|
||||
fully_disclosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t pager_t::visual_selected_completion_index(size_t rows, size_t cols) const {
|
||||
// No completions -> no selection.
|
||||
if (completion_infos.empty()) {
|
||||
return PAGER_SELECTION_NONE;
|
||||
}
|
||||
|
||||
size_t result = selected_completion_idx;
|
||||
if (result == 0) {
|
||||
return result;
|
||||
}
|
||||
if (rows == 0 || cols == 0) {
|
||||
return PAGER_SELECTION_NONE;
|
||||
}
|
||||
if (result != PAGER_SELECTION_NONE) {
|
||||
// If the selected completion is beyond the last selection, go left by columns until it's
|
||||
// within it. This is how we implement "column memory".
|
||||
while (result >= completion_infos.size() && result >= rows) {
|
||||
result -= rows;
|
||||
}
|
||||
|
||||
// If we are still beyond the last selection, clamp it.
|
||||
if (result >= completion_infos.size()) result = completion_infos.size() - 1;
|
||||
}
|
||||
assert(result == PAGER_SELECTION_NONE || result < completion_infos.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
// It's possible we have no visual selection but are still navigating the contents, e.g. every
|
||||
// completion is filtered.
|
||||
bool pager_t::is_navigating_contents() const {
|
||||
return selected_completion_idx != PAGER_SELECTION_NONE;
|
||||
}
|
||||
|
||||
void pager_t::set_fully_disclosed() { fully_disclosed = true; }
|
||||
|
||||
const completion_t *pager_t::selected_completion(const page_rendering_t &rendering) const {
|
||||
const completion_t *result = nullptr;
|
||||
size_t idx = visual_selected_completion_index(rendering.rows, rendering.cols);
|
||||
if (idx != PAGER_SELECTION_NONE) {
|
||||
result = &*completion_infos.at(idx).representative;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t pager_t::selected_completion_index() const { return selected_completion_idx; }
|
||||
|
||||
void pager_t::set_selected_completion_index(size_t new_index) {
|
||||
// Current users are off by one at most.
|
||||
assert(new_index == PAGER_SELECTION_NONE || new_index <= completion_infos.size());
|
||||
if (new_index == completion_infos.size()) --new_index;
|
||||
selected_completion_idx = new_index;
|
||||
}
|
||||
|
||||
/// Get the selected row and column. Completions are rendered column first, i.e. we go south before
|
||||
/// we go west. So if we have N rows, and our selected index is N + 2, then our row is 2 (mod by N)
|
||||
/// and our column is 1 (divide by N).
|
||||
size_t pager_t::get_selected_row(const page_rendering_t &rendering) const {
|
||||
if (rendering.rows == 0) return PAGER_SELECTION_NONE;
|
||||
|
||||
return rendering.selected_completion_idx == PAGER_SELECTION_NONE
|
||||
? PAGER_SELECTION_NONE
|
||||
: rendering.selected_completion_idx % rendering.rows;
|
||||
}
|
||||
|
||||
size_t pager_t::get_selected_row(size_t rows) const {
|
||||
if (rows == 0) return PAGER_SELECTION_NONE;
|
||||
|
||||
return selected_completion_idx == PAGER_SELECTION_NONE ? PAGER_SELECTION_NONE
|
||||
: selected_completion_idx % rows;
|
||||
}
|
||||
|
||||
size_t pager_t::get_selected_column(const page_rendering_t &rendering) const {
|
||||
if (rendering.rows == 0) return PAGER_SELECTION_NONE;
|
||||
|
||||
return rendering.selected_completion_idx == PAGER_SELECTION_NONE
|
||||
? PAGER_SELECTION_NONE
|
||||
: rendering.selected_completion_idx / rendering.rows;
|
||||
}
|
||||
|
||||
void pager_t::clear() {
|
||||
unfiltered_completion_infos.clear();
|
||||
completion_infos.clear();
|
||||
prefix.clear();
|
||||
highlight_prefix = false;
|
||||
selected_completion_idx = PAGER_SELECTION_NONE;
|
||||
fully_disclosed = false;
|
||||
search_field_shown = false;
|
||||
search_field_line.clear();
|
||||
extra_progress_text.clear();
|
||||
}
|
||||
|
||||
void pager_t::set_search_field_shown(bool flag) { this->search_field_shown = flag; }
|
||||
|
||||
bool pager_t::is_search_field_shown() const { return this->search_field_shown; }
|
||||
|
||||
size_t pager_t::cursor_position() const {
|
||||
size_t result = std::wcslen(SEARCH_FIELD_PROMPT) + this->search_field_line.position();
|
||||
// Clamp it to the right edge.
|
||||
if (available_term_width > 0 && result + 1 > available_term_width) {
|
||||
result = available_term_width - 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
page_rendering_t::page_rendering_t()
|
||||
: screen_data(new_screen_data()), search_field_line(new_editable_line()) {}
|
218
src/pager.h
218
src/pager.h
|
@ -18,219 +18,15 @@ struct termsize_t;
|
|||
|
||||
#define PAGER_SELECTION_NONE static_cast<size_t>(-1)
|
||||
|
||||
/// Represents rendering from the pager.
|
||||
class page_rendering_t {
|
||||
public:
|
||||
size_t term_width{size_t(-1)};
|
||||
size_t term_height{size_t(-1)};
|
||||
size_t rows{0};
|
||||
size_t cols{0};
|
||||
size_t row_start{0};
|
||||
size_t row_end{0};
|
||||
size_t selected_completion_idx{size_t(-1)};
|
||||
rust::Box<ScreenData> screen_data;
|
||||
|
||||
const screen_data_t *screen_data_ffi() const { return &*screen_data; }
|
||||
size_t remaining_to_disclose{0};
|
||||
|
||||
bool search_field_shown{false};
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
rust::Box<editable_line_t> search_field_line;
|
||||
#include "pager.rs.h"
|
||||
#else
|
||||
struct PageRendering;
|
||||
enum class selection_motion_t;
|
||||
struct Pager;
|
||||
#endif
|
||||
|
||||
// Returns a rendering with invalid data, useful to indicate "no rendering".
|
||||
page_rendering_t();
|
||||
page_rendering_t(const page_rendering_t &) = delete;
|
||||
page_rendering_t(page_rendering_t &&) = default;
|
||||
page_rendering_t &operator=(const page_rendering_t &) = delete;
|
||||
page_rendering_t &operator=(page_rendering_t &&) = default;
|
||||
};
|
||||
|
||||
enum class selection_motion_t {
|
||||
// Visual directions.
|
||||
north,
|
||||
east,
|
||||
south,
|
||||
west,
|
||||
page_north,
|
||||
page_south,
|
||||
|
||||
// Logical directions.
|
||||
next,
|
||||
prev,
|
||||
|
||||
// Special value that means deselect.
|
||||
deselect
|
||||
};
|
||||
|
||||
// The space between adjacent completions.
|
||||
#define PAGER_SPACER_STRING L" "
|
||||
#define PAGER_SPACER_STRING_WIDTH 2
|
||||
|
||||
// How many rows we will show in the "initial" pager.
|
||||
#define PAGER_UNDISCLOSED_MAX_ROWS 4
|
||||
|
||||
class pager_t {
|
||||
size_t available_term_width{0};
|
||||
size_t available_term_height{0};
|
||||
|
||||
size_t selected_completion_idx{PAGER_SELECTION_NONE};
|
||||
size_t suggested_row_start{0};
|
||||
|
||||
// Fully disclosed means that we show all completions.
|
||||
bool fully_disclosed{false};
|
||||
|
||||
// Whether we show the search field.
|
||||
bool search_field_shown{false};
|
||||
|
||||
// Returns the index of the completion that should draw selected, using the given number of
|
||||
// columns.
|
||||
size_t visual_selected_completion_index(size_t rows, size_t cols) const;
|
||||
|
||||
public:
|
||||
/// Data structure describing one or a group of related completions.
|
||||
struct comp_t {
|
||||
/// The list of all completion strings this entry applies to.
|
||||
std::vector<wcstring> comp{};
|
||||
/// The description.
|
||||
wcstring desc{};
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
/// The representative completion.
|
||||
rust::Box<completion_t> representative = new_completion();
|
||||
#endif
|
||||
/// The per-character highlighting, used when this is a full shell command.
|
||||
std::vector<highlight_spec_t> colors{};
|
||||
/// On-screen width of the completion string.
|
||||
size_t comp_width{0};
|
||||
/// On-screen width of the description information.
|
||||
size_t desc_width{0};
|
||||
|
||||
comp_t() = default;
|
||||
comp_t(const comp_t &other);
|
||||
comp_t &operator=(const comp_t &other);
|
||||
comp_t(comp_t &&) = default;
|
||||
comp_t &operator=(comp_t &&) = default;
|
||||
|
||||
// Our text looks like this:
|
||||
// completion (description)
|
||||
// Two spaces separating, plus parens, yields 4 total extra space
|
||||
// but only if we have a description of course
|
||||
size_t description_punctuated_width() const {
|
||||
return this->desc_width + (this->desc_width ? 4 : 0);
|
||||
}
|
||||
|
||||
// Returns the preferred width, containing the sum of the
|
||||
// width of the completion, separator, description
|
||||
size_t preferred_width() const {
|
||||
return this->comp_width + this->description_punctuated_width();
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
using comp_info_list_t = std::vector<comp_t>;
|
||||
|
||||
// The filtered list of completion infos.
|
||||
comp_info_list_t completion_infos;
|
||||
|
||||
// The unfiltered list. Note there's a lot of duplication here.
|
||||
comp_info_list_t unfiltered_completion_infos;
|
||||
|
||||
// This tracks if the completion list has been changed since we last rendered. If yes,
|
||||
// then we definitely need to re-render.
|
||||
bool have_unrendered_completions = false;
|
||||
|
||||
wcstring prefix;
|
||||
bool highlight_prefix = false;
|
||||
|
||||
bool completion_try_print(size_t cols, const wcstring &prefix, const comp_info_list_t &lst,
|
||||
page_rendering_t *rendering, size_t suggested_start_row) const;
|
||||
|
||||
void recalc_min_widths(comp_info_list_t *lst) const;
|
||||
void measure_completion_infos(std::vector<comp_t> *infos, const wcstring &prefix) const;
|
||||
|
||||
bool completion_info_passes_filter(const comp_t &info) const;
|
||||
|
||||
void completion_print(size_t cols, const size_t *width_by_column, size_t row_start,
|
||||
size_t row_stop, const wcstring &prefix, const comp_info_list_t &lst,
|
||||
page_rendering_t *rendering) const;
|
||||
rust::Box<Line> completion_print_item(const wcstring &prefix, const comp_t *c, size_t row,
|
||||
size_t column, size_t width, bool secondary,
|
||||
bool selected, page_rendering_t *rendering) const;
|
||||
|
||||
public:
|
||||
// The text of the search field.
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
rust::Box<editable_line_t> search_field_line_box;
|
||||
editable_line_t &search_field_line = *search_field_line_box;
|
||||
#endif
|
||||
|
||||
// Extra text to display at the bottom of the pager.
|
||||
wcstring extra_progress_text{};
|
||||
|
||||
// Sets the set of completions.
|
||||
void set_completions(const completion_list_t &raw_completions);
|
||||
|
||||
// Sets the prefix.
|
||||
void set_prefix(const wcstring &pref, bool highlight = true);
|
||||
|
||||
// Sets the terminal size.
|
||||
void set_term_size(const termsize_t &ts);
|
||||
|
||||
// Changes the selected completion in the given direction according to the layout of the given
|
||||
// rendering. Returns true if the selection changed.
|
||||
bool select_next_completion_in_direction(selection_motion_t direction,
|
||||
const page_rendering_t &rendering);
|
||||
|
||||
// Returns the currently selected completion for the given rendering.
|
||||
const completion_t *selected_completion(const page_rendering_t &rendering) const;
|
||||
|
||||
size_t selected_completion_index() const;
|
||||
void set_selected_completion_index(size_t new_index);
|
||||
|
||||
// Indicates the row and column for the given rendering. Returns -1 if no selection.
|
||||
size_t get_selected_row(const page_rendering_t &rendering) const;
|
||||
size_t get_selected_column(const page_rendering_t &rendering) const;
|
||||
// Indicates the row assuming we render this many rows. Returns -1 if no selection.
|
||||
size_t get_selected_row(size_t rows) const;
|
||||
|
||||
// Produces a rendering of the completions, at the given term size.
|
||||
page_rendering_t render() const;
|
||||
|
||||
// \return true if the given rendering needs to be updated.
|
||||
bool rendering_needs_update(const page_rendering_t &rendering) const;
|
||||
|
||||
// Updates the rendering.
|
||||
void update_rendering(page_rendering_t *rendering);
|
||||
|
||||
// Indicates if there are no completions, and therefore nothing to render.
|
||||
bool empty() const;
|
||||
|
||||
// Clears all completions and the prefix.
|
||||
void clear();
|
||||
|
||||
// Updates the completions list per the filter.
|
||||
void refilter_completions();
|
||||
|
||||
// Sets whether the search field is shown.
|
||||
void set_search_field_shown(bool flag);
|
||||
|
||||
// Gets whether the search field shown.
|
||||
bool is_search_field_shown() const;
|
||||
|
||||
// Indicates if we are navigating our contents.
|
||||
bool is_navigating_contents() const;
|
||||
|
||||
// Become fully disclosed.
|
||||
void set_fully_disclosed();
|
||||
|
||||
// Position of the cursor.
|
||||
size_t cursor_position() const;
|
||||
|
||||
pager_t();
|
||||
~pager_t();
|
||||
};
|
||||
|
||||
void pager_set_term_size_ffi(pager_t &pager, const void *ts);
|
||||
void pager_update_rendering_ffi(pager_t &pager, page_rendering_t &rendering);
|
||||
using page_rendering_t = PageRendering;
|
||||
using pager_t = Pager;
|
||||
|
||||
#endif
|
||||
|
|
|
@ -548,9 +548,10 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
|
|||
/// The current autosuggestion.
|
||||
autosuggestion_t autosuggestion;
|
||||
/// Current pager.
|
||||
pager_t pager;
|
||||
rust::Box<pager_t> pager_box = new_pager();
|
||||
pager_t &pager = *pager_box;
|
||||
/// The output of the pager.
|
||||
page_rendering_t current_page_rendering;
|
||||
rust::Box<page_rendering_t> current_page_rendering = new_page_rendering();
|
||||
/// When backspacing, we temporarily suppress autosuggestions.
|
||||
bool suppress_autosuggestion{false};
|
||||
|
||||
|
@ -626,7 +627,7 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
|
|||
/// field.
|
||||
const editable_line_t *active_edit_line() const {
|
||||
if (this->is_navigating_pager_contents() && this->pager.is_search_field_shown()) {
|
||||
return &this->pager.search_field_line;
|
||||
return this->pager.search_field_line();
|
||||
}
|
||||
return &this->command_line;
|
||||
}
|
||||
|
@ -985,7 +986,7 @@ bool reader_data_t::is_repaint_needed(const std::vector<highlight_spec_t> *mcolo
|
|||
return val;
|
||||
};
|
||||
|
||||
bool focused_on_pager = active_edit_line() == &pager.search_field_line;
|
||||
bool focused_on_pager = active_edit_line() == pager.search_field_line();
|
||||
const layout_data_t &last = this->rendered_layout;
|
||||
return check(force_exec_prompt_and_repaint, L"forced") ||
|
||||
check(*command_line.text() != last.text, L"text") ||
|
||||
|
@ -999,12 +1000,12 @@ bool reader_data_t::is_repaint_needed(const std::vector<highlight_spec_t> *mcolo
|
|||
check(left_prompt_buff != last.left_prompt_buff, L"left_prompt") ||
|
||||
check(mode_prompt_buff != last.mode_prompt_buff, L"mode_prompt") ||
|
||||
check(right_prompt_buff != last.right_prompt_buff, L"right_prompt") ||
|
||||
check(pager.rendering_needs_update(current_page_rendering), L"pager");
|
||||
check(pager.rendering_needs_update(*current_page_rendering), L"pager");
|
||||
}
|
||||
|
||||
layout_data_t reader_data_t::make_layout_data() const {
|
||||
layout_data_t result{};
|
||||
bool focused_on_pager = active_edit_line() == &pager.search_field_line;
|
||||
bool focused_on_pager = active_edit_line() == pager.search_field_line();
|
||||
result.text = *command_line.text();
|
||||
for (auto &color : editable_line_colors(command_line)) {
|
||||
result.colors.push_back(color);
|
||||
|
@ -1012,7 +1013,7 @@ layout_data_t reader_data_t::make_layout_data() const {
|
|||
assert(result.text.size() == result.colors.size());
|
||||
result.position = focused_on_pager ? pager.cursor_position() : command_line.position();
|
||||
result.selection = selection;
|
||||
result.focused_on_pager = (active_edit_line() == &pager.search_field_line);
|
||||
result.focused_on_pager = (active_edit_line() == pager.search_field_line());
|
||||
result.history_search_range = history_search.search_range_if_active();
|
||||
result.autosuggestion = autosuggestion.text;
|
||||
result.left_prompt_buff = left_prompt_buff;
|
||||
|
@ -1073,7 +1074,7 @@ void reader_data_t::paint_layout(const wchar_t *reason) {
|
|||
// Prepend the mode prompt to the left prompt.
|
||||
screen->write(mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line,
|
||||
cmd_line->size(), *ffi_colors, indents, data.position, parser().vars_boxed(),
|
||||
pager, current_page_rendering, data.focused_on_pager);
|
||||
pager, *current_page_rendering, data.focused_on_pager);
|
||||
}
|
||||
|
||||
/// Internal helper function for handling killing parts of text.
|
||||
|
@ -1103,7 +1104,7 @@ void reader_data_t::command_line_changed(const editable_line_t *el) {
|
|||
if (el == &this->command_line) {
|
||||
// Update the gen count.
|
||||
s_generation.store(1 + read_generation_count(), std::memory_order_relaxed);
|
||||
} else if (el == &this->pager.search_field_line) {
|
||||
} else if (el == this->pager.search_field_line()) {
|
||||
if (history_pager_active) {
|
||||
fill_history_pager(history_pager_invocation_t::Anew,
|
||||
history_search_direction_t::Backward);
|
||||
|
@ -1117,7 +1118,7 @@ void reader_data_t::command_line_changed(const editable_line_t *el) {
|
|||
}
|
||||
|
||||
void reader_data_t::maybe_refilter_pager(const editable_line_t *el) {
|
||||
if (el == &this->pager.search_field_line) {
|
||||
if (el == this->pager.search_field_line()) {
|
||||
command_line_changed(el);
|
||||
}
|
||||
}
|
||||
|
@ -1185,14 +1186,14 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why,
|
|||
old_pager_index = pager.selected_completion_index();
|
||||
break;
|
||||
}
|
||||
const wcstring search_term = *pager.search_field_line.text();
|
||||
const wcstring search_term = *pager.search_field_line()->text();
|
||||
auto shared_this = this->shared_from_this();
|
||||
std::function<history_pager_result_t()> func = [=]() {
|
||||
return history_pager_search(**shared_this->history, direction, index, search_term);
|
||||
};
|
||||
std::function<void(const history_pager_result_t &)> completion =
|
||||
[=](const history_pager_result_t &result) {
|
||||
if (search_term != *shared_this->pager.search_field_line.text())
|
||||
if (search_term != *shared_this->pager.search_field_line()->text())
|
||||
return; // Stale request.
|
||||
if (result.matched_commands->empty() && why == history_pager_invocation_t::Advance) {
|
||||
// No more matches, keep the existing ones and flash.
|
||||
|
@ -1207,8 +1208,8 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why,
|
|||
shared_this->history_pager_history_index_start = index;
|
||||
shared_this->history_pager_history_index_end = result.final_index;
|
||||
}
|
||||
shared_this->pager.extra_progress_text =
|
||||
result.have_more_results ? _(L"Search again for more results") : L"";
|
||||
shared_this->pager.set_extra_progress_text(
|
||||
result.have_more_results ? _(L"Search again for more results") : L"");
|
||||
shared_this->pager.set_completions(*result.matched_commands);
|
||||
if (why == history_pager_invocation_t::Refresh) {
|
||||
pager.set_selected_completion_index(*old_pager_index);
|
||||
|
@ -1226,7 +1227,7 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why,
|
|||
void reader_data_t::pager_selection_changed() {
|
||||
ASSERT_IS_MAIN_THREAD();
|
||||
|
||||
const completion_t *completion = this->pager.selected_completion(this->current_page_rendering);
|
||||
const completion_t *completion = this->pager.selected_completion(*this->current_page_rendering);
|
||||
|
||||
// Update the cursor and command line.
|
||||
size_t cursor_pos = this->cycle_cursor_pos;
|
||||
|
@ -2036,7 +2037,8 @@ void reader_data_t::clear_pager() {
|
|||
|
||||
void reader_data_t::select_completion_in_direction(selection_motion_t dir,
|
||||
bool force_selection_change) {
|
||||
bool selection_changed = pager.select_next_completion_in_direction(dir, current_page_rendering);
|
||||
bool selection_changed =
|
||||
pager.select_next_completion_in_direction(dir, *current_page_rendering);
|
||||
if (force_selection_change || selection_changed) {
|
||||
pager_selection_changed();
|
||||
}
|
||||
|
@ -2288,7 +2290,7 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok
|
|||
}
|
||||
|
||||
// Update the pager data.
|
||||
pager.set_prefix(prefix);
|
||||
pager.set_prefix(prefix, true);
|
||||
pager.set_completions(surviving_completions);
|
||||
// Modify the command line to reflect the new pager.
|
||||
pager_selection_changed();
|
||||
|
@ -2953,10 +2955,10 @@ bool check_exit_loop_maybe_warning(reader_data_t *data) {
|
|||
|
||||
static bool selection_is_at_top(const reader_data_t *data) {
|
||||
const pager_t *pager = &data->pager;
|
||||
size_t row = pager->get_selected_row(data->current_page_rendering);
|
||||
size_t row = pager->get_selected_row(*data->current_page_rendering);
|
||||
if (row != 0 && row != PAGER_SELECTION_NONE) return false;
|
||||
|
||||
size_t col = pager->get_selected_column(data->current_page_rendering);
|
||||
size_t col = pager->get_selected_column(*data->current_page_rendering);
|
||||
return !(col != 0 && col != PAGER_SELECTION_NONE);
|
||||
}
|
||||
|
||||
|
@ -2969,7 +2971,7 @@ void reader_data_t::update_commandline_state() const {
|
|||
}
|
||||
snapshot->selection = this->get_selection();
|
||||
snapshot->pager_mode = !this->pager.empty();
|
||||
snapshot->pager_fully_disclosed = this->current_page_rendering.remaining_to_disclose == 0;
|
||||
snapshot->pager_fully_disclosed = this->current_page_rendering->remaining_to_disclose() == 0;
|
||||
snapshot->search_mode = this->history_search.active();
|
||||
snapshot->initialized = true;
|
||||
}
|
||||
|
@ -3420,7 +3422,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
|
|||
(!rls.comp->empty() && !rls.complete_did_insert && rls.last_cmd == rl::complete)) {
|
||||
// The user typed complete more than once in a row. If we are not yet fully
|
||||
// disclosed, then become so; otherwise cycle through our available completions.
|
||||
if (current_page_rendering.remaining_to_disclose > 0) {
|
||||
if (current_page_rendering->remaining_to_disclose() > 0) {
|
||||
pager.set_fully_disclosed();
|
||||
} else {
|
||||
select_completion_in_direction(c == rl::complete ? selection_motion_t::next
|
||||
|
@ -3670,11 +3672,11 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
|
|||
pager.set_prefix(MB_CUR_MAX > 1 ? L"► " : L"> ", false /* highlight */);
|
||||
// Update the search field, which triggers the actual history search.
|
||||
if (!history_search.active() || history_search.search_string().empty()) {
|
||||
insert_string(&pager.search_field_line, *command_line.text());
|
||||
insert_string(pager.search_field_line(), *command_line.text());
|
||||
} else {
|
||||
// If we have an actual history search already going, reuse that term
|
||||
// - this is if the user looks around a bit and decides to switch to the pager.
|
||||
insert_string(&pager.search_field_line, history_search.search_string());
|
||||
insert_string(pager.search_field_line(), history_search.search_string());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -3684,7 +3686,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
|
|||
break;
|
||||
}
|
||||
inputter.function_set_status(true);
|
||||
if (auto completion = pager.selected_completion(current_page_rendering)) {
|
||||
if (auto completion = pager.selected_completion(*current_page_rendering)) {
|
||||
(*history)->remove(*completion->completion());
|
||||
(*history)->save();
|
||||
fill_history_pager(history_pager_invocation_t::Refresh,
|
||||
|
@ -4268,9 +4270,9 @@ bool reader_data_t::handle_execute(readline_loop_state_t &rls) {
|
|||
if (this->history_pager_active &&
|
||||
this->pager.selected_completion_index() == PAGER_SELECTION_NONE) {
|
||||
command_line.push_edit(
|
||||
new_edit(0, command_line.size(), *this->pager.search_field_line.text()),
|
||||
new_edit(0, command_line.size(), *this->pager.search_field_line()->text()),
|
||||
/* allow_coalesce */ false);
|
||||
command_line.set_position(this->pager.search_field_line.position());
|
||||
command_line.set_position(this->pager.search_field_line()->position());
|
||||
}
|
||||
clear_pager();
|
||||
return true;
|
||||
|
@ -4455,7 +4457,7 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
|
|||
}
|
||||
|
||||
// Clear the pager if necessary.
|
||||
bool focused_on_search_field = (active_edit_line() == &pager.search_field_line);
|
||||
bool focused_on_search_field = (active_edit_line() == pager.search_field_line());
|
||||
if (!history_search.active() &&
|
||||
command_ends_paging(readline_cmd, focused_on_search_field)) {
|
||||
clear_pager();
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
#define FISH_SCREEN_H
|
||||
#include "config.h" // IWYU pragma: keep
|
||||
|
||||
class pager_t;
|
||||
class page_rendering_t;
|
||||
struct PageRendering;
|
||||
struct Pager;
|
||||
using page_rendering_t = PageRendering;
|
||||
using pager_t = Pager;
|
||||
|
||||
#if INCLUDE_RUST_HEADERS
|
||||
#include "screen.rs.h"
|
||||
|
|
Loading…
Reference in New Issue
Block a user