Render control characters as Unicode Control Pictures

Inserting Tab or Backspace characters causes weird glitches. Sometimes it's
useful to paste tabs as part of a code block.

Render tabs as "␉" and so on for other ASCII control characters, see
https://unicode-table.com/en/blocks/control-pictures/. This fixes the
width-related glitches.

You can see it in action by inserting some control characters into the
command line:

	set chars
	for x in (seq 1 0x1F)
		set -a chars (printf "%02x\\\\x%02x" $x $x)
	end
	eval set chars $chars
	commandline -i "echo '" $chars

Fixes #6923
Fixes #5274
Closes #7295

We could extend this approach to display a fallback symbol for every unknown
nonprintable character, not just ASCII control characters.

In future we might want to support tab properly.
This commit is contained in:
Johannes Altmanninger 2020-08-29 09:26:34 +02:00
parent d3b700f98c
commit 0627c9d9af
3 changed files with 34 additions and 23 deletions

View File

@ -49,6 +49,7 @@ Interactive improvements
- When the cursor is on a command that resolves to a script, :kbd:`Alt-O` will now open that script in your editor.
- :kbd:`Alt-E` now passes the cursor position to the external editor also if the editor aliases a supported editor (via ``complete --wraps``).
- Option completion now uses fuzzy subsequence filtering as well. This means that ``--fb`` may be completed to ``--foobar`` if there is no better match.
- ASCII control characters are now rendered using symbols from Unicode's Control Pictures block.
New or improved bindings
^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -153,6 +153,11 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod
bind --preset -M paste \e\[201~ __fish_stop_bracketed_paste
# In paste-mode, everything self-inserts except for the sequence to get out of it
bind --preset -M paste "" self-insert
# Pass through formatting control characters else they may be dropped
# on some terminals.
bind --preset -M paste \b 'commandline -i \b'
bind --preset -M paste \t 'commandline -i \t'
bind --preset -M paste \v 'commandline -i \v'
# Without this, a \r will overwrite the other text, rendering it invisible - which makes the exercise kinda pointless.
bind --preset -M paste \r "commandline -i \n"

View File

@ -29,11 +29,11 @@ use crate::flog::FLOGF;
use crate::future::IsSomeAnd;
use crate::global_safety::RelaxedAtomicBool;
use crate::highlight::HighlightColorResolver;
use crate::highlight::HighlightSpec;
use crate::output::Outputter;
use crate::termsize::{termsize_last, Termsize};
use crate::wchar::prelude::*;
use crate::wcstringutil::string_prefixes_string;
use crate::{highlight::HighlightSpec, wcstringutil::fish_wcwidth_visible};
#[derive(Clone, Default)]
pub struct HighlightedChar {
@ -64,7 +64,7 @@ impl Line {
pub fn append(&mut self, character: char, highlight: HighlightSpec) {
self.text.push(HighlightedChar {
highlight,
character,
character: rendered_character(character),
})
}
@ -101,11 +101,7 @@ impl Line {
pub fn wcswidth_min_0(&self, max: usize /* = usize::MAX */) -> usize {
let mut result: usize = 0;
for c in &self.text[..max.min(self.text.len())] {
let w = fish_wcwidth_visible(c.character);
// A backspace at the start of the line does nothing.
if w > 0 || result > 0 {
result = result.checked_add_signed(w).unwrap();
}
result += wcwidth_rendered(c.character);
}
result
}
@ -329,10 +325,7 @@ impl Screen {
colors[i],
usize::try_from(indent[i]).unwrap(),
first_line_prompt_space,
usize::try_from(fish_wcwidth_visible(
effective_commandline.as_char_slice()[i],
))
.unwrap(),
wcwidth_rendered(effective_commandline.as_char_slice()[i]),
);
i += 1;
}
@ -933,8 +926,7 @@ impl Screen {
// Skip over skip_remaining width worth of characters.
let mut j = 0;
while j < o_line(&zelf, i).len() {
let width =
usize::try_from(fish_wcwidth_visible(o_line(&zelf, i).char_at(j))).unwrap();
let width = wcwidth_rendered(o_line(&zelf, i).char_at(j));
if skip_remaining < width {
break;
}
@ -945,7 +937,7 @@ impl Screen {
// Skip over zero-width characters (e.g. combining marks at the end of the prompt).
while j < o_line(&zelf, i).len() {
let width = fish_wcwidth_visible(o_line(&zelf, i).char_at(j));
let width = wcwidth_rendered(o_line(&zelf, i).char_at(j));
if width > 0 {
break;
}
@ -978,7 +970,7 @@ impl Screen {
let color = o_line(&zelf, i).color_at(j);
set_color(&mut zelf, color);
let ch = o_line(&zelf, i).char_at(j);
let width = usize::try_from(fish_wcwidth_visible(ch)).unwrap();
let width = wcwidth_rendered(ch);
zelf.write_char(ch, isize::try_from(width).unwrap());
current_width += width;
j += 1;
@ -1525,11 +1517,7 @@ fn measure_run_from(
width = next_tab_stop(width);
} else {
// Ordinary char. Add its width with care to ignore control chars which have width -1.
let w = fish_wcwidth_visible(input.char_at(idx));
// A backspace at the start of the line does nothing.
if w != -1 || width > 0 {
width = width.checked_add_signed(w).unwrap();
}
width += wcwidth_rendered(input.char_at(idx));
}
idx += 1;
}
@ -1575,7 +1563,7 @@ fn truncate_run(
curr_width = measure_run_from(run, 0, None, cache);
idx = 0;
} else {
let char_width = fish_wcwidth_visible(c) as usize;
let char_width = wcwidth_rendered(c);
curr_width -= std::cmp::min(curr_width, char_width);
run.remove(idx);
}
@ -1724,7 +1712,7 @@ fn compute_layout(
multiline = true;
break;
} else {
first_line_width += usize::try_from(fish_wcwidth_visible(c)).unwrap();
first_line_width += wcwidth_rendered(c);
}
}
let first_command_line_width = first_line_width;
@ -1739,7 +1727,7 @@ fn compute_layout(
autosuggest_truncated_widths.reserve(1 + autosuggestion_str.len());
for c in autosuggestion.chars() {
autosuggest_truncated_widths.push(autosuggest_total_width);
autosuggest_total_width += usize::try_from(fish_wcwidth_visible(c)).unwrap();
autosuggest_total_width += wcwidth_rendered(c);
}
}
@ -1830,3 +1818,20 @@ fn compute_layout(
result.autosuggestion = autosuggestion.to_owned();
result
}
// Display non-printable control characters as a graphic symbol.
// This is to prevent control characters like \t and \v from moving the
// cursor in a way we don't handle. The ones we do handle are \r and
// \n.
// See https://unicode-table.com/en/blocks/control-pictures/
fn rendered_character(c: char) -> char {
if c <= '\x1F' {
char::from_u32(u32::from(c) + 0x2400).unwrap()
} else {
c
}
}
fn wcwidth_rendered(c: char) -> usize {
usize::try_from(fish_wcwidth(rendered_character(c))).unwrap_or_default()
}