Added goto line command
This commit is contained in:
parent
d352d04030
commit
846cc31174
8 changed files with 117 additions and 48 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ pub enum Action {
|
||||||
OpenOpener(PathBuf), // Open the file opener
|
OpenOpener(PathBuf), // Open the file opener
|
||||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||||
OpenFile(PathBuf), // Open the file and switch the current pane to it
|
OpenFile(PathBuf), // Open the file and switch the current pane to it
|
||||||
|
CommandStart(&'static str), // Start a new command
|
||||||
|
GotoLine(isize), // Go to the specified file line
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -220,6 +222,22 @@ impl RawEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_command_start(&self) -> Option<Action> {
|
||||||
|
if matches!(
|
||||||
|
&self.0,
|
||||||
|
TerminalEvent::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('l'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Some(Action::CommandStart("goto_line"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_go(&self) -> Option<Action> {
|
pub fn to_go(&self) -> Option<Action> {
|
||||||
if matches!(
|
if matches!(
|
||||||
&self.0,
|
&self.0,
|
||||||
|
|
|
||||||
16
src/state.rs
16
src/state.rs
|
|
@ -123,7 +123,12 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
// If the file doesn't exist, create a new file
|
// If the file doesn't exist, create a new file
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
(path.parent().map(Path::to_owned), Vec::new())
|
let dir = path
|
||||||
|
.parent()
|
||||||
|
.filter(|p| p.to_str() != Some(""))
|
||||||
|
.map(Path::to_owned)
|
||||||
|
.or_else(|| std::env::current_dir().ok());
|
||||||
|
(dir, Vec::new())
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
|
@ -152,6 +157,15 @@ impl Buffer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn goto_line_cursor(&mut self, cursor_id: CursorId, line: isize) {
|
||||||
|
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
cursor.pos = self.text.to_pos([0, line]);
|
||||||
|
cursor.reset_desired_col(&self.text);
|
||||||
|
cursor.base = cursor.pos;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_cursor(
|
pub fn move_cursor(
|
||||||
&mut self,
|
&mut self,
|
||||||
cursor_id: CursorId,
|
cursor_id: CursorId,
|
||||||
|
|
|
||||||
|
|
@ -185,12 +185,8 @@ impl<'a> Rect<'a> {
|
||||||
self.rect([0, 0], self.size())
|
self.rect([0, 0], self.size())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text<C: Borrow<char>>(
|
pub fn text(&mut self, origin: [isize; 2], text: &str) -> Rect {
|
||||||
&mut self,
|
for (idx, c) in text.chars().enumerate() {
|
||||||
origin: [isize; 2],
|
|
||||||
text: impl IntoIterator<Item = C>,
|
|
||||||
) -> Rect {
|
|
||||||
for (idx, c) in text.into_iter().enumerate() {
|
|
||||||
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0
|
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0
|
||||||
{
|
{
|
||||||
let cell = Cell {
|
let cell = Cell {
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,11 @@ impl Input {
|
||||||
self.refocus(buffer, cursor_id);
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
|
Some(Action::GotoLine(line)) => {
|
||||||
|
buffer.goto_line_cursor(cursor_id, line);
|
||||||
|
self.refocus(buffer, cursor_id);
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
_ => Err(event),
|
_ => Err(event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,13 +141,13 @@ impl Input {
|
||||||
.with_bg(state.theme.margin_bg)
|
.with_bg(state.theme.margin_bg)
|
||||||
.with_fg(state.theme.margin_line_num)
|
.with_fg(state.theme.margin_line_num)
|
||||||
.fill(' ')
|
.fill(' ')
|
||||||
.text([0, 0], ">".chars()),
|
.text([0, 0], ">"),
|
||||||
Mode::Doc => frame
|
Mode::Doc => frame
|
||||||
.rect([0, i], [margin_w, 1])
|
.rect([0, i], [margin_w, 1])
|
||||||
.with_bg(state.theme.margin_bg)
|
.with_bg(state.theme.margin_bg)
|
||||||
.with_fg(state.theme.margin_line_num)
|
.with_fg(state.theme.margin_line_num)
|
||||||
.fill(' ')
|
.fill(' ')
|
||||||
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars()),
|
.text([1, 0], &format!("{:>line_num_w$}", line_num + 1)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Line
|
// Line
|
||||||
|
|
@ -166,7 +171,7 @@ impl Input {
|
||||||
state.theme.unfocus_select_bg
|
state.theme.unfocus_select_bg
|
||||||
})
|
})
|
||||||
.with_fg(fg)
|
.with_fg(fg)
|
||||||
.text([i as isize, 0], &[c]);
|
.text([i as isize, 0], c.encode_utf8(&mut [0; 4]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ impl Visual for Label {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.with(|frame| {
|
frame.with(|frame| {
|
||||||
for (idx, line) in self.lines().enumerate() {
|
for (idx, line) in self.lines().enumerate() {
|
||||||
frame.text([0, idx as isize], line.chars());
|
frame.text([0, idx as isize], &line);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,29 @@ pub struct Prompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
pub fn new() -> Self {
|
pub fn new(init: &str) -> Self {
|
||||||
let mut buffer = Buffer::default();
|
let mut buffer = Buffer::default();
|
||||||
|
let cursor_id = buffer.start_session();
|
||||||
|
buffer.enter(cursor_id, init.chars());
|
||||||
Self {
|
Self {
|
||||||
cursor_id: buffer.start_session(),
|
|
||||||
buffer,
|
buffer,
|
||||||
|
cursor_id,
|
||||||
input: Input::prompt(),
|
input: Input::prompt(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_action(&self) -> Option<Action> {
|
pub fn parse_action(&self) -> Result<Action, String> {
|
||||||
match self.buffer.text.to_string().as_str() {
|
let cmd = self.buffer.text.to_string();
|
||||||
|
let mut args = cmd.as_str().split_whitespace();
|
||||||
|
|
||||||
|
match args.next() {
|
||||||
// The root sees 'cancel' as an initiator for quitting
|
// The root sees 'cancel' as an initiator for quitting
|
||||||
"q" | "quit" => Some(Action::Cancel),
|
Some("q" | "quit") => Ok(Action::Cancel),
|
||||||
"version" => Some(Action::Show(
|
Some("version") => Ok(Action::Show(
|
||||||
Some(format!("Version")),
|
Some(format!("Version")),
|
||||||
format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
|
format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
|
||||||
)),
|
)),
|
||||||
"?" | "help" => Some(Action::Show(
|
Some("?" | "help") => Ok(Action::Show(
|
||||||
Some(format!("Help")),
|
Some(format!("Help")),
|
||||||
format!(
|
format!(
|
||||||
"Temporary help info:\n\
|
"Temporary help info:\n\
|
||||||
|
|
@ -37,9 +42,20 @@ impl Prompt {
|
||||||
- help"
|
- help"
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
"pane_move_left" => Some(Action::PaneMove(Dir::Left)),
|
Some("pane_move_left") => Ok(Action::PaneMove(Dir::Left)),
|
||||||
"pane_move_right" => Some(Action::PaneMove(Dir::Right)),
|
Some("pane_move_right") => Ok(Action::PaneMove(Dir::Right)),
|
||||||
_ => None,
|
Some("goto_line") => {
|
||||||
|
// Subtract 1 due to zero indexing
|
||||||
|
let line = args
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "Expected argument".to_string())?
|
||||||
|
.parse::<isize>()
|
||||||
|
.map_err(|_| "Expected integer".to_string())?
|
||||||
|
- 1;
|
||||||
|
Ok(Action::GotoLine(line))
|
||||||
|
}
|
||||||
|
Some(cmd) => Err(format!("Unknown command `{cmd}`")),
|
||||||
|
None => Err(format!("No command entered")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,22 +66,20 @@ impl Prompt {
|
||||||
|
|
||||||
impl Element<()> for Prompt {
|
impl Element<()> for Prompt {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||||
match event.to_action(|e| e.to_go().or_else(|| e.to_cancel())) {
|
match event.to_action(|e| {
|
||||||
|
e.to_go()
|
||||||
|
.or_else(|| e.to_cancel().or_else(|| e.to_command_start()))
|
||||||
|
}) {
|
||||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||||
Some(Action::Go) => {
|
Some(Action::Go) => match self.parse_action() {
|
||||||
if let Some(action) = self.get_action() {
|
Ok(action) => {
|
||||||
self.buffer.clear();
|
self.buffer.clear();
|
||||||
Ok(Resp::end(Some(action.into())))
|
Ok(Resp::end(Some(action.into())))
|
||||||
} else {
|
|
||||||
Ok(Resp::handled(Some(
|
|
||||||
Action::Show(
|
|
||||||
Some(format!("Error")),
|
|
||||||
format!("unknown command `{}`", self.buffer.text.to_string()),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
Err(err) => Ok(Resp::handled(Some(
|
||||||
|
Action::Show(Some(format!("Error")), err).into(),
|
||||||
|
))),
|
||||||
|
},
|
||||||
_ => self
|
_ => self
|
||||||
.input
|
.input
|
||||||
.handle(&mut self.buffer, self.cursor_id, event)
|
.handle(&mut self.buffer, self.cursor_id, event)
|
||||||
|
|
@ -248,7 +262,7 @@ impl Visual for BufferId {
|
||||||
let Some(buffer) = state.buffers.get(*self) else {
|
let Some(buffer) = state.buffers.get(*self) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
frame.text([0, 0], buffer.name().unwrap_or("<unknown>").chars());
|
frame.text([0, 0], buffer.name().unwrap_or("<unknown>"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,11 +327,15 @@ impl Opener {
|
||||||
is_link: entry.file_type().ok()?.is_symlink(),
|
is_link: entry.file_type().ok()?.is_symlink(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.chain([FileOption {
|
.chain(if filter != "" {
|
||||||
path: [dir, &filter].into_iter().collect(),
|
Some(FileOption {
|
||||||
kind: FileKind::New,
|
path: [dir, &filter].into_iter().collect(),
|
||||||
is_link: false,
|
kind: FileKind::New,
|
||||||
}]);
|
is_link: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
// TODO
|
// TODO
|
||||||
self.options.set_options(options, |e| {
|
self.options.set_options(options, |e| {
|
||||||
let name = e.path.file_name()?.to_str()?.to_lowercase();
|
let name = e.path.file_name()?.to_str()?.to_lowercase();
|
||||||
|
|
@ -395,9 +413,15 @@ pub struct FileOption {
|
||||||
impl Visual for FileOption {
|
impl Visual for FileOption {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
let name = match self.path.file_name().and_then(|n| n.to_str()) {
|
let name = match self.path.file_name().and_then(|n| n.to_str()) {
|
||||||
Some(name) if matches!(self.kind, FileKind::Dir) => format!("{}/", name),
|
Some(name) if matches!(self.kind, FileKind::Dir) => format!("{name}/"),
|
||||||
Some(name) => name.to_string(),
|
Some(name) => format!("{name}"),
|
||||||
None => format!("<unknown>"),
|
None => format!("Unknown"),
|
||||||
|
};
|
||||||
|
let desc = match self.kind {
|
||||||
|
FileKind::Dir => "Directory",
|
||||||
|
FileKind::Unknown => "Unknown filesystem item",
|
||||||
|
FileKind::File => "File",
|
||||||
|
FileKind::New => "Create new file",
|
||||||
};
|
};
|
||||||
frame
|
frame
|
||||||
.with_fg(match self.kind {
|
.with_fg(match self.kind {
|
||||||
|
|
@ -405,7 +429,10 @@ impl Visual for FileOption {
|
||||||
FileKind::File | FileKind::Unknown => state.theme.option_file,
|
FileKind::File | FileKind::Unknown => state.theme.option_file,
|
||||||
FileKind::New => state.theme.option_new,
|
FileKind::New => state.theme.option_new,
|
||||||
})
|
})
|
||||||
.text([0, 0], name.chars());
|
.text([0, 0], &name);
|
||||||
|
frame.with_fg(state.theme.margin_line_num).with(|f| {
|
||||||
|
f.text([f.size()[0] as isize / 2, 0], &desc);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,13 +77,17 @@ impl Element<()> for Root {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle 'top-level' actions
|
// Handle 'top-level' actions
|
||||||
if let Some(action) =
|
if let Some(action) = action.and_then(|e| {
|
||||||
action.and_then(|e| e.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel())))
|
e.to_action(|e| {
|
||||||
{
|
e.to_open_prompt()
|
||||||
|
.or_else(|| e.to_cancel())
|
||||||
|
.or_else(|| e.to_command_start())
|
||||||
|
})
|
||||||
|
}) {
|
||||||
match action {
|
match action {
|
||||||
Action::OpenPrompt => {
|
Action::OpenPrompt => {
|
||||||
self.tasks.clear(); // Prompt overrides all
|
self.tasks.clear(); // Prompt overrides all
|
||||||
self.tasks.push(Task::Prompt(Prompt::new()));
|
self.tasks.push(Task::Prompt(Prompt::new("")));
|
||||||
}
|
}
|
||||||
Action::OpenSwitcher => {
|
Action::OpenSwitcher => {
|
||||||
self.tasks.clear(); // Overrides all
|
self.tasks.clear(); // Overrides all
|
||||||
|
|
@ -94,6 +98,11 @@ impl Element<()> for Root {
|
||||||
self.tasks.clear(); // Overrides all
|
self.tasks.clear(); // Overrides all
|
||||||
self.tasks.push(Task::Opener(Opener::new(path)));
|
self.tasks.push(Task::Opener(Opener::new(path)));
|
||||||
}
|
}
|
||||||
|
Action::CommandStart(cmd) => {
|
||||||
|
self.tasks.clear(); // Prompt overrides all
|
||||||
|
self.tasks
|
||||||
|
.push(Task::Prompt(Prompt::new(&format!("{cmd} "))));
|
||||||
|
}
|
||||||
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
|
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
|
||||||
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
|
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
|
||||||
action: Action::Quit,
|
action: Action::Quit,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue