Experimental new "inline pager" implementation, reminiscent of zsh. Pager contents now appear below the prompt and disappear when no longer needed. They can also be navigated with arrow keys or tab.

New pager is disabled by default for now. It can be enabled by setting the fish_new_pager variable to 1.

Work and discussed is tracked in https://github.com/fish-shell/fish-shell/issues/1264
This commit is contained in:
ridiculousfish 2014-01-24 18:12:46 -08:00
commit df60c00d16
19 changed files with 1516 additions and 193 deletions

View file

@ -91,7 +91,8 @@ FISH_OBJS := function.o builtin.o complete.o env.o exec.o expand.o \
env_universal.o env_universal_common.o input_common.o event.o \
signal.o io.o parse_util.o common.o screen.o path.o autoload.o \
parser_keywords.o iothread.o color.o postfork.o \
builtin_test.o parse_tree.o parse_productions.o parse_execution.cpp
builtin_test.o parse_tree.o parse_productions.o parse_execution.cpp \
pager.cpp
FISH_INDENT_OBJS := fish_indent.o print_help.o common.o \
parser_keywords.o wutil.o tokenizer.o

View file

@ -149,7 +149,7 @@ static void write_part(const wchar_t *begin,
{
wchar_t *buff = wcsndup(begin, end-begin);
// fwprintf( stderr, L"Subshell: %ls, end char %lc\n", buff, *end );
wcstring out;
wcstring out;
tokenizer_t tok(buff, TOK_ACCEPT_UNFINISHED);
for (; tok_has_next(&tok); tok_next(&tok))
{
@ -213,6 +213,7 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
int cursor_mode = 0;
int line_mode = 0;
int search_mode = 0;
int paging_mode = 0;
const wchar_t *begin, *end;
current_buffer = (wchar_t *)builtin_complete_get_temporary_buffer();
@ -251,71 +252,24 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
static const struct woption
long_options[] =
{
{
L"append", no_argument, 0, 'a'
}
,
{
L"insert", no_argument, 0, 'i'
}
,
{
L"replace", no_argument, 0, 'r'
}
,
{
L"current-job", no_argument, 0, 'j'
}
,
{
L"current-process", no_argument, 0, 'p'
}
,
{
L"current-token", no_argument, 0, 't'
}
,
{
L"current-buffer", no_argument, 0, 'b'
}
,
{
L"cut-at-cursor", no_argument, 0, 'c'
}
,
{
L"function", no_argument, 0, 'f'
}
,
{
L"tokenize", no_argument, 0, 'o'
}
,
{
L"help", no_argument, 0, 'h'
}
,
{
L"input", required_argument, 0, 'I'
}
,
{
L"cursor", no_argument, 0, 'C'
}
,
{
L"line", no_argument, 0, 'L'
}
,
{
L"search-mode", no_argument, 0, 'S'
}
,
{
0, 0, 0, 0
}
}
;
{ L"append", no_argument, 0, 'a' },
{ L"insert", no_argument, 0, 'i' },
{ L"replace", no_argument, 0, 'r' },
{ L"current-job", no_argument, 0, 'j' },
{ L"current-process", no_argument, 0, 'p' },
{ L"current-token", no_argument, 0, 't' },
{ L"current-buffer", no_argument, 0, 'b' },
{ L"cut-at-cursor", no_argument, 0, 'c' },
{ L"function", no_argument, 0, 'f' },
{ L"tokenize", no_argument, 0, 'o' },
{ L"help", no_argument, 0, 'h' },
{ L"input", required_argument, 0, 'I' },
{ L"cursor", no_argument, 0, 'C' },
{ L"line", no_argument, 0, 'L' },
{ L"search-mode", no_argument, 0, 'S' },
{ L"paging-mode", no_argument, 0, 'P' },
{ 0, 0, 0, 0 }
};
int opt_index = 0;
@ -398,6 +352,10 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
search_mode = 1;
break;
case 'P':
paging_mode = 1;
break;
case 'h':
builtin_print_help(parser, argv[0], stdout_buffer);
return 0;
@ -415,7 +373,7 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
/*
Check for invalid switch combinations
*/
if (buffer_part || cut_at_cursor || append_mode || tokenize || cursor_mode || line_mode || search_mode)
if (buffer_part || cut_at_cursor || append_mode || tokenize || cursor_mode || line_mode || search_mode || paging_mode)
{
append_format(stderr_buffer,
BUILTIN_ERR_COMBO,
@ -464,7 +422,7 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
/*
Check for invalid switch combinations
*/
if ((search_mode || line_mode || cursor_mode) && (argc-woptind > 1))
if ((search_mode || line_mode || cursor_mode || paging_mode) && (argc-woptind > 1))
{
append_format(stderr_buffer,
@ -475,7 +433,7 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
return 1;
}
if ((buffer_part || tokenize || cut_at_cursor) && (cursor_mode || line_mode || search_mode))
if ((buffer_part || tokenize || cut_at_cursor) && (cursor_mode || line_mode || search_mode || paging_mode))
{
append_format(stderr_buffer,
BUILTIN_ERR_COMBO,
@ -564,7 +522,12 @@ static int builtin_commandline(parser_t &parser, wchar_t **argv)
if (search_mode)
{
return !reader_search_mode();
return ! reader_search_mode();
}
if (paging_mode)
{
return ! reader_has_pager_contents();
}

View file

@ -87,6 +87,37 @@ enum
};
typedef unsigned int escape_flags_t;
/* Directions */
enum selection_direction_t
{
/* visual directions */
direction_north,
direction_east,
direction_south,
direction_west,
/* logical directions */
direction_next,
direction_prev,
/* special value that means deselect */
direction_deselect
};
inline bool selection_direction_is_cardinal(selection_direction_t dir)
{
switch (dir)
{
case direction_north:
case direction_east:
case direction_south:
case direction_west:
return true;
default:
return false;
}
}
/**
Helper macro for errors
*/

View file

@ -57,6 +57,16 @@ If \c commandline is called during a call to complete a given string
using <code>complete -C STRING</code>, \c commandline will consider the
specified string to be the current contents of the command line.
The following options output metadata about the commandline state:
- \c -L or \c --line print the line that the cursor is on, with the topmost
line starting at 1
- \c -S or \c --search-mode evaluates to true if the commandline is performing
a history search
- \c -P or \c --paging-mode evaluates to true if the commandline is showing
pager contents, such as tab completions
\subsection commandline-example Example
<tt>commandline -j $history[3]</tt> replaces the job under the cursor with the

View file

@ -62,6 +62,7 @@
D01A2D24169B736200767098 /* man1 in Copy Files */ = {isa = PBXBuildFile; fileRef = D01A2D23169B730A00767098 /* man1 */; };
D01A2D25169B737700767098 /* man1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = D01A2D23169B730A00767098 /* man1 */; };
D031890C15E36E4600D9CC39 /* base in Resources */ = {isa = PBXBuildFile; fileRef = D031890915E36D9800D9CC39 /* base */; };
D032388B1849D1980032CF2C /* pager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D03238891849D1980032CF2C /* pager.cpp */; };
D033781115DC6D4C00A634BA /* completions in CopyFiles */ = {isa = PBXBuildFile; fileRef = D025C02715D1FEA100B9DB63 /* completions */; };
D033781215DC6D5200A634BA /* functions in CopyFiles */ = {isa = PBXBuildFile; fileRef = D025C02815D1FEA100B9DB63 /* functions */; };
D033781315DC6D5400A634BA /* tools in CopyFiles */ = {isa = PBXBuildFile; fileRef = D025C02915D1FEA100B9DB63 /* tools */; };
@ -386,6 +387,8 @@
D025C02815D1FEA100B9DB63 /* functions */ = {isa = PBXFileReference; lastKnownFileType = folder; name = functions; path = share/functions; sourceTree = "<group>"; };
D025C02915D1FEA100B9DB63 /* tools */ = {isa = PBXFileReference; lastKnownFileType = folder; name = tools; path = share/tools; sourceTree = "<group>"; };
D031890915E36D9800D9CC39 /* base */ = {isa = PBXFileReference; lastKnownFileType = text; path = base; sourceTree = BUILT_PRODUCTS_DIR; };
D03238891849D1980032CF2C /* pager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = pager.cpp; sourceTree = "<group>"; };
D032388A1849D1980032CF2C /* pager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pager.h; sourceTree = "<group>"; };
D03EE83814DF88B200FC7150 /* lru.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = lru.h; sourceTree = "<group>"; };
D052D8091868F7FC003ABCBD /* parse_execution.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = parse_execution.cpp; sourceTree = "<group>"; };
D052D80A1868F7FC003ABCBD /* parse_execution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = parse_execution.h; sourceTree = "<group>"; };
@ -699,6 +702,8 @@
D0A0855013B3ACEE0099B651 /* mimedb.cpp */,
D0A0851A13B3ACEE0099B651 /* output.h */,
D0A0855113B3ACEE0099B651 /* output.cpp */,
D032388A1849D1980032CF2C /* pager.h */,
D03238891849D1980032CF2C /* pager.cpp */,
D0A0851B13B3ACEE0099B651 /* parse_util.h */,
D0A0855213B3ACEE0099B651 /* parse_util.cpp */,
D0A0851C13B3ACEE0099B651 /* parser_keywords.h */,
@ -1254,6 +1259,7 @@
D0D02A7915983888008E62BD /* intern.cpp in Sources */,
D0D02A7A15983916008E62BD /* env_universal.cpp in Sources */,
D0D02A7B15983928008E62BD /* env_universal_common.cpp in Sources */,
D032388B1849D1980032CF2C /* pager.cpp in Sources */,
D0D02A89159839DF008E62BD /* fish.cpp in Sources */,
D0C52F371765284C00BFAB82 /* parse_tree.cpp in Sources */,
D0FE8EE8179FB760008C9F21 /* parse_productions.cpp in Sources */,

View file

@ -60,6 +60,7 @@
#include "signal.h"
#include "parse_tree.h"
#include "parse_util.h"
#include "pager.h"
static const char * const * s_arguments;
static int s_test_run_count = 0;
@ -1098,6 +1099,99 @@ static void test_path()
if (! paths_are_equivalent(L"/", L"/")) err(L"Bug in canonical PATH code on line %ld", (long)__LINE__);
}
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".
*/
completion_list_t completions;
for (size_t i=0; i < 19; i++)
{
append_completion(completions, L"abcdefghij");
}
pager_t pager;
pager.set_completions(completions);
pager.set_term_size(80, 24);
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_direction_t dir;
size_t sel;
}
cmds[] =
{
/* Tab completion to get into the list */
{direction_next, 0},
/* Westward motion in upper left wraps along the top row */
{direction_west, 16},
{direction_east, 1},
/* "Next" motion goes down the column */
{direction_next, 2},
{direction_next, 3},
{direction_west, 18},
{direction_east, 3},
{direction_east, 7},
{direction_east, 11},
{direction_east, 15},
{direction_east, 3},
{direction_west, 18},
{direction_east, 3},
/* Eastward motion wraps along the bottom, westward goes to the prior column */
{direction_east, 7},
{direction_east, 11},
{direction_east, 15},
{direction_east, 3},
/* Column memory */
{direction_west, 18},
{direction_south, 15},
{direction_north, 18},
{direction_west, 14},
{direction_south, 15},
{direction_north, 14}
};
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);
}
}
}
enum word_motion_t
{
word_motion_left,
@ -2716,6 +2810,7 @@ int main(int argc, char **argv)
if (should_test_function("abbreviations")) test_abbreviations();
if (should_test_function("test")) test_test();
if (should_test_function("path")) test_path();
if (should_test_function("pager_navigation")) test_pager_navigation();
if (should_test_function("word_motion")) test_word_motion();
if (should_test_function("is_potential_path")) test_is_potential_path();
if (should_test_function("colors")) test_colors();

View file

@ -61,7 +61,14 @@ static const wchar_t * const highlight_var[] =
L"fish_color_escape",
L"fish_color_quote",
L"fish_color_redirection",
L"fish_color_autosuggestion"
L"fish_color_autosuggestion",
L"fish_pager_color_prefix",
L"fish_pager_color_completion",
L"fish_pager_color_description",
L"fish_pager_color_progress",
L"fish_pager_color_secondary"
};
/* If the given path looks like it's relative to the working directory, then prepend that working directory. */
@ -355,6 +362,9 @@ rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background)
{
rgb_color_t result = rgb_color_t::normal();
/* If sloppy_background is set, then we look at the foreground color even if is_background is set */
bool treat_as_background = is_background && ! (highlight & highlight_modifier_sloppy_background);
/* Get the primary variable */
size_t idx = highlight_get_primary(highlight);
if (idx >= VAR_COUNT)
@ -370,9 +380,9 @@ rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background)
val_wstr = env_get_string(highlight_var[0]);
if (! val_wstr.missing())
result = parse_color(val_wstr, is_background);
result = parse_color(val_wstr, treat_as_background);
/* Handle modifiers. Just one for now */
/* Handle modifiers. */
if (highlight & highlight_modifier_valid_path)
{
env_var_t val2_wstr = env_get_string(L"fish_color_valid_path");

View file

@ -29,10 +29,18 @@ enum
highlight_spec_redirection, //redirection
highlight_spec_autosuggestion, //autosuggestion
// Pager support
highlight_spec_pager_prefix,
highlight_spec_pager_completion,
highlight_spec_pager_description,
highlight_spec_pager_progress,
highlight_spec_pager_secondary,
HIGHLIGHT_SPEC_PRIMARY_MASK = 0xFF,
/* The following values are modifiers */
highlight_modifier_valid_path = 0x100,
highlight_modifier_sloppy_background = 0x200, //hackish, indicates that we should treat a foreground color as background, per certain historical behavior
/* Very special value */
highlight_spec_invalid = 0xFFFF
@ -47,6 +55,7 @@ inline highlight_spec_t highlight_get_primary(highlight_spec_t val)
inline highlight_spec_t highlight_make_background(highlight_spec_t val)
{
assert(val >> 16 == 0); //should have nothing in upper bits, otherwise this is already a background
return val << 16;
}

872
pager.cpp Normal file
View file

@ -0,0 +1,872 @@
#include "config.h"
#include "pager.h"
#include "highlight.h"
#include "input_common.h"
#include <vector>
#include <map>
#define PAGER_SELECTION_NONE ((size_t)(-1))
typedef pager_t::comp_t comp_t;
typedef std::vector<completion_t> completion_list_t;
typedef std::vector<comp_t> comp_info_list_t;
/** The minimum width (in characters) the terminal may have for fish_pager to not refuse showing the completions */
#define PAGER_MIN_WIDTH 16
/** The maximum number of columns of completion to attempt to fit onto the screen */
#define PAGER_MAX_COLS 6
/* Returns numer / denom, rounding up */
static size_t divide_round_up(size_t numer, size_t denom)
{
return numer / denom + (numer % denom ? 1 : 0);
}
/**
This function calculates the minimum width for each completion
entry in the specified array_list. This width depends on the
terminal size, so this function should be called when the terminal
changes size.
*/
void pager_t::recalc_min_widths(comp_info_list_t * lst) const
{
for (size_t i=0; i<lst->size(); i++)
{
comp_t *c = &lst->at(i);
c->min_width = mini(c->desc_width, maxi(0, available_term_width/3 - 2)) +
mini(c->desc_width, maxi(0, available_term_width/5 - 4)) +4;
}
}
/**
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 ellisiszed even if the string fits but takes up the whole space.
*/
static int print_max(const wcstring &str, highlight_spec_t color, int max, bool has_more, line_t *line)
{
int written = 0;
for (size_t i=0; i < str.size(); i++)
{
wchar_t c = str.at(i);
if (written + wcwidth(c) > max)
break;
if ((written + wcwidth(c) == max) && (has_more || i + 1 < str.size()))
{
line->append(ellipsis_char, color);
written += wcwidth(ellipsis_char);
break;
}
line->append(c, color);
written += wcwidth(c);
}
return written;
}
/**
Print the specified item using at the specified amount of space
*/
line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, size_t column, int width, bool secondary, bool selected, page_rendering_t *rendering) const
{
int comp_width=0, desc_width=0;
int written=0;
line_t line_data;
if (c->pref_width <= width)
{
/*
The entry fits, we give it as much space as it wants
*/
comp_width = c->comp_width;
desc_width = c->desc_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.
*/
int desc_all = c->desc_width?c->desc_width+4:0;
comp_width = maxi(mini(c->comp_width, 2*(width-4)/3), width - desc_all);
if (c->desc_width)
desc_width = width-comp_width-4;
}
int bg_color = secondary ? highlight_spec_pager_secondary : highlight_spec_normal;
if (selected)
{
bg_color = highlight_spec_search_match;
}
for (size_t i=0; i<c->comp.size(); i++)
{
const wcstring &comp = c->comp.at(i);
if (i != 0)
written += print_max(PAGER_SPACER_STRING, highlight_spec_normal, comp_width - written, true /* has_more */, &line_data);
int packed_color = highlight_spec_pager_prefix | highlight_make_background(bg_color);
written += print_max(prefix, packed_color, comp_width - written, ! comp.empty(), &line_data);
packed_color = highlight_spec_pager_completion | highlight_make_background(bg_color);
written += print_max(comp, packed_color, comp_width - written, i + 1 < c->comp.size(), &line_data);
}
if (desc_width)
{
int packed_color = highlight_spec_pager_description | highlight_make_background(bg_color);
while (written < (width-desc_width-2)) //the 2 here refers to the parenthesis below
{
written += print_max(L" ", packed_color, 1, false, &line_data);
}
written += print_max(L"(", packed_color, 1, false, &line_data);
written += print_max(c->desc, packed_color, desc_width, false, &line_data);
written += print_max(L")", packed_color, 1, false, &line_data);
}
else
{
while (written < width)
{
written += print_max(L" ", 0, 1, false, &line_data);
}
}
return line_data;
}
/**
Print the specified part of the completion list, using the
specified column offsets and quoting style.
\param l The list of completions to print
\param cols number of columns to print in
\param width 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
*/
void pager_t::completion_print(size_t cols, int *width_per_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_start >= 0);
assert(row_stop >= row_start);
rendering->row_start = row_start;
rendering->row_end = row_stop;
size_t rows = (lst.size()-1)/cols+1;
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++)
{
int is_last = (col==(cols-1));
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" */
line_t line = completion_print_item(prefix, el, row, col, width_per_column[col] - (is_last ? 0 : PAGER_SPACER_STRING_WIDTH), row%2, is_selected, rendering);
/* If there's more to come, append two spaces */
if (col + 1 < cols)
{
line.append(PAGER_SPACER_STRING, 0);
}
/* 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::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 wcstring &prefix)
{
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_ALL | ESCAPE_NO_QUOTED));
// Append the mangled description
comp_info->desc = comp.description;
mangle_1_completion_description(&comp_info->desc);
// Set the representative completion
comp_info->representative = comp;
}
return result;
}
void pager_t::measure_completion_infos(comp_info_list_t *infos, const wcstring &prefix) const
{
size_t prefix_len = my_wcswidth(prefix.c_str());
for (size_t i=0; i < infos->size(); i++)
{
comp_t *comp = &infos->at(i);
// Compute comp_width
const wcstring_list_t &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;
comp->comp_width += prefix_len + my_wcswidth(comp_strings.at(j).c_str());
}
// Compute desc_width
comp->desc_width = my_wcswidth(comp->desc.c_str());
// Compute preferred width
comp->pref_width = comp->comp_width + comp->desc_width + (comp->desc_width?4:0);
}
recalc_min_widths(infos);
}
void pager_t::set_completions(const completion_list_t &raw_completions)
{
// Get completion infos out of it
completion_infos = process_completions_into_infos(raw_completions, prefix.c_str());
// Maybe join them
if (prefix == L"-")
join_completions(&completion_infos);
// Compute their various widths
measure_completion_infos(&completion_infos, prefix);
}
void pager_t::set_prefix(const wcstring &pref)
{
prefix = pref;
}
void pager_t::set_term_size(int w, int h)
{
assert(w > 0);
assert(h > 0);
available_term_width = w;
available_term_height = h;
recalc_min_widths(&completion_infos);
}
/**
Try to print the list of completions l with the prefix prefix using
cols as the number of columns. Return true if the completion list was
printed, false if the terminal is to 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
{
/*
The calculated preferred width of each column
*/
int pref_width[PAGER_MAX_COLS] = {0};
/*
The calculated minimum width of each column
*/
int min_width[PAGER_MAX_COLS] = {0};
/*
If the list can be printed with this width, width will contain the width of each column
*/
int *width=pref_width;
/* Set to one if the list should be printed at this width */
bool print = false;
/* Compute the effective term width and term height, accounting for disclosure */
int term_width = this->available_term_width;
int term_height = this->available_term_height - 1; // we always subtract 1 to make room for a comment row
if (! this->fully_disclosed)
term_height = mini(term_height, 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;
}
int pref_tot_width=0;
int min_tot_width = 0;
/* Skip completions on tiny terminals */
if (term_width < PAGER_MIN_WIDTH)
return true;
/* Calculate how wide the list would be */
for (long col = 0; col < cols; col++)
{
for (long row = 0; row<row_count; row++)
{
int pref,min;
const comp_t *c;
if (lst.size() <= col*row_count + row)
continue;
c = &lst.at(col*row_count + row);
pref = c->pref_width;
min = c->min_width;
if (col != cols-1)
{
pref += 2;
min += 2;
}
min_width[col] = maxi(min_width[col],
min);
pref_width[col] = maxi(pref_width[col],
pref);
}
min_tot_width += min_width[col];
pref_tot_width += pref_width[col];
}
/*
Force fit if one column
*/
if (cols == 1)
{
if (pref_tot_width > term_width)
{
pref_width[0] = term_width;
}
width = pref_width;
print = true;
}
else if (pref_tot_width <= term_width)
{
/* Terminal is wide enough. Print the list! */
width = pref_width;
print = true;
}
else
{
long next_rows = (lst.size()-1)/(cols-1)+1;
/* fwprintf( stderr,
L"cols %d, min_tot %d, term %d, rows=%d, nextrows %d, termrows %d, diff %d\n",
cols,
min_tot_width, term_width,
rows, next_rows, term_height,
pref_tot_width-term_width );
*/
if (min_tot_width < term_width &&
(((row_count < term_height) && (next_rows >= term_height)) ||
(pref_tot_width-term_width< 4 && cols < 3)))
{
/*
Terminal almost wide enough, or squeezing makes the
whole list fit on-screen.
This part of the code is really important. People hate
having to scroll through the completion list. In cases
where there are a huge number of completions, it can't
be helped, but it is not uncommon for the completions to
_almost_ fit on one screen. In those cases, it is almost
always desirable to 'squeeze' the completions into a
single page.
If we are using N columns and can get everything to
fit using squeezing, but everything would also fit
using N-1 columns, don't try.
*/
int tot_width = min_tot_width;
width = min_width;
while (tot_width < term_width)
{
for (long i=0; (i<cols) && (tot_width < term_width); i++)
{
if (width[i] < pref_width[i])
{
width[i]++;
tot_width++;
}
}
}
print = true;
}
}
if (print)
{
/* 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 = mini(suggested_start_row, last_starting_row);
stop_row = start_row + term_height;
assert(start_row >= 0 && 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, start_row, stop_row, prefix, lst, rendering);
/* Ellipsis helper string. Either empty or containing the ellipsis char */
const wchar_t ellipsis_string[] = {ellipsis_char == L'\x2026' ? L'\x2026' : L'\0', L'\0'};
/* Add the progress line. It's a "more to disclose" line if necessary, or a row listing if it's scrollable; otherwise ignore it */
wcstring progress_text;
if (rendering->remaining_to_disclose == 1)
{
/* I don't expect this case to ever happen */
progress_text = format_string(L"%lsand 1 more row", ellipsis_string);
}
else if (rendering->remaining_to_disclose > 1)
{
progress_text = format_string(L"%lsand %lu more rows", ellipsis_string, (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);
}
if (! progress_text.empty())
{
line_t &line = rendering->screen_data.add_line();
print_max(progress_text.c_str(), highlight_spec_pager_progress | highlight_make_background(highlight_spec_pager_progress), term_width, true /* has_more */, &line);
}
}
return print;
}
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;
if (! this->empty())
{
for (int 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 (min_cols_required_for_rows < cols)
{
/* Next iteration will be better, so skip this one */
continue;
}
rendering.cols = (size_t)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;
}
void pager_t::update_rendering(page_rendering_t *rendering) const
{
if (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 = this->render();
}
}
pager_t::pager_t() : available_term_width(0), available_term_height(0), selected_completion_idx(PAGER_SELECTION_NONE), suggested_row_start(0), fully_disclosed(false)
{
}
bool pager_t::empty() const
{
return completion_infos.empty();
}
const completion_t *pager_t::select_next_completion_in_direction(selection_direction_t direction, const page_rendering_t &rendering)
{
/* Must have something to select */
if (this->empty())
{
return NULL;
}
/* Handle the case of nothing selected yet */
if (selected_completion_idx == PAGER_SELECTION_NONE)
{
switch (direction)
{
/* These directions do something sane */
case direction_south:
case direction_next:
case direction_prev:
if (direction == direction_prev)
{
selected_completion_idx = completion_infos.size() - 1;
}
else
{
selected_completion_idx = 0;
}
return selected_completion(rendering);
/* These do nothing */
case direction_north:
case direction_east:
case direction_west:
case direction_deselect:
default:
return NULL;
}
}
/* Ok, we had something selected already. Select something different. */
size_t new_selected_completion_idx = selected_completion_idx;
if (! selection_direction_is_cardinal(direction))
{
/* Next, previous, or deselect, all easy */
if (direction == direction_deselect)
{
new_selected_completion_idx = PAGER_SELECTION_NONE;
}
else if (direction == direction_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 == direction_prev)
{
if (selected_completion_idx == 0)
{
new_selected_completion_idx = completion_infos.size() - 1;
}
else
{
new_selected_completion_idx = selected_completion_idx - 1;
}
}
else
{
assert(0 && "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);
switch (direction)
{
case direction_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--;
}
break;
}
case direction_south:
{
/* Go down, unless we are in the last row. Note that this means that we may set selected_completion_idx to an out-of-bounds value if the last row is incomplete; this is a feature (it allows "last column memory"). */
if (current_row + 1 < rendering.rows)
{
current_row++;
}
else
{
current_row = 0;
if (current_col + 1 < rendering.cols)
current_col++;
}
break;
}
case direction_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;
if (current_row + 1 < rendering.rows)
current_row++;
}
break;
}
case direction_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--;
}
break;
}
default:
assert(0 && "Unknown cardinal direction");
break;
}
/* Compute the new index based on the changed row */
new_selected_completion_idx = current_col * rendering.rows + current_row;
}
if (new_selected_completion_idx != selected_completion_idx)
{
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 && selected_completion_idx != PAGER_SELECTION_NONE) //paranoia
{
size_t row_containing_selection = this->get_selected_row(rendering);
/* Ensure our suggested row start is not past the selected row */
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)
{
/* Perform disclosure */
fully_disclosed = true;
}
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 selected_completion(rendering);
}
else
{
return NULL;
}
}
size_t pager_t::visual_selected_completion_index(size_t rows, size_t cols) const
{
size_t result = selected_completion_idx;
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;
}
}
assert(result == PAGER_SELECTION_NONE || result < completion_infos.size());
return result;
}
const completion_t *pager_t::selected_completion(const page_rendering_t &rendering) const
{
const completion_t * result = NULL;
size_t idx = visual_selected_completion_index(rendering.rows, rendering.cols);
if (idx != PAGER_SELECTION_NONE)
{
result = &completion_infos.at(idx).representative;
}
return result;
}
/* 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
{
return selected_completion_idx == PAGER_SELECTION_NONE ? PAGER_SELECTION_NONE : selected_completion_idx % rendering.rows;
}
size_t pager_t::get_selected_column(const page_rendering_t &rendering) const
{
return selected_completion_idx == PAGER_SELECTION_NONE ? PAGER_SELECTION_NONE : selected_completion_idx / rendering.rows;
}
void pager_t::clear()
{
completion_infos.clear();
prefix.clear();
selected_completion_idx = PAGER_SELECTION_NONE;
fully_disclosed = false;
}
/* Constructor */
page_rendering_t::page_rendering_t() : term_width(-1), term_height(-1), rows(0), cols(0), row_start(0), row_end(0), selected_completion_idx(-1), remaining_to_disclose(0)
{
}

131
pager.h Normal file
View file

@ -0,0 +1,131 @@
/** \file pager.h
Pager support
*/
#include "complete.h"
#include "screen.h"
/* Represents rendering from the pager */
class page_rendering_t
{
public:
int term_width;
int term_height;
size_t rows;
size_t cols;
size_t row_start;
size_t row_end;
size_t selected_completion_idx;
screen_data_t screen_data;
size_t remaining_to_disclose;
/* Returns a rendering with invalid data, useful to indicate "no rendering" */
page_rendering_t();
};
/* 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
typedef std::vector<completion_t> completion_list_t;
page_rendering_t render_completions(const completion_list_t &raw_completions, const wcstring &prefix);
class pager_t
{
int available_term_width;
int available_term_height;
size_t selected_completion_idx;
size_t suggested_row_start;
/* Fully disclosed means that we show all completions */
bool fully_disclosed;
/* 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;
/** Data structure describing one or a group of related completions */
public:
struct comp_t
{
/** The list of all completin strings this entry applies to */
wcstring_list_t comp;
/** The description */
wcstring desc;
/** The representative completion */
completion_t representative;
/** On-screen width of the completion string */
int comp_width;
/** On-screen width of the description information */
int desc_width;
/** Preferred total width */
int pref_width;
/** Minimum acceptable width */
int min_width;
comp_t() : comp(), desc(), representative(L""), comp_width(0), desc_width(0), pref_width(0), min_width(0)
{
}
};
private:
typedef std::vector<comp_t> comp_info_list_t;
comp_info_list_t completion_infos;
wcstring prefix;
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;
void completion_print(size_t cols, int *width_per_column, size_t row_start, size_t row_stop, const wcstring &prefix, const comp_info_list_t &lst, page_rendering_t *rendering) const;
line_t completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, size_t column, int width, bool secondary, bool selected, page_rendering_t *rendering) const;
public:
/* Sets the set of completions */
void set_completions(const completion_list_t &comp);
/* Sets the prefix */
void set_prefix(const wcstring &pref);
/* Sets the terminal width and height */
void set_term_size(int w, int h);
/* Changes the selected completion in the given direction according to the layout of the given rendering. Returns the newly selected completion if it changed, NULL if nothing was selected or it did not change. */
const completion_t *select_next_completion_in_direction(selection_direction_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;
/* 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;
/* Produces a rendering of the completions, at the given term size */
page_rendering_t render() const;
/* Updates the rendering if it's stale */
void update_rendering(page_rendering_t *rendering) const;
/* Indicates if there are no completions, and therefore nothing to render */
bool empty() const;
/* Clears all completions and the prefix */
void clear();
/* Constructor */
pager_t();
};

View file

@ -126,7 +126,7 @@ RESOLVE(statement)
if (token1.type == parse_token_type_string)
{
// If we are a function, then look for help arguments
// Othewrise, if the next token looks like an option (starts with a dash), then parse it as a decorated statement
// Otherwise, if the next token looks like an option (starts with a dash), then parse it as a decorated statement
if (token1.keyword == parse_keyword_function && token2.is_help_argument)
{
return 4;

View file

@ -3122,3 +3122,16 @@ bool parser_use_ast(void)
return from_string<bool>(var);
}
}
bool pager_use_inline(void)
{
env_var_t var = env_get_string(L"fish_new_pager");
if (var.missing_or_empty())
{
return 0;
}
else
{
return from_string<bool>(var);
}
}

View file

@ -547,6 +547,7 @@ public:
/* Temporary */
bool parser_use_ast(void);
bool pager_use_inline(void);
#endif

View file

@ -100,6 +100,7 @@ commence.
#include "parse_util.h"
#include "parser_keywords.h"
#include "parse_tree.h"
#include "pager.h"
/**
Maximum length of prefix string when printing completion
@ -197,6 +198,15 @@ public:
/** String containing the autosuggestion */
wcstring autosuggestion;
/** Current pager */
pager_t pager;
/** Current page rendering */
page_rendering_t current_page_rendering;
/** Whether we are navigating the pager */
bool is_navigating_pager;
/** Whether autosuggesting is allowed at all */
bool allow_autosuggestion;
@ -330,6 +340,7 @@ public:
/** Constructor */
reader_data_t() :
is_navigating_pager(0),
allow_autosuggestion(0),
suppress_autosuggestion(0),
expand_abbreviations(0),
@ -351,6 +362,9 @@ public:
}
};
/* Sets the command line contents, without clearing the pager */
static void reader_set_buffer_maintaining_pager(const wcstring &b, size_t pos);
/**
The current interactive reading context
*/
@ -531,6 +545,11 @@ static void reader_repaint()
std::vector<int> indents = data->indents;
indents.resize(len);
// Re-render our completions page if necessary
// We set the term size to 1 less than the true term height. This means we will always show the (bottom) line of the prompt.
data->pager.set_term_size(maxi(1, common_get_width()), maxi(1, common_get_height() - 1));
data->pager.update_rendering(&data->current_page_rendering);
s_write(&data->screen,
data->left_prompt_buff,
data->right_prompt_buff,
@ -538,7 +557,8 @@ static void reader_repaint()
data->command_length(),
&colors[0],
&indents[0],
data->buff_pos);
data->buff_pos,
data->current_page_rendering);
data->repaint_needed = false;
}
@ -1195,7 +1215,7 @@ static void completion_insert(const wchar_t *val, complete_flags_t flags)
{
size_t cursor = data->buff_pos;
wcstring new_command_line = completion_apply_to_command_line(val, flags, data->command_line, &cursor, false /* not append only */);
reader_set_buffer(new_command_line, cursor);
reader_set_buffer_maintaining_pager(new_command_line, cursor);
/* Since we just inserted a completion, don't immediately do a new autosuggestion */
data->suppress_autosuggestion = true;
@ -1509,6 +1529,49 @@ static void accept_autosuggestion(bool full)
}
}
static bool is_navigating_pager_contents()
{
return data && data->pager.selected_completion(data->current_page_rendering) != NULL;
}
/* Ensure we have no pager contents */
static void clear_pager()
{
if (data)
{
data->pager.clear();
data->current_page_rendering = page_rendering_t();
reader_repaint_needed();
}
}
static void select_completion_in_direction(enum selection_direction_t dir, const wcstring &cycle_command_line, size_t cycle_cursor_pos)
{
const completion_t *next_comp = data->pager.select_next_completion_in_direction(dir, data->current_page_rendering);
if (next_comp != NULL || dir == direction_deselect)
{
/* Update the cursor and command line */
size_t cursor_pos = cycle_cursor_pos;
wcstring new_cmd_line;
if (dir == direction_deselect)
{
new_cmd_line = cycle_command_line;
}
else
{
new_cmd_line = completion_apply_to_command_line(next_comp->completion, next_comp->flags, cycle_command_line, &cursor_pos, false);
}
reader_set_buffer_maintaining_pager(new_cmd_line, cursor_pos);
/* Since we just inserted a completion, don't immediately do a new autosuggestion */
data->suppress_autosuggestion = true;
/* Trigger repaint (see #765) */
reader_repaint_needed();
}
}
/**
Flash the screen. This function only changed the color of the
current line, since the flash_screen sequnce is rather painful to
@ -1627,40 +1690,6 @@ static void prioritize_completions(std::vector<completion_t> &comp)
sort(comp.begin(), comp.end(), compare_completions_by_match_type);
}
/* Given a list of completions, get the completion at an index past *inout_idx, and then increment it. inout_idx should be initialized to (size_t)(-1) for the first call. */
static const completion_t *cycle_competions(const std::vector<completion_t> &comp, const wcstring &command_line, size_t *inout_idx)
{
const size_t size = comp.size();
if (size == 0)
return NULL;
// note start_idx will be set to -1 initially, so that when it gets incremented we start at 0
const size_t start_idx = *inout_idx;
size_t idx = start_idx;
const completion_t *result = NULL;
size_t remaining = comp.size();
while (remaining--)
{
/* Bump the index */
idx = (idx + 1) % size;
/* Get the completion */
const completion_t &c = comp.at(idx);
/* Try this completion */
if (!(c.flags & COMPLETE_REPLACES_TOKEN) || reader_can_replace(command_line, c.flags))
{
/* Success */
result = &c;
break;
}
}
*inout_idx = idx;
return result;
}
/**
Handle the list of completions. This means the following:
@ -1843,22 +1872,32 @@ static bool handle_completions(const std::vector<completion_t> &comp)
prefix.append(data->command_line, prefix_start + len - PREFIX_MAX_LEN, PREFIX_MAX_LEN);
}
wchar_t quote;
parse_util_get_parameter_info(data->command_line, data->buff_pos, &quote, NULL, NULL);
bool is_quoted = (quote != L'\0');
if (pager_use_inline())
{
int is_quoted;
/* Inline pager */
data->pager.set_prefix(prefix);
data->pager.set_completions(surviving_completions);
wchar_t quote;
parse_util_get_parameter_info(data->command_line, data->buff_pos, &quote, NULL, NULL);
is_quoted = (quote != L'\0');
/* Clear the autosuggestion from the old commandline before abandoning it (see #561) */
/* Invalidate our rendering */
data->current_page_rendering = page_rendering_t();
}
else
{
/* Classic pager. Clear the autosuggestion from the old commandline before abandoning it (see #561) */
if (! data->autosuggestion.empty())
reader_repaint_without_autosuggestion();
write_loop(1, "\n", 1);
run_pager(prefix, is_quoted, surviving_completions);
s_reset(&data->screen, screen_reset_abandon_line);
}
s_reset(&data->screen, screen_reset_abandon_line);
reader_repaint();
success = false;
}
@ -2353,11 +2392,9 @@ history_t *reader_get_history(void)
return data ? data->history : NULL;
}
void reader_set_buffer(const wcstring &b, size_t pos)
/* Sets the command line contents, without clearing the pager */
static void reader_set_buffer_maintaining_pager(const wcstring &b, size_t pos)
{
if (!data)
return;
/* Callers like to pass us pointers into ourselves, so be careful! I don't know if we can use operator= with a pointer to our interior, so use an intermediate. */
size_t command_line_len = b.size();
data->command_line = b;
@ -2369,6 +2406,7 @@ void reader_set_buffer(const wcstring &b, size_t pos)
data->buff_pos = pos;
/* Clear history search and pager contents */
data->search_mode = NO_SEARCH;
data->search_buff.clear();
data->history_search.go_to_end();
@ -2377,6 +2415,16 @@ void reader_set_buffer(const wcstring &b, size_t pos)
reader_repaint_needed();
}
/* Sets the command line contents, clearing the pager */
void reader_set_buffer(const wcstring &b, size_t pos)
{
if (!data)
return;
clear_pager();
reader_set_buffer_maintaining_pager(b, pos);
}
size_t reader_get_cursor_pos()
{
@ -3036,7 +3084,27 @@ const wchar_t *reader_readline(void)
if (last_char != R_YANK && last_char != R_YANK_POP)
yank_len=0;
const wchar_t *buff = data->command_line.c_str();
/* We clear pager contents for most events, except for a few */
switch (c)
{
case R_COMPLETE:
case R_BACKWARD_CHAR:
case R_FORWARD_CHAR:
case R_UP_LINE:
case R_DOWN_LINE:
case R_NULL:
case R_REPAINT:
case R_SUPPRESS_AUTOSUGGESTION:
break;
default:
clear_pager();
break;
}
//fprintf(stderr, "\n\nchar: %ls\n\n", describe_char(c).c_str());
const wchar_t * const buff = data->command_line.c_str();
switch (c)
{
@ -3049,7 +3117,7 @@ const wchar_t *reader_readline(void)
data->buff_pos--;
}
reader_repaint();
reader_repaint_needed();
break;
}
@ -3068,7 +3136,7 @@ const wchar_t *reader_readline(void)
accept_autosuggestion(true);
}
reader_repaint();
reader_repaint_needed();
break;
}
@ -3077,7 +3145,7 @@ const wchar_t *reader_readline(void)
{
data->buff_pos = 0;
reader_repaint();
reader_repaint_needed();
break;
}
@ -3086,13 +3154,12 @@ const wchar_t *reader_readline(void)
{
data->buff_pos = data->command_length();
reader_repaint();
reader_repaint_needed();
break;
}
case R_NULL:
{
reader_repaint_if_needed();
break;
}
@ -3123,22 +3190,10 @@ const wchar_t *reader_readline(void)
if (!data->complete_func)
break;
if (! comp_empty && last_char == R_COMPLETE)
if (is_navigating_pager_contents() || (! comp_empty && last_char == R_COMPLETE))
{
/* The user typed R_COMPLETE more than once in a row. Cycle through our available completions */
const completion_t *next_comp = cycle_competions(comp, cycle_command_line, &completion_cycle_idx);
if (next_comp != NULL)
{
size_t cursor_pos = cycle_cursor_pos;
const wcstring new_cmd_line = completion_apply_to_command_line(next_comp->completion, next_comp->flags, cycle_command_line, &cursor_pos, false);
reader_set_buffer(new_cmd_line, cursor_pos);
/* Since we just inserted a completion, don't immediately do a new autosuggestion */
data->suppress_autosuggestion = true;
/* Trigger repaint (see #765) */
reader_repaint_if_needed();
}
/* The user typed R_COMPLETE more than once in a row. Cycle through our available completions. */
select_completion_in_direction(direction_next, cycle_command_line, cycle_cursor_pos);
}
else
{
@ -3195,9 +3250,6 @@ const wchar_t *reader_readline(void)
/* Start the cycle at the beginning */
completion_cycle_idx = (size_t)(-1);
/* Repaint */
reader_repaint_if_needed();
}
break;
@ -3329,8 +3381,7 @@ const wchar_t *reader_readline(void)
}
data->search_buff.clear();
reader_super_highlight_me_plenty(data->buff_pos);
reader_repaint();
reader_repaint_needed();
}
break;
@ -3404,7 +3455,7 @@ const wchar_t *reader_readline(void)
}
finished=1;
data->buff_pos=data->command_length();
reader_repaint();
reader_repaint_needed();
break;
}
@ -3425,7 +3476,7 @@ const wchar_t *reader_readline(void)
default:
{
s_reset(&data->screen, screen_reset_abandon_line);
reader_repaint();
reader_repaint_needed();
break;
}
@ -3522,10 +3573,14 @@ const wchar_t *reader_readline(void)
/* Move left*/
case R_BACKWARD_CHAR:
{
if (data->buff_pos > 0)
if (is_navigating_pager_contents())
{
select_completion_in_direction(direction_west, cycle_command_line, cycle_cursor_pos);
}
else if (data->buff_pos > 0)
{
data->buff_pos--;
reader_repaint();
reader_repaint_needed();
}
break;
}
@ -3533,10 +3588,14 @@ const wchar_t *reader_readline(void)
/* Move right*/
case R_FORWARD_CHAR:
{
if (data->buff_pos < data->command_length())
if (is_navigating_pager_contents())
{
select_completion_in_direction(direction_east, cycle_command_line, cycle_cursor_pos);
}
else if (data->buff_pos < data->command_length())
{
data->buff_pos++;
reader_repaint();
reader_repaint_needed();
}
else
{
@ -3605,38 +3664,70 @@ const wchar_t *reader_readline(void)
case R_UP_LINE:
case R_DOWN_LINE:
{
int line_old = parse_util_get_line_from_offset(data->command_line, data->buff_pos);
int line_new;
if (c == R_UP_LINE)
line_new = line_old-1;
else
line_new = line_old+1;
int line_count = parse_util_lineno(data->command_line.c_str(), data->command_length())-1;
if (line_new >= 0 && line_new <= line_count)
if (is_navigating_pager_contents())
{
size_t base_pos_new;
size_t base_pos_old;
/* We are already navigating pager contents. */
selection_direction_t direction;
if (c == R_DOWN_LINE)
{
/* Down arrow is always south */
direction = direction_south;
}
else if (data->pager.get_selected_row(data->current_page_rendering) == 0 && data->pager.get_selected_column(data->current_page_rendering) == 0)
{
/* Up arrow, but we are in the first column and first row. End navigation */
direction = direction_deselect;
}
else
{
/* Up arrow, go north */
direction = direction_north;
}
int indent_old;
int indent_new;
size_t line_offset_old;
size_t total_offset_new;
/* Now do the selection */
select_completion_in_direction(direction, cycle_command_line, cycle_cursor_pos);
}
else if (c == R_DOWN_LINE && ! data->pager.empty())
{
/* We pressed down with a non-empty pager contents, begin navigation */
select_completion_in_direction(direction_south, cycle_command_line, cycle_cursor_pos);
}
else
{
/* Not navigating the pager contents */
int line_old = parse_util_get_line_from_offset(data->command_line, data->buff_pos);
int line_new;
base_pos_new = parse_util_get_offset_from_line(data->command_line, line_new);
if (c == R_UP_LINE)
line_new = line_old-1;
else
line_new = line_old+1;
base_pos_old = parse_util_get_offset_from_line(data->command_line, line_old);
int line_count = parse_util_lineno(data->command_line.c_str(), data->command_length())-1;
assert(base_pos_new != (size_t)(-1) && base_pos_old != (size_t)(-1));
indent_old = data->indents.at(base_pos_old);
indent_new = data->indents.at(base_pos_new);
if (line_new >= 0 && line_new <= line_count)
{
size_t base_pos_new;
size_t base_pos_old;
line_offset_old = data->buff_pos - parse_util_get_offset_from_line(data->command_line, line_old);
total_offset_new = parse_util_get_offset(data->command_line, line_new, line_offset_old - 4*(indent_new-indent_old));
data->buff_pos = total_offset_new;
reader_repaint();
int indent_old;
int indent_new;
size_t line_offset_old;
size_t total_offset_new;
base_pos_new = parse_util_get_offset_from_line(data->command_line, line_new);
base_pos_old = parse_util_get_offset_from_line(data->command_line, line_old);
assert(base_pos_new != (size_t)(-1) && base_pos_old != (size_t)(-1));
indent_old = data->indents.at(base_pos_old);
indent_new = data->indents.at(base_pos_new);
line_offset_old = data->buff_pos - parse_util_get_offset_from_line(data->command_line, line_old);
total_offset_new = parse_util_get_offset(data->command_line, line_new, line_offset_old - 4*(indent_new-indent_old));
data->buff_pos = total_offset_new;
reader_repaint_needed();
}
}
break;
@ -3646,7 +3737,7 @@ const wchar_t *reader_readline(void)
{
data->suppress_autosuggestion = true;
data->autosuggestion.clear();
reader_repaint();
reader_repaint_needed();
break;
}
@ -3754,7 +3845,7 @@ const wchar_t *reader_readline(void)
}
data->command_line_changed();
reader_super_highlight_me_plenty(data->buff_pos);
reader_repaint();
reader_repaint_needed();
break;
}
@ -3796,10 +3887,20 @@ const wchar_t *reader_readline(void)
}
last_char = c;
reader_repaint_if_needed();
}
writestr(L"\n");
/* Ensure we have no pager contents when we exit */
if (! data->pager.empty())
{
/* Clear to end of screen to erase the pager contents. TODO: this may fail if eos doesn't exist, in which case we should emit newlines */
screen_force_clear_to_end();
data->pager.clear();
}
if (!reader_exit_forced())
{
if (tcsetattr(0,TCSANOW,&old_modes)) /* return to previous mode */
@ -3820,7 +3921,17 @@ int reader_search_mode()
return -1;
}
return !!data->search_mode;
return !! data->search_mode;
}
int reader_has_pager_contents()
{
if (!data)
{
return -1;
}
return ! data->current_page_rendering.screen_data.empty();
}

View file

@ -239,10 +239,18 @@ int reader_shell_test(const wchar_t *b);
/**
Test whether the interactive reader is in search mode.
\return o if not in search mode, 1 if in search mode and -1 if not in interactive mode
\return 0 if not in search mode, 1 if in search mode and -1 if not in interactive mode
*/
int reader_search_mode();
/**
Test whether the interactive reader has visible pager contents.
\return 0 if it has pager contents, 1 if it does not have pager contents, and -1 if not in interactive mode
*/
int reader_has_pager_contents();
/* Given a command line and an autosuggestion, return the string that gets shown to the user. Exposed for testing purposes only. */
wcstring combine_command_and_autosuggestion(const wcstring &cmdline, const wcstring &autosuggestion);

View file

@ -45,6 +45,7 @@ efficient way for transforming that to the desired screen content.
#include "highlight.h"
#include "screen.h"
#include "env.h"
#include "pager.h"
/** The number of characters to indent new blocks */
#define INDENT_STEP 4
@ -1027,7 +1028,7 @@ static void s_update(screen_t *scr, const wchar_t *left_prompt, const wchar_t *r
if (! output.empty())
{
write_loop(1, &output.at(0), output.size());
write_loop(STDOUT_FILENO, &output.at(0), output.size());
}
/* We have now synced our actual screen against our desired screen. Note that this is a big assignment! */
@ -1235,7 +1236,8 @@ void s_write(screen_t *s,
size_t explicit_len,
const highlight_spec_t *colors,
const int *indent,
size_t cursor_pos)
size_t cursor_pos,
const page_rendering_t &pager)
{
screen_data_t::cursor_t cursor_arr;
@ -1322,6 +1324,10 @@ void s_write(screen_t *s,
}
s->desired.cursor = cursor_arr;
/* Append pager_data (none if empty) */
s->desired.append_lines(pager.screen_data);
s_update(s, layout.left_prompt.c_str(), layout.right_prompt.c_str());
s_save_status(s);
}
@ -1427,6 +1433,22 @@ void s_reset(screen_t *s, screen_reset_mode_t mode)
fstat(2, &s->prev_buff_2);
}
bool screen_force_clear_to_end()
{
bool result = false;
if (clr_eos)
{
data_buffer_t output;
s_write_mbs(&output, clr_eos);
if (! output.empty())
{
write_loop(STDOUT_FILENO, &output.at(0), output.size());
result = true;
}
}
return result;
}
screen_t::screen_t() :
desired(),
actual(),

