Skip to content

Commit 0018705

Browse files
committed
Implement optional rectangular prompt support
This is a reimplementation of #338 which was reverted. When `Highligher::has_continuation_prompt` returns true, rustyline enables a special mode that passes the original prompt for every line to the highlighter, and highlighter is free to change color or characters of the prompt as long as it's length is the same (usual contract for the highlighter) Note: 1. Wrapped lines are not prefixed by prompt (this may be fixed in future releases either by prepending them, or by collapsing and scrolling lines wider than terminal). 2. Unlike #338 this PR doesn't change the meaning of `cursor` and `end` fields of the `Layout` structure.
1 parent 7fe1925 commit 0018705

File tree

10 files changed

+342
-117
lines changed

10 files changed

+342
-117
lines changed

examples/example.rs

+16-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use rustyline::completion::{Completer, FilenameCompleter, Pair};
44
use rustyline::config::OutputStreamType;
55
use rustyline::error::ReadlineError;
66
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
7+
use rustyline::highlight::{PromptInfo};
78
use rustyline::hint::{Hinter, HistoryHinter};
89
use rustyline::validate::{self, MatchingBracketValidator, Validator};
910
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
@@ -16,6 +17,7 @@ struct MyHelper {
1617
validator: MatchingBracketValidator,
1718
hinter: HistoryHinter,
1819
colored_prompt: String,
20+
continuation_prompt: String,
1921
}
2022

2123
impl Completer for MyHelper {
@@ -41,15 +43,23 @@ impl Highlighter for MyHelper {
4143
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
4244
&'s self,
4345
prompt: &'p str,
44-
default: bool,
46+
info: PromptInfo<'_>,
4547
) -> Cow<'b, str> {
46-
if default {
47-
Borrowed(&self.colored_prompt)
48+
if info.is_default() {
49+
if info.line_no() > 0 {
50+
Borrowed(&self.continuation_prompt)
51+
} else {
52+
Borrowed(&self.colored_prompt)
53+
}
4854
} else {
4955
Borrowed(prompt)
5056
}
5157
}
5258

59+
fn has_continuation_prompt(&self) -> bool {
60+
return true;
61+
}
62+
5363
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
5464
Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
5565
}
@@ -90,7 +100,8 @@ fn main() -> rustyline::Result<()> {
90100
completer: FilenameCompleter::new(),
91101
highlighter: MatchingBracketHighlighter::new(),
92102
hinter: HistoryHinter {},
93-
colored_prompt: "".to_owned(),
103+
colored_prompt: " 0> ".to_owned(),
104+
continuation_prompt: "\x1b[1;32m...> \x1b[0m".to_owned(),
94105
validator: MatchingBracketValidator::new(),
95106
};
96107
let mut rl = Editor::with_config(config);
@@ -102,7 +113,7 @@ fn main() -> rustyline::Result<()> {
102113
}
103114
let mut count = 1;
104115
loop {
105-
let p = format!("{}> ", count);
116+
let p = format!("{:>3}> ", count);
106117
rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
107118
let readline = rl.readline(&p);
108119
match readline {

src/edit.rs

+60-40
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,24 @@ use crate::tty::{Renderer, Term, Terminal};
1818
use crate::undo::Changeset;
1919
use crate::validate::{ValidationContext, ValidationResult};
2020

21+
22+
#[derive(Debug)]
23+
pub struct Prompt<'prompt> {
24+
/// Prompt to display (rl_prompt)
25+
pub text: &'prompt str,
26+
/// Prompt Unicode/visible width and height
27+
pub size: Position,
28+
/// Is this a default (user-defined) prompt, or temporary like `(arg: 0)`?
29+
pub is_default: bool,
30+
/// Is prompt rectangular or single line
31+
pub has_continuation: bool,
32+
}
33+
2134
/// Represent the state during line editing.
2235
/// Implement rendering.
2336
pub struct State<'out, 'prompt, H: Helper> {
2437
pub out: &'out mut <Terminal as Term>::Writer,
25-
prompt: &'prompt str, // Prompt to display (rl_prompt)
26-
prompt_size: Position, // Prompt Unicode/visible width and height
38+
prompt: Prompt<'prompt>,
2739
pub line: LineBuffer, // Edited line buffer
2840
pub layout: Layout,
2941
saved_line_for_history: LineBuffer, // Current edited line before history browsing
@@ -48,11 +60,18 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
4860
helper: Option<&'out H>,
4961
ctx: Context<'out>,
5062
) -> State<'out, 'prompt, H> {
51-
let prompt_size = out.calculate_position(prompt, Position::default());
63+
let prompt_size = out.calculate_position(prompt, Position::default(), 0);
64+
let has_continuation = helper
65+
.map(|h| h.has_continuation_prompt())
66+
.unwrap_or(false);
5267
State {
5368
out,
54-
prompt,
55-
prompt_size,
69+
prompt: Prompt {
70+
text: prompt,
71+
size: prompt_size,
72+
is_default: true,
73+
has_continuation,
74+
},
5675
line: LineBuffer::with_capacity(MAX_LINE).can_growth(true),
5776
layout: Layout::default(),
5877
saved_line_for_history: LineBuffer::with_capacity(MAX_LINE).can_growth(true),
@@ -83,9 +102,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
83102
let rc = input_state.next_cmd(rdr, self, single_esc_abort);
84103
if rc.is_err() && self.out.sigwinch() {
85104
self.out.update_size();
86-
self.prompt_size = self
105+
self.prompt.size = self
87106
.out
88-
.calculate_position(self.prompt, Position::default());
107+
.calculate_position(self.prompt.text, Position::default(), 0);
89108
self.refresh_line()?;
90109
continue;
91110
}
@@ -110,20 +129,17 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
110129

111130
pub fn move_cursor(&mut self) -> Result<()> {
112131
// calculate the desired position of the cursor
113-
let cursor = self
114-
.out
115-
.calculate_position(&self.line[..self.line.pos()], self.prompt_size);
116-
if self.layout.cursor == cursor {
132+
let new_layout = self.out.compute_layout(
133+
&self.prompt, &self.line, None);
134+
if new_layout.cursor == self.layout.cursor {
117135
return Ok(());
118136
}
119137
if self.highlight_char() {
120-
let prompt_size = self.prompt_size;
121-
self.refresh(self.prompt, prompt_size, true, Info::NoHint)?;
138+
self.refresh_default(Info::NoHint)?;
122139
} else {
123-
self.out.move_cursor(self.layout.cursor, cursor)?;
124-
self.layout.prompt_size = self.prompt_size;
125-
self.layout.cursor = cursor;
126-
debug_assert!(self.layout.prompt_size <= self.layout.cursor);
140+
self.out.move_cursor(self.layout.cursor, new_layout.cursor)?;
141+
self.layout.prompt_size = self.prompt.size;
142+
self.layout.cursor = new_layout.cursor;
127143
debug_assert!(self.layout.cursor <= self.layout.end);
128144
}
129145
Ok(())
@@ -133,13 +149,16 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
133149
self.out.move_cursor_at_leftmost(rdr)
134150
}
135151

136-
fn refresh(
137-
&mut self,
138-
prompt: &str,
139-
prompt_size: Position,
140-
default_prompt: bool,
141-
info: Info<'_>,
142-
) -> Result<()> {
152+
fn refresh(&mut self, prompt: &Prompt<'_>, info: Info<'_>) -> Result<()> {
153+
self._refresh(Some(prompt), info)
154+
}
155+
fn refresh_default(&mut self, info: Info<'_>) -> Result<()> {
156+
// We pass None, because we can't pass `&self.prompt`
157+
// to the method having `&mut self` as a receiver
158+
self._refresh(None, info)
159+
}
160+
fn _refresh(&mut self, non_default_prompt: Option<&Prompt<'_>>, info: Info<'_>) -> Result<()> {
161+
let prompt = non_default_prompt.unwrap_or(&self.prompt);
143162
let info = match info {
144163
Info::NoHint => None,
145164
Info::Hint => self.hint.as_deref(),
@@ -151,10 +170,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
151170
None
152171
};
153172

154-
let new_layout = self
155-
.out
156-
.compute_layout(prompt_size, default_prompt, &self.line, info);
157-
173+
let new_layout = self.out.compute_layout(prompt, &self.line, info);
158174
debug!(target: "rustyline", "old layout: {:?}", self.layout);
159175
debug!(target: "rustyline", "new layout: {:?}", new_layout);
160176
self.out.refresh_line(
@@ -239,24 +255,27 @@ impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> {
239255

240256
impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {
241257
fn refresh_line(&mut self) -> Result<()> {
242-
let prompt_size = self.prompt_size;
243258
self.hint();
244259
self.highlight_char();
245-
self.refresh(self.prompt, prompt_size, true, Info::Hint)
260+
self.refresh_default(Info::Hint)
246261
}
247262

248263
fn refresh_line_with_msg(&mut self, msg: Option<String>) -> Result<()> {
249-
let prompt_size = self.prompt_size;
250264
self.hint = None;
251265
self.highlight_char();
252-
self.refresh(self.prompt, prompt_size, true, Info::Msg(msg.as_deref()))
266+
self.refresh_default(Info::Msg(msg.as_deref()))
253267
}
254268

255269
fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> {
256-
let prompt_size = self.out.calculate_position(prompt, Position::default());
270+
let prompt = Prompt {
271+
text: prompt,
272+
size: self.out.calculate_position(prompt, Position::default(), 0),
273+
is_default: false,
274+
has_continuation: false,
275+
};
257276
self.hint();
258277
self.highlight_char();
259-
self.refresh(prompt, prompt_size, false, Info::Hint)
278+
self.refresh(&prompt, Info::Hint)
260279
}
261280

262281
fn doing_insert(&mut self) {
@@ -284,7 +303,6 @@ impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> {
284303
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285304
f.debug_struct("State")
286305
.field("prompt", &self.prompt)
287-
.field("prompt_size", &self.prompt_size)
288306
.field("buf", &self.line)
289307
.field("cols", &self.out.get_columns())
290308
.field("layout", &self.layout)
@@ -305,7 +323,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
305323
pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> {
306324
if let Some(push) = self.line.insert(ch, n) {
307325
if push {
308-
let prompt_size = self.prompt_size;
309326
let no_previous_hint = self.hint.is_none();
310327
self.hint();
311328
let width = ch.width().unwrap_or(0);
@@ -318,13 +335,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
318335
// Avoid a full update of the line in the trivial case.
319336
self.layout.cursor.col += width;
320337
self.layout.end.col += width;
321-
debug_assert!(self.layout.prompt_size <= self.layout.cursor);
322338
debug_assert!(self.layout.cursor <= self.layout.end);
323339
let bits = ch.encode_utf8(&mut self.byte_buffer);
324340
let bits = bits.as_bytes();
325341
self.out.write_and_flush(bits)
326342
} else {
327-
self.refresh(self.prompt, prompt_size, true, Info::Hint)
343+
self.refresh_default(Info::Hint)
328344
}
329345
} else {
330346
self.refresh_line()
@@ -664,8 +680,12 @@ pub fn init_state<'out, H: Helper>(
664680
) -> State<'out, 'static, H> {
665681
State {
666682
out,
667-
prompt: "",
668-
prompt_size: Position::default(),
683+
prompt: Prompt {
684+
text: "",
685+
size: Position::default(),
686+
is_default: true,
687+
has_continuation: false,
688+
},
669689
line: LineBuffer::init(line, pos, None),
670690
layout: Layout::default(),
671691
saved_line_for_history: LineBuffer::with_capacity(100),

0 commit comments

Comments
 (0)