Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions examples/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::highlight::{PromptInfo};
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::validate::{self, MatchingBracketValidator, Validator};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
Expand All @@ -16,6 +17,7 @@ struct MyHelper {
validator: MatchingBracketValidator,
hinter: HistoryHinter,
colored_prompt: String,
continuation_prompt: String,
}

impl Completer for MyHelper {
Expand All @@ -41,15 +43,23 @@ impl Highlighter for MyHelper {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
info: PromptInfo<'_>,
) -> Cow<'b, str> {
if default {
Borrowed(&self.colored_prompt)
if info.is_default() {
if info.line_no() > 0 {
Borrowed(&self.continuation_prompt)
} else {
Borrowed(&self.colored_prompt)
}
} else {
Borrowed(prompt)
}
}

fn has_continuation_prompt(&self) -> bool {
return true;
}

fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
}
Expand Down Expand Up @@ -90,7 +100,8 @@ fn main() -> rustyline::Result<()> {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
colored_prompt: "".to_owned(),
colored_prompt: " 0> ".to_owned(),
continuation_prompt: "\x1b[1;32m...> \x1b[0m".to_owned(),
validator: MatchingBracketValidator::new(),
};
let mut rl = Editor::with_config(config);
Expand All @@ -102,7 +113,7 @@ fn main() -> rustyline::Result<()> {
}
let mut count = 1;
loop {
let p = format!("{}> ", count);
let p = format!("{:>3}> ", count);
rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
let readline = rl.readline(&p);
match readline {
Expand Down
104 changes: 62 additions & 42 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,24 @@ use crate::tty::{Renderer, Term, Terminal};
use crate::undo::Changeset;
use crate::validate::{ValidationContext, ValidationResult};


#[derive(Debug)]
pub struct Prompt<'prompt> {
/// Prompt to display (rl_prompt)
pub text: &'prompt str,
/// Prompt Unicode/visible width and height
pub size: Position,
/// Is this a default (user-defined) prompt, or temporary like `(arg: 0)`?
pub is_default: bool,
/// Is prompt rectangular or single line
pub has_continuation: bool,
}

/// Represent the state during line editing.
/// Implement rendering.
pub struct State<'out, 'prompt, H: Helper> {
pub out: &'out mut <Terminal as Term>::Writer,
prompt: &'prompt str, // Prompt to display (rl_prompt)
prompt_size: Position, // Prompt Unicode/visible width and height
prompt: Prompt<'prompt>,
pub line: LineBuffer, // Edited line buffer
pub layout: Layout,
saved_line_for_history: LineBuffer, // Current edited line before history browsing
Expand All @@ -48,11 +60,18 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
helper: Option<&'out H>,
ctx: Context<'out>,
) -> State<'out, 'prompt, H> {
let prompt_size = out.calculate_position(prompt, Position::default());
let prompt_size = out.meter().update(prompt);
let has_continuation = helper
.map(|h| h.has_continuation_prompt())
.unwrap_or(false);
State {
out,
prompt,
prompt_size,
prompt: Prompt {
text: prompt,
size: prompt_size,
is_default: true,
has_continuation,
},
line: LineBuffer::with_capacity(MAX_LINE).can_growth(true),
layout: Layout::default(),
saved_line_for_history: LineBuffer::with_capacity(MAX_LINE).can_growth(true),
Expand Down Expand Up @@ -83,9 +102,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
let rc = input_state.next_cmd(rdr, self, single_esc_abort);
if rc.is_err() && self.out.sigwinch() {
self.out.update_size();
self.prompt_size = self
.out
.calculate_position(self.prompt, Position::default());
self.prompt.size = self.out.meter().update(self.prompt.text);
self.refresh_line()?;
continue;
}
Expand All @@ -110,20 +127,18 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {

pub fn move_cursor(&mut self) -> Result<()> {
// calculate the desired position of the cursor
let cursor = self
.out
.calculate_position(&self.line[..self.line.pos()], self.prompt_size);
if self.layout.cursor == cursor {
let new_layout = self.out.compute_layout(
&self.prompt, &self.line, None, self.layout.scroll_top);
if new_layout.cursor == self.layout.cursor {
return Ok(());
}
if self.highlight_char() {
let prompt_size = self.prompt_size;
self.refresh(self.prompt, prompt_size, true, Info::NoHint)?;
let scroll_changed = new_layout.scroll_top != self.layout.scroll_top;
if scroll_changed || self.highlight_char() {
self.refresh_default(Info::NoHint)?;
} else {
self.out.move_cursor(self.layout.cursor, cursor)?;
self.layout.prompt_size = self.prompt_size;
self.layout.cursor = cursor;
debug_assert!(self.layout.prompt_size <= self.layout.cursor);
self.out.move_cursor(self.layout.cursor, new_layout.cursor)?;
self.layout.prompt_size = self.prompt.size;
self.layout.cursor = new_layout.cursor;
debug_assert!(self.layout.cursor <= self.layout.end);
}
Ok(())
Expand All @@ -133,13 +148,16 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
self.out.move_cursor_at_leftmost(rdr)
}

fn refresh(
&mut self,
prompt: &str,
prompt_size: Position,
default_prompt: bool,
info: Info<'_>,
) -> Result<()> {
fn refresh(&mut self, prompt: &Prompt<'_>, info: Info<'_>) -> Result<()> {
self._refresh(Some(prompt), info)
}
fn refresh_default(&mut self, info: Info<'_>) -> Result<()> {
// We pass None, because we can't pass `&self.prompt`
// to the method having `&mut self` as a receiver
self._refresh(None, info)
}
fn _refresh(&mut self, non_default_prompt: Option<&Prompt<'_>>, info: Info<'_>) -> Result<()> {
let prompt = non_default_prompt.unwrap_or(&self.prompt);
let info = match info {
Info::NoHint => None,
Info::Hint => self.hint.as_deref(),
Expand All @@ -151,10 +169,8 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
None
};

let new_layout = self
.out
.compute_layout(prompt_size, default_prompt, &self.line, info);

let new_layout = self.out.compute_layout(prompt, &self.line, info,
self.layout.scroll_top);
debug!(target: "rustyline", "old layout: {:?}", self.layout);
debug!(target: "rustyline", "new layout: {:?}", new_layout);
self.out.refresh_line(
Expand Down Expand Up @@ -239,24 +255,27 @@ impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> {

impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {
fn refresh_line(&mut self) -> Result<()> {
let prompt_size = self.prompt_size;
self.hint();
self.highlight_char();
self.refresh(self.prompt, prompt_size, true, Info::Hint)
self.refresh_default(Info::Hint)
}

fn refresh_line_with_msg(&mut self, msg: Option<String>) -> Result<()> {
let prompt_size = self.prompt_size;
self.hint = None;
self.highlight_char();
self.refresh(self.prompt, prompt_size, true, Info::Msg(msg.as_deref()))
self.refresh_default(Info::Msg(msg.as_deref()))
}

fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> {
let prompt_size = self.out.calculate_position(prompt, Position::default());
let prompt = Prompt {
text: prompt,
size: self.out.meter().update(prompt),
is_default: false,
has_continuation: false,
};
self.hint();
self.highlight_char();
self.refresh(prompt, prompt_size, false, Info::Hint)
self.refresh(&prompt, Info::Hint)
}

fn doing_insert(&mut self) {
Expand Down Expand Up @@ -284,7 +303,6 @@ impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("State")
.field("prompt", &self.prompt)
.field("prompt_size", &self.prompt_size)
.field("buf", &self.line)
.field("cols", &self.out.get_columns())
.field("layout", &self.layout)
Expand All @@ -305,7 +323,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> {
if let Some(push) = self.line.insert(ch, n) {
if push {
let prompt_size = self.prompt_size;
let no_previous_hint = self.hint.is_none();
self.hint();
let width = ch.width().unwrap_or(0);
Expand All @@ -318,13 +335,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
// Avoid a full update of the line in the trivial case.
self.layout.cursor.col += width;
self.layout.end.col += width;
debug_assert!(self.layout.prompt_size <= self.layout.cursor);
debug_assert!(self.layout.cursor <= self.layout.end);
let bits = ch.encode_utf8(&mut self.byte_buffer);
let bits = bits.as_bytes();
self.out.write_and_flush(bits)
} else {
self.refresh(self.prompt, prompt_size, true, Info::Hint)
self.refresh_default(Info::Hint)
}
} else {
self.refresh_line()
Expand Down Expand Up @@ -664,8 +680,12 @@ pub fn init_state<'out, H: Helper>(
) -> State<'out, 'static, H> {
State {
out,
prompt: "",
prompt_size: Position::default(),
prompt: Prompt {
text: "",
size: Position::default(),
is_default: true,
has_continuation: false,
},
line: LineBuffer::init(line, pos, None),
layout: Layout::default(),
saved_line_for_history: LineBuffer::with_capacity(100),
Expand Down
Loading