View file

@ -13,8 +13,11 @@
#define FISH_SCREEN_H
#include <vector>
#include <sys/stat.h>
#include "highlight.h"
class page_rendering_t;
/**
A class representing a single line of a screen.
*/
@ -40,6 +43,17 @@ struct line_t
colors.push_back(color);
}
void append(const wchar_t *txt, highlight_spec_t color)
{
for (size_t i=0; txt[i]; i++)
{
text.push_back(txt[i]);
colors.push_back(color);
}
}
size_t size(void) const
{
return text.size();
@ -55,6 +69,12 @@ struct line_t
return colors.at(idx);
}
void append_line(const line_t &line)
{
text.insert(text.end(), line.text.begin(), line.text.end());
colors.insert(colors.end(), line.colors.begin(), line.colors.end());
}
};
/**
@ -103,6 +123,16 @@ public:
{
return line_datas.size();
}
void append_lines(const screen_data_t &d)
{
this->line_datas.insert(this->line_datas.end(), d.line_datas.begin(), d.line_datas.end());
}
bool empty() const
{
return line_datas.empty();
}
};
/**
@ -190,7 +220,8 @@ void s_write(screen_t *s,
size_t explicit_len,
const highlight_spec_t *colors,
const int *indent,
size_t cursor_pos);
size_t cursor_pos,
const page_rendering_t &pager_data);
/**
This function resets the screen buffers internal knowledge about
@ -228,6 +259,9 @@ enum screen_reset_mode_t
void s_reset(screen_t *s, screen_reset_mode_t mode);
/* Issues an immediate clr_eos, returning if it existed */
bool screen_force_clear_to_end();
/* Returns the length of an escape code. Exposed for testing purposes only. */
size_t escape_code_length(const wchar_t *code);

View file

@ -5,6 +5,12 @@ function up-or-search -d "Depending on cursor position and current mode, either
return
end
# If we are navigating the pager, then up always navigates
if commandline --paging-mode
commandline -f up-line
return
end
# We are not already in search mode.
# If we are on the top line, start search mode,
# otherwise move up