Compare commits

...

10 commits

13 changed files with 680 additions and 265 deletions

View file

@ -36,18 +36,22 @@ pub enum Action {
OpenOpener(PathBuf), // Open the file opener OpenOpener(PathBuf), // Open the file opener
OpenFinder(Option<String>), // Open the finder, with the given default query OpenFinder(Option<String>), // Open the finder, with the given default query
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, usize), // Open the file (on the given line) and switch the current pane to it
CommandStart(&'static str), // Start a new command CommandStart(&'static str), // Start a new command
GotoLine(isize), // Go to the specified file line GotoLine(isize), // Go to the specified file line
SelectToken, // Fully select the token under the cursor BeginSearch(String), // Request to begin a search with the given needle
SelectAll, // Fully select the entire input OpenSearcher(PathBuf, String), // Start a project-wide search with the given location and needle
Save, // Save the current buffer SelectToken, // Fully select the token under the cursor
SelectAll, // Fully select the entire input
Save, // Save the current buffer
Mouse(MouseAction, [isize; 2], bool), // (action, pos, is_ctrl) Mouse(MouseAction, [isize; 2], bool), // (action, pos, is_ctrl)
Undo, Undo,
Redo, Redo,
Copy, Copy,
Cut, Cut,
Paste, Paste,
Duplicate,
Comment,
} }
/// How far should movement go? /// How far should movement go?
@ -304,7 +308,7 @@ impl RawEvent {
} }
} }
pub fn to_open_opener(&self, path: PathBuf) -> Option<Action> { pub fn to_open_opener(&self, path: &PathBuf) -> Option<Action> {
if matches!( if matches!(
&self.0, &self.0,
TerminalEvent::Key(KeyEvent { TerminalEvent::Key(KeyEvent {
@ -314,13 +318,13 @@ impl RawEvent {
.. ..
}) })
) { ) {
Some(Action::OpenOpener(path)) Some(Action::OpenOpener(path.clone()))
} else { } else {
None None
} }
} }
pub fn to_open_finder(&self, selection: Option<String>) -> Option<Action> { pub fn to_open_finder(&self, query: Option<String>) -> Option<Action> {
if matches!( if matches!(
&self.0, &self.0,
TerminalEvent::Key(KeyEvent { TerminalEvent::Key(KeyEvent {
@ -330,7 +334,7 @@ impl RawEvent {
.. ..
}) })
) { ) {
Some(Action::OpenFinder(selection)) Some(Action::OpenFinder(query))
} else { } else {
None None
} }
@ -347,6 +351,16 @@ impl RawEvent {
}) })
) { ) {
Some(Action::CommandStart("goto_line")) Some(Action::CommandStart("goto_line"))
} else if matches!(
&self.0,
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('f'),
modifiers,
kind: KeyEventKind::Press,
..
}) if *modifiers == KeyModifiers::CONTROL | KeyModifiers::SHIFT
) {
Some(Action::CommandStart("search"))
} else { } else {
None None
} }
@ -464,6 +478,18 @@ impl RawEvent {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
.. ..
}) => Some(Action::Paste), }) => Some(Action::Paste),
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
}) => Some(Action::Duplicate),
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('7'), // ?????
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
}) => Some(Action::Comment),
_ => None, _ => None,
} }
} }

View file

@ -34,8 +34,8 @@ pub enum TokenKind {
Constant, Constant,
} }
#[derive(Default)]
pub struct Highlighter { pub struct Highlighter {
// regex: meta::Regex,
matchers: Vec<Regex>, matchers: Vec<Regex>,
entries: Vec<TokenKind>, entries: Vec<TokenKind>,
} }
@ -55,187 +55,19 @@ impl Highlighter {
} }
pub fn with(mut self, token: TokenKind, p: impl AsRef<str>) -> Self { pub fn with(mut self, token: TokenKind, p: impl AsRef<str>) -> Self {
self.entries.push(token); self.with_many([(token, p)])
self.matchers
.push(Regex::parser().parse(p.as_ref()).unwrap());
self
} }
pub fn from_file_name(file_name: &Path) -> Option<Self> { pub fn with_many<P: AsRef<str>>(
match file_name.extension()?.to_str()? { mut self,
"rs" => Some(Self::rust()), patterns: impl IntoIterator<Item = (TokenKind, P)>,
"md" => Some(Self::markdown()), ) -> Self {
"toml" => Some(Self::toml()), for (token, p) in patterns {
"c" | "h" | "cpp" | "hpp" | "cxx" | "js" | "ts" | "go" => Some(Self::generic_clike()), self.entries.push(token);
"glsl" | "vert" | "frag" => Some(Self::glsl()), self.matchers
_ => None, .push(Regex::parser().parse(p.as_ref()).unwrap());
} }
} self
pub fn markdown() -> Self {
Self::new_from_regex([
// Links
(TokenKind::String, r"\[[^\]]*\](\([^\)]*\))?"),
// Header
(TokenKind::Doc, r"^#+[[:space:]][^$]*$"),
// List item
(TokenKind::Operator, r"^[[:space:]]?[\-([0-9]+[\)\.])]"),
// Bold
(TokenKind::Property, r"\*\*[^(\*\*)]*\*\*"),
// Italics
(TokenKind::Attribute, r"\*[^\*]*\*"),
// Code block
(TokenKind::Operator, r"^```[^(^```)]*^```"),
// Inline code
(TokenKind::Constant, r"`[^`$]*[`$]"),
// HTML
(TokenKind::Special, r"<[^<>]*>"),
])
}
pub fn rust() -> Self {
Self::new_from_regex([
// Both kinds of comments match multiple lines
(
TokenKind::Doc,
r"\/\/[\/!][^\n]*$(\n[[:space:]]\/\/[\/!][^\n]*$)*",
),
(TokenKind::Comment, r"\/\/[^$]*$(\n[[:space:]]\/\/[^$]*$)*"),
// Multi-line comment
(TokenKind::Comment, r"\/\*[^(\*\/)]*\*\/"),
(
TokenKind::Keyword,
r"\b[(pub)(enum)(let)(self)(Self)(fn)(impl)(struct)(use)(if)(while)(for)(in)(loop)(mod)(match)(else)(break)(continue)(trait)(const)(static)(type)(mut)(as)(crate)(extern)(move)(ref)(return)(super)(unsafe)(use)(where)(async)(dyn)(try)(gen)(macro_rules)(union)(raw)]\b",
),
(TokenKind::Constant, r"\b[(true)(false)]\b"),
// Flow-control operators count as keywords
(TokenKind::Keyword, r"\.await\b"),
// Macro invocations: println!
(TokenKind::Macro, r"\b[A-Za-z_][A-Za-z0-9_]*!"),
// Meta-variables
(TokenKind::Macro, r"\$[A-Za-z_][A-Za-z0-9_]*\b"),
(TokenKind::Constant, r"\b[A-Z][A-Z0-9_]+\b"),
(TokenKind::Type, r"\b[A-Z][A-Za-z0-9_]*\b"),
// Primitives
(
TokenKind::Type,
r"\b[(u8)(u16)(u32)(u64)(u128)(i8)(i16)(i32)(i64)(i128)(usize)(isize)(bool)(str)(char)(f16)(f32)(f64)(f128)]\b",
),
// "foo" or b"foo" or r#"foo"#
(TokenKind::String, r#"b?r?(#*)@("[(\\")[^("~)]]*("~))"#),
// Characters
(
TokenKind::String,
r#"b?'[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^']]'"#,
),
(
TokenKind::Operator,
r"[(&(mut)?)(\?)(\+=?)(\-=?)(\*=?)(\/=?)(%=?)(!=?)(==?)(&&?=?)(\|\|?=?)(<<?=?)(>>?=?)(\.\.[\.=]?)\\\~\^:;,\@(=>?)]",
),
// Fields and methods: a.foo
(TokenKind::Property, r"\.[a-z_][A-Za-z0-9_]*"),
// Paths: std::foo::bar
(TokenKind::Property, r"[A-Za-z_][A-Za-z0-9_]*::"),
// Lifetimes
(TokenKind::Special, r"'[a-z_][A-Za-z0-9_]*\b"),
(TokenKind::Ident, r"\b[a-z_][A-Za-z0-9_]*\b"),
(TokenKind::Number, r"[0-9][A-Za-z0-9_\.]*"),
(TokenKind::Delimiter, r"[\{\}\(\)\[\]]"),
(TokenKind::Macro, r"[\{\}\(\)\[\]]"),
(TokenKind::Attribute, r"#!?\[[^\]]*\]"),
])
}
pub fn clike(keyword: &str, r#type: &str, builtin: &str) -> Self {
Self::new_from_regex([
// Both kinds of comments match multiple lines
(
TokenKind::Doc,
r"\/\/[\/!][^\n]*$(\n[[:space:]]\/\/[\/!][^\n]*$)*",
),
(TokenKind::Comment, r"\/\/[^$]*$(\n[[:space:]]\/\/[^$]*$)*"),
// Multi-line comment
(TokenKind::Comment, r"\/\*[^(\*\/)]*\*\/"),
(TokenKind::Keyword, keyword),
(TokenKind::Macro, builtin),
(TokenKind::Constant, r"\b[(true)(false)]\b"),
// Flow-control operators count as keywords
(TokenKind::Keyword, r"\b[(\.await)\?]\b"),
(TokenKind::Constant, r"\b[A-Z][A-Z0-9_]+\b"),
(TokenKind::Type, r"\b[A-Z][A-Za-z0-9_]*\b"),
// Primitives
(TokenKind::Type, r#type),
// "foo" or b"foo" or r#"foo"#
(TokenKind::String, r#"b?r?(#*)@("[(\\")[^("~)]]*("~))"#),
// Character strings
(
TokenKind::String,
r#"b?'[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^']]*'"#,
),
(
TokenKind::Operator,
r"[(&)(\?)(\+\+)(\-\-)(\+=?)(\-=?)(\*=?)(\/=?)(%=?)(!=?)(==?)(&&?=?)(\|\|?=?)(<<?=?)(>>?=?)(\.\.[\.=]?)\\\~\^:;,\@(=>?)]",
),
// Fields and methods: a.foo
(TokenKind::Property, r"\.[a-z_][A-Za-z0-9_]*"),
// Paths: std::foo::bar
(TokenKind::Property, r"[A-Za-z_][A-Za-z0-9_]*::"),
(TokenKind::Ident, r"\b[a-z_][A-Za-z0-9_]*\b"),
(TokenKind::Number, r"[0-9][A-Za-z0-9_\.]*"),
(TokenKind::Delimiter, r"[\{\}\(\)\[\]]"),
// Preprocessor
(TokenKind::Macro, r"^#[^$]*$"),
])
}
pub fn generic_clike() -> Self {
Self::clike(
// keyword
r"\b[(var)(enum)(let)(this)(fn)(struct)(class)(import)(if)(while)(for)(in)(loop)(else)(break)(continue)(const)(static)(type)(extern)(return)(async)(throw)(catch)(union)(auto)(namespace)(public)(private)(function)(func)]\b",
// types
r"\b[(([(unsigned)(signed)][[:space:]])*u?int[0-9]*(_t)?)(float)(double)(bool)(char)(size_t)(void)]\b",
"[]",
)
}
pub fn glsl() -> Self {
Self::clike(
// keyword
r"\b[(struct)(if)(while)(for)(else)(break)(continue)(const)(return)(layout)(uniform)(set)(binding)(location)(in)]\b",
// types
r"\b[(u?int)(float)(double)(bool)(void)([ui]?vec[1-4]*)([ui]?mat[1-4]*)(texture[(2D)(3D)]?(Cube)?)([ui]?sampler[(2D)(3D)]?(Shadow)?)]\b",
// Builtins
r"\b[(dot)(cross)(textureSize)(normalize)(texelFetch)(textureProj)(max)(min)(clamp)(reflect)(mix)(distance)(length)(abs)(pow)(sign)(sin)(cos)(tan)(fract)(mod)(round)(step)]\b",
)
}
pub fn toml() -> Self {
Self::new_from_regex([
// Header
(TokenKind::Doc, r#"^\[[^\n\]]*\]$"#),
// Delimiters
(TokenKind::Delimiter, r"[\{\}\(\)\[\]]"),
// Operators
(TokenKind::Operator, r"[=,]"),
// Numbers
(TokenKind::Number, r"[0-9][A-Za-z0-9_\.]*"),
// Double-quoted strings
(
TokenKind::String,
r#"b?"[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^"]]*""#,
),
// Single-quoted strings
(
TokenKind::String,
r#"b?'[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^']]*'"#,
),
// Booleans
(TokenKind::Constant, r"\b[(true)(false)]\b"),
// Identifier
(TokenKind::Ident, r"\b[a-z_][A-Za-z0-9_\-]*\b"),
// Comments
(TokenKind::Comment, r"#[^$]*$"),
])
} }
fn highlight_str(&self, mut s: &[char]) -> Vec<Token> { fn highlight_str(&self, mut s: &[char]) -> Vec<Token> {
@ -264,17 +96,14 @@ impl Highlighter {
tokens tokens
} }
pub fn highlight(self, s: &[char]) -> Highlights { pub fn highlight(&self, s: &[char]) -> Highlights {
let tokens = self.highlight_str(s); let tokens = self.highlight_str(s);
Highlights { Highlights { tokens }
highlighter: self,
tokens,
}
} }
} }
#[derive(Default)]
pub struct Highlights { pub struct Highlights {
pub highlighter: Highlighter,
tokens: Vec<Token>, tokens: Vec<Token>,
} }

242
src/lang/mod.rs Normal file
View file

@ -0,0 +1,242 @@
use super::*;
use crate::highlight::{Highlighter, TokenKind};
use std::path::Path;
#[derive(Default)]
pub struct LangPack {
pub highlighter: Highlighter,
pub comment_syntax: Option<Vec<char>>,
}
impl LangPack {
pub fn from_file_name(file_name: &Path) -> Self {
match file_name.extension().and_then(|e| e.to_str()).unwrap_or("") {
"rs" => Self {
highlighter: Highlighter::default().rust(),
comment_syntax: Some(vec!['/', '/', ' ']),
},
"md" => Self {
highlighter: Highlighter::default().markdown(),
comment_syntax: None,
},
"toml" => Self {
highlighter: Highlighter::default().toml(),
comment_syntax: Some(vec!['#', ' ']),
},
"c" | "h" | "cpp" | "hpp" | "cxx" | "js" | "ts" | "go" => Self {
highlighter: Highlighter::default().generic_clike(),
comment_syntax: Some(vec!['/', '/', ' ']),
},
"glsl" | "vert" | "frag" => Self {
highlighter: Highlighter::default().glsl(),
comment_syntax: Some(vec!['/', '/', ' ']),
},
"py" => Self {
highlighter: Highlighter::default().python(),
comment_syntax: Some(vec!['#', ' ']),
},
"tao" => Self {
highlighter: Highlighter::default().tao(),
comment_syntax: Some(vec!['#', ' ']),
},
_ => Self {
highlighter: Highlighter::default(),
comment_syntax: None,
},
}
}
}
impl Highlighter {
pub fn markdown(self) -> Self {
self
// Links
.with(TokenKind::String, r"\[[^\]]*\](\([^\)]*\))?")
// Header
.with(TokenKind::Doc, r"^#+[[:space:]][^$]*$")
// List item
.with(TokenKind::Operator, r"^[[:space:]]?[\-([0-9]+[\)\.])]")
// Bold
.with(TokenKind::Property, r"\*\*[^(\*\*)]*\*\*")
// Italics
.with(TokenKind::Attribute, r"\*[^\*]*\*")
// Code block
.with(TokenKind::Operator, r"^```[^(^```)]*^```")
// Inline code
.with(TokenKind::Constant, r"`[^`$]*[`$]")
// HTML
.with(TokenKind::Special, r"<[^<>]*>")
}
pub fn rust(self) -> Self {
self
// Both kinds of comments match multiple lines
.with(TokenKind::Doc, r"\/\/[\/!][^\n]*$(\n[[:space:]]\/\/[\/!][^\n]*$)*")
.with(TokenKind::Comment, r"\/\/[^$]*$(\n[[:space:]]\/\/[^$]*$)*")
// Multi-line comment
.with(TokenKind::Comment, r"\/\*[^(\*\/)]*\*\/")
.with(
TokenKind::Keyword,
r"\b[(pub)(enum)(let)(self)(Self)(fn)(impl)(struct)(use)(if)(while)(for)(in)(loop)(mod)(match)(else)(break)(continue)(trait)(const)(static)(type)(mut)(as)(crate)(extern)(move)(ref)(return)(super)(unsafe)(use)(where)(async)(dyn)(try)(gen)(macro_rules)(union)(raw)]\b",
)
.with(TokenKind::Constant, r"\b[(true)(false)]\b")
// Flow-control operators count as keywords
.with(TokenKind::Keyword, r"\.await\b")
// Macro invocations: println!
.with(TokenKind::Macro, r"\b[A-Za-z_][A-Za-z0-9_]*!")
// Meta-variables
.with(TokenKind::Macro, r"\$[A-Za-z_][A-Za-z0-9_]*\b")
.with(TokenKind::Constant, r"\b[A-Z][A-Z0-9_]+\b")
.with(TokenKind::Type, r"\b[A-Z][A-Za-z0-9_]*\b")
// Primitives
.with(
TokenKind::Type,
r"\b[(u8)(u16)(u32)(u64)(u128)(i8)(i16)(i32)(i64)(i128)(usize)(isize)(bool)(str)(char)(f16)(f32)(f64)(f128)]\b",
)
// "foo" or b"foo" or r#"foo"#
.with(TokenKind::String, r#"b?r?(#*)@("[(\\")[^("~)]]*("~))"#)
// Characters
.with(
TokenKind::String,
r#"b?'[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^']]'"#,
)
.with(
TokenKind::Operator,
r"[(&(mut)?)(\?)(\+=?)(\-=?)(\*=?)(\/=?)(%=?)(!=?)(==?)(&&?=?)(\|\|?=?)(<<?=?)(>>?=?)(\.\.[\.=]?)\\\~\^:;,\@(=>?)]",
)
// Fields and methods: a.foo
.with(TokenKind::Property, r"\.[a-z_][A-Za-z0-9_]*")
// Paths: std::foo::bar
.with(TokenKind::Property, r"[A-Za-z_][A-Za-z0-9_]*::")
// Lifetimes
.with(TokenKind::Special, r"'[a-z_][A-Za-z0-9_]*\b")
.with(TokenKind::Ident, r"\b[a-z_][A-Za-z0-9_]*\b")
.with(TokenKind::Number, r"[0-9][A-Za-z0-9_\.]*")
.with(TokenKind::Delimiter, r"[\{\}\(\)\[\]]")
.with(TokenKind::Macro, r"[\{\}\(\)\[\]]")
.with(TokenKind::Attribute, r"#!?\[[^\]]*\]")
}
fn clike_comments(self) -> Self {
self
// Both kinds of comments match multiple lines
.with(
TokenKind::Doc,
r"\/\/[\/!][^\n]*$(\n[[:space:]]\/\/[\/!][^\n]*$)*",
)
// Regular comment
.with(TokenKind::Comment, r"\/\/[^$]*$(\n[[:space:]]\/\/[^$]*$)*")
// Multi-line comment
.with(TokenKind::Comment, r"\/\*[^(\*\/)]*\*\/")
}
fn clike_preprocessor(self) -> Self {
self.with(TokenKind::Macro, r"^#[^$]*$")
}
pub fn clike(self) -> Self {
self
.with(TokenKind::Constant, r"\b[(true)(false)]\b")
.with(TokenKind::Constant, r"\b[A-Z][A-Z0-9_]+\b")
.with(TokenKind::Type, r"\b[A-Z][A-Za-z0-9_]*\b")
// "foo" or b"foo" or r#"foo"#
.with(TokenKind::String, r#"b?r?(#*)@("[(\\")[^("~)]]*("~))"#)
// Character strings
.with(TokenKind::String, r#"b?'[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^']]*'"#)
.with(
TokenKind::Operator,
r"[(&)(\?)(\+\+)(\-\-)(\+=?)(\-=?)(\*=?)(\/=?)(%=?)(!=?)(==?)(&&?=?)(\|\|?=?)(<<?=?)(>>?=?)(\.\.[\.=]?)\\\~\^:;,\@(=>?)]",
)
// Fields and methods: a.foo
.with(TokenKind::Property, r"\.[a-z_][A-Za-z0-9_]*")
// Paths: std::foo::bar
.with(TokenKind::Property, r"[A-Za-z_][A-Za-z0-9_]*::")
.with(TokenKind::Ident, r"\b[a-z_][A-Za-z0-9_]*\b")
.with(TokenKind::Number, r"[0-9][A-Za-z0-9_\.]*")
.with(TokenKind::Delimiter, r"[\{\}\(\)\[\]]")
}
pub fn generic_clike(self) -> Self {
self
// Keywords
.with(TokenKind::Keyword, r"\b[(var)(enum)(let)(this)(fn)(struct)(class)(import)(if)(while)(for)(in)(loop)(else)(break)(continue)(const)(static)(type)(extern)(return)(async)(throw)(catch)(union)(auto)(namespace)(public)(private)(function)(func)]\b")
// Primitives
.with(TokenKind::Type, r"\b[(([(unsigned)(signed)][[:space:]])*u?int[0-9]*(_t)?)(float)(double)(bool)(char)(size_t)(void)]\b")
.clike_comments()
.clike_preprocessor()
.clike()
}
pub fn glsl(self) -> Self {
self
// Keywords
.with(TokenKind::Keyword, r"\b[(struct)(if)(while)(for)(else)(break)(continue)(const)(return)(layout)(uniform)(set)(binding)(location)(in)]\b")
// Primitives
.with(TokenKind::Type, r"\b[(u?int)(float)(double)(bool)(void)([ui]?vec[1-4]*)([ui]?mat[1-4]*)(texture[(2D)(3D)]?(Cube)?)([ui]?sampler[(2D)(3D)]?(Shadow)?)]\b")
// Builtins
.with(TokenKind::Macro, r"\b[(dot)(cross)(textureSize)(normalize)(texelFetch)(textureProj)(max)(min)(clamp)(reflect)(mix)(distance)(length)(abs)(pow)(sign)(sin)(cos)(tan)(fract)(mod)(round)(step)]\b")
.clike_comments()
.clike_preprocessor()
.clike()
}
pub fn python(self) -> Self {
self
// Keywords
.with(TokenKind::Keyword, r"\b[(and)(as)(assert)(break)(class)(continue)(def)(del)(elif)(else)(except)(finally)(for)(from)(global)(if)(import)(in)(is)(lambda)(nonlocal)(not)(or)(pass)(raise)(return)(try)(while)(with)(yield)]\b")
// Primitives
.with(TokenKind::Type, r"\b[]\b")
// Builtins
.with(TokenKind::Macro, r"\b[(True)(False)(None)]\b")
// Doc comments
.with(TokenKind::Doc, r"^##[^$]*$")
// Comments
.with(TokenKind::Comment, r"^#[^$]*$")
.clike()
}
pub fn tao(self) -> Self {
self
// Keywords
.with(TokenKind::Keyword, r"\b[(data)(member)(def)(class)(type)(effect)(import)(handle)(with)(match)(if)(else)(for)(of)(let)(fn)(return)(in)(mod)(where)(when)(do)]\b")
// Primitives
.with(TokenKind::Type, r"\b[(Str)(Bool)(Nat)(Char)]\b")
// Builtins
.with(TokenKind::Macro, r"\b[(True)(False)]\b")
// Doc comments
.with(TokenKind::Doc, r"^##[^$]*$")
// Comments
.with(TokenKind::Comment, r"^#[^$]*$")
// Attributes
.with(TokenKind::Attribute, r"\$!?\[[^\]]*\]")
.clike()
}
pub fn toml(self) -> Self {
self
// Header
.with(TokenKind::Doc, r#"^\[[^\n\]]*\]$"#)
// Delimiters
.with(TokenKind::Delimiter, r"[\{\}\(\)\[\]]")
// Operators
.with(TokenKind::Operator, r"[=,]")
// Numbers
.with(TokenKind::Number, r"[0-9][A-Za-z0-9_\.]*")
// Double-quoted strings
.with(
TokenKind::String,
r#"b?"[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^"]]*""#,
)
// Single-quoted strings
.with(
TokenKind::String,
r#"b?'[(\\[nrt\\0(x[0-7A-Za-z][0-7A-Za-z])])[^']]*'"#,
)
// Booleans
.with(TokenKind::Constant, r"\b[(true)(false)]\b")
// Identifier
.with(TokenKind::Ident, r"\b[a-z_][A-Za-z0-9_\-]*\b")
// Comments
.with(TokenKind::Comment, r"#[^$]*$")
}
}

View file

@ -1,5 +1,6 @@
mod action; mod action;
mod highlight; mod highlight;
mod lang;
mod state; mod state;
mod terminal; mod terminal;
mod theme; mod theme;

View file

@ -1,8 +1,4 @@
use crate::{ use crate::{Args, Dir, Error, highlight::Highlights, lang::LangPack, theme};
Args, Dir, Error,
highlight::{Highlighter, Highlights},
theme,
};
use clipboard::{ClipboardContext, ClipboardProvider}; use clipboard::{ClipboardContext, ClipboardProvider};
use slotmap::{HopSlotMap, new_key_type}; use slotmap::{HopSlotMap, new_key_type};
use std::{ use std::{
@ -145,13 +141,15 @@ impl Text {
pub struct Buffer { pub struct Buffer {
pub unsaved: bool, pub unsaved: bool,
pub text: Text, pub text: Text,
pub highlights: Option<Highlights>, pub lang: LangPack,
pub highlights: Highlights,
pub cursors: HopSlotMap<CursorId, Cursor>, pub cursors: HopSlotMap<CursorId, Cursor>,
pub dir: Option<PathBuf>, pub dir: Option<PathBuf>,
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
pub undo: Vec<Change>, pub undo: Vec<Change>,
pub redo: Vec<Change>, pub redo: Vec<Change>,
action_counter: usize, action_counter: usize,
most_recent_rank: usize,
} }
pub struct Change { pub struct Change {
@ -197,9 +195,11 @@ impl Buffer {
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
let lang = LangPack::from_file_name(&path);
Ok(Self { Ok(Self {
unsaved, unsaved,
highlights: Highlighter::from_file_name(&path).map(|h| h.highlight(&chars)), highlights: lang.highlighter.highlight(&chars),
lang,
text: Text { chars }, text: Text { chars },
cursors: HopSlotMap::default(), cursors: HopSlotMap::default(),
dir, dir,
@ -207,6 +207,7 @@ impl Buffer {
undo: Vec::new(), undo: Vec::new(),
redo: Vec::new(), redo: Vec::new(),
action_counter: 0, action_counter: 0,
most_recent_rank: 0,
}) })
} }
@ -231,10 +232,7 @@ impl Buffer {
} }
fn update_highlights(&mut self) { fn update_highlights(&mut self) {
self.highlights = self self.highlights = self.lang.highlighter.highlight(self.text.chars());
.highlights
.take()
.map(|hl| hl.highlighter.highlight(self.text.chars()));
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
@ -264,24 +262,20 @@ impl Buffer {
let Some(cursor) = self.cursors.get_mut(cursor_id) else { let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return; return;
}; };
if let Some(tok) = self
.highlights let a = self.highlights.get_at(cursor.pos);
.as_ref() let b = self.highlights.get_at(cursor.pos.saturating_sub(1));
// Choose the longest token that the cursor is touching if let Some(tok) = a
.and_then(|hl| { .zip(b)
let a = hl.get_at(cursor.pos); .map(|(a, b)| {
let b = hl.get_at(cursor.pos.saturating_sub(1)); if a.range.end - a.range.start > b.range.end - b.range.start {
a.zip(b) a
.map(|(a, b)| { } else {
if a.range.end - a.range.start > b.range.end - b.range.start { b
a }
} else {
b
}
})
.or(a)
.or(b)
}) })
.or(a)
.or(b)
{ {
cursor.select(tok.range.clone()); cursor.select(tok.range.clone());
} else { } else {
@ -768,6 +762,73 @@ impl Buffer {
} }
} }
pub fn duplicate(&mut self, cursor_id: CursorId) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return;
};
if let Some(s) = cursor.selection()
&& let Some(text) = cursor.selection().and_then(|s| self.text.chars().get(s))
{
// cursor.place_at(s.end);
self.insert_after(cursor_id, text.to_vec())
} else {
let coord = self.text.to_coord(cursor.pos);
let line = self
.text
.lines()
.nth(coord[1].max(0) as usize)
.map(|l| l.to_vec());
if let Some(line) = line {
let end_of_line = self.text.to_pos([0, coord[1] + 1]);
self.insert(end_of_line, line);
}
}
}
pub fn comment(&mut self, cursor_id: CursorId) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return;
};
let Some(comment_syntax) = self.lang.comment_syntax.clone() else {
return;
};
let lines = cursor
.selection()
.map(|s| self.text.to_coord(s.start)[1]..=self.text.to_coord(s.end)[1])
.unwrap_or_else(|| {
let coord = self.text.to_coord(cursor.pos);
coord[1]..=coord[1]
});
let mut indent: Option<&[char]> = None;
for line_idx in lines.clone() {
indent = Some(match (indent, self.text.indent_of_line(line_idx)) {
(Some(indent), new_indent) => {
&new_indent[..indent
.iter()
.zip(new_indent)
.take_while(|(x, y)| x == y)
.count()]
}
(None, new_indent) => new_indent,
});
}
let indent = indent.unwrap_or(&[]).to_vec();
for line_idx in lines {
let pos = self.text.to_pos([indent.len() as isize, line_idx]);
if self
.text
.chars()
.get(pos..)
.map_or(false, |l| l.starts_with(&comment_syntax))
{
self.remove(pos..pos + comment_syntax.len());
} else {
self.insert(pos, comment_syntax.iter().copied());
}
}
}
pub fn start_session(&mut self) -> CursorId { pub fn start_session(&mut self) -> CursorId {
self.cursors.insert(Cursor::default()) self.cursors.insert(Cursor::default())
} }
@ -791,6 +852,7 @@ pub struct State {
pub buffers: HopSlotMap<BufferId, Buffer>, pub buffers: HopSlotMap<BufferId, Buffer>,
pub tick: u64, pub tick: u64,
pub theme: theme::Theme, pub theme: theme::Theme,
pub most_recent_counter: usize,
} }
impl TryFrom<Args> for State { impl TryFrom<Args> for State {
@ -800,6 +862,7 @@ impl TryFrom<Args> for State {
buffers: HopSlotMap::default(), buffers: HopSlotMap::default(),
tick: 0, tick: 0,
theme: theme::Theme::default(), theme: theme::Theme::default(),
most_recent_counter: 0,
}; };
if args.paths.is_empty() { if args.paths.is_empty() {
@ -829,4 +892,17 @@ impl State {
pub fn tick(&mut self) { pub fn tick(&mut self) {
self.tick += 1; self.tick += 1;
} }
pub fn set_most_recent(&mut self, buffer: BufferId) {
if let Some(buffer) = self.buffers.get_mut(buffer) {
self.most_recent_counter += 1;
buffer.most_recent_rank = self.most_recent_counter;
}
}
pub fn most_recent(&self) -> Vec<BufferId> {
let mut most_recent = self.buffers.keys().collect::<Vec<_>>();
most_recent.sort_by_key(|b| core::cmp::Reverse(self.buffers[*b].most_recent_rank));
most_recent
}
} }

View file

@ -249,6 +249,10 @@ impl<'a> Rect<'a> {
} }
self.rect([0, 0], self.size()) self.rect([0, 0], self.size())
} }
pub fn set_title(&mut self, title: String) {
self.fb.title = title;
}
} }
#[derive(Default)] #[derive(Default)]
@ -256,6 +260,7 @@ pub struct Framebuffer {
size: [u16; 2], size: [u16; 2],
cells: Vec<Cell>, cells: Vec<Cell>,
cursor: Option<([u16; 2], CursorStyle)>, cursor: Option<([u16; 2], CursorStyle)>,
title: String,
} }
impl Framebuffer { impl Framebuffer {
@ -284,12 +289,14 @@ impl<'a> Terminal<'a> {
fn enter(mut stdout: impl io::Write) { fn enter(mut stdout: impl io::Write) {
let _ = terminal::enable_raw_mode(); let _ = terminal::enable_raw_mode();
let _ = stdout.execute(terminal::EnterAlternateScreen); let _ = stdout.execute(terminal::EnterAlternateScreen);
let _ = stdout.execute(terminal::DisableLineWrap);
let _ = stdout.execute(event::EnableMouseCapture); let _ = stdout.execute(event::EnableMouseCapture);
} }
fn leave(mut stdout: impl io::Write) { fn leave(mut stdout: impl io::Write) {
let _ = terminal::disable_raw_mode(); let _ = terminal::disable_raw_mode();
let _ = stdout.execute(terminal::LeaveAlternateScreen); let _ = stdout.execute(terminal::LeaveAlternateScreen);
let _ = stdout.execute(terminal::EnableLineWrap);
let _ = stdout.execute(cursor::Show); let _ = stdout.execute(cursor::Show);
let _ = stdout.execute(event::DisableMouseCapture); let _ = stdout.execute(event::DisableMouseCapture);
} }
@ -348,6 +355,10 @@ impl<'a> Terminal<'a> {
stdout.queue(style::Print('\x07')).unwrap(); stdout.queue(style::Print('\x07')).unwrap();
} }
if self.fb[0].title != self.fb[1].title {
stdout.queue(terminal::SetTitle(&self.fb[0].title)).unwrap();
}
let mut cursor_pos = [0, 0]; let mut cursor_pos = [0, 0];
let mut fg = Color::Reset; let mut fg = Color::Reset;
let mut bg = Color::Reset; let mut bg = Color::Reset;
@ -394,6 +405,9 @@ impl<'a> Terminal<'a> {
// Convert non-printable chars // Convert non-printable chars
let c = match self.fb[0].cells[pos].c { let c = match self.fb[0].cells[pos].c {
c if c.is_whitespace() => ' ', c if c.is_whitespace() => ' ',
c if c.is_control() => {
char::from_u32(9216 + c as u32).unwrap_or('?')
}
c => c, c => c,
}; };
stdout.queue(style::Print(c)).unwrap(); stdout.queue(style::Print(c)).unwrap();

View file

@ -10,7 +10,7 @@ pub struct Doc {
// Remember the cursor we use for each buffer // Remember the cursor we use for each buffer
cursors: HashMap<BufferId, CursorId>, cursors: HashMap<BufferId, CursorId>,
input: Input, input: Input,
search: Option<Search>, finder: Option<Finder>,
} }
impl Doc { impl Doc {
@ -22,7 +22,7 @@ impl Doc {
.into_iter() .into_iter()
.collect(), .collect(),
input: Input::default(), input: Input::default(),
search: None, finder: None,
} }
} }
@ -36,6 +36,7 @@ impl Doc {
} }
fn switch_buffer(&mut self, state: &mut State, buffer: BufferId) { fn switch_buffer(&mut self, state: &mut State, buffer: BufferId) {
state.set_most_recent(buffer);
self.buffer = buffer; self.buffer = buffer;
let Some(buffer) = state.buffers.get_mut(self.buffer) else { let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return; return;
@ -53,10 +54,10 @@ impl Element for Doc {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
let cursor_id = self.cursors[&self.buffer]; let cursor_id = self.cursors[&self.buffer];
if let Some(search) = &mut self.search { if let Some(finder) = &mut self.finder {
let resp = search.handle(state, &mut self.input, self.buffer, cursor_id, event)?; let resp = finder.handle(state, &mut self.input, self.buffer, cursor_id, event)?;
if resp.is_end() { if resp.is_end() {
self.search = None; self.finder = None;
} }
return Ok(Resp::handled(resp.event)); return Ok(Resp::handled(resp.event));
} }
@ -70,21 +71,18 @@ impl Element for Doc {
.to_owned() .to_owned()
.unwrap_or_else(|| std::env::current_dir().expect("no working dir")); .unwrap_or_else(|| std::env::current_dir().expect("no working dir"));
let selection = buffer.cursors[cursor_id]
.selection()
.map(|range| buffer.text.chars()[range].iter().copied().collect());
match event.to_action(|e| { match event.to_action(|e| {
e.to_open_switcher() e.to_open_switcher()
.or_else(|| e.to_open_opener(open_path)) .or_else(|| e.to_open_opener(&open_path))
.or_else(|| e.to_open_finder(selection)) .or_else(|| e.to_open_finder(None))
.or_else(|| e.to_move()) .or_else(|| e.to_move())
.or_else(|| e.to_save()) .or_else(|| e.to_save())
}) { }) {
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))), action @ Some(Action::OpenSwitcher) | action @ Some(Action::OpenOpener(_)) => {
action @ Some(Action::OpenOpener(_)) => Ok(Resp::handled(action.map(Into::into))), Ok(Resp::handled(action.map(Into::into)))
}
ref action @ Some(Action::OpenFinder(ref query)) => { ref action @ Some(Action::OpenFinder(ref query)) => {
self.search = Some(Search::new( self.finder = Some(Finder::new(
buffer.cursors[cursor_id], buffer.cursors[cursor_id],
query.clone(), query.clone(),
state, state,
@ -94,13 +92,27 @@ impl Element for Doc {
)); ));
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::BeginSearch(needle)) => {
let path = buffer
.path
.clone()
.unwrap_or_else(|| std::env::current_dir().expect("no cwd"));
Ok(Resp::handled(Some(
Action::OpenSearcher(path, needle).into(),
)))
}
Some(Action::SwitchBuffer(new_buffer)) => { Some(Action::SwitchBuffer(new_buffer)) => {
self.switch_buffer(state, new_buffer); self.switch_buffer(state, new_buffer);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::OpenFile(path)) => match state.open_or_get(path) { Some(Action::OpenFile(path, line_idx)) => match state.open_or_get(path) {
Ok(buffer_id) => { Ok(buffer_id) => {
self.switch_buffer(state, buffer_id); self.switch_buffer(state, buffer_id);
if let Some(buffer) = state.buffers.get_mut(self.buffer) {
let cursor_id = self.cursors[&self.buffer];
buffer.goto_cursor(cursor_id, [0, line_idx as isize], true);
self.input.refocus(buffer, cursor_id);
}
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Err(err) => Ok(Resp::handled(Some( Err(err) => Ok(Resp::handled(Some(
@ -130,40 +142,48 @@ impl Visual for Doc {
}; };
let cursor_id = self.cursors[&self.buffer]; let cursor_id = self.cursors[&self.buffer];
let search_h = if self.search.is_some() { 3 } else { 0 }; if frame.has_focus() {
frame.set_title(if let Some(path) = &buffer.path {
format!("{}: {}", env!("CARGO_PKG_NAME"), path.display())
} else {
format!("{}: Unsaved", env!("CARGO_PKG_NAME"))
});
}
let finder_h = if self.finder.is_some() { 3 } else { 0 };
// Render input // Render input
frame frame
.rect( .rect(
[0, 0], [0, 0],
[frame.size()[0], frame.size()[1].saturating_sub(search_h)], [frame.size()[0], frame.size()[1].saturating_sub(finder_h)],
) )
.with_focus(true /*self.search.is_none()*/) .with_focus(true /*self.finder.is_none()*/)
.with(|f| { .with(|f| {
self.input.render( self.input.render(
state, state,
buffer.name().as_deref(), buffer.name().as_deref(),
buffer, buffer,
cursor_id, cursor_id,
self.search.as_ref(), self.finder.as_ref(),
f, f,
) )
}); });
// Render search // Render finder
if let Some(search) = &mut self.search { if let Some(finder) = &mut self.finder {
frame frame
.rect( .rect(
[0, frame.size()[1].saturating_sub(search_h)], [0, frame.size()[1].saturating_sub(finder_h)],
[frame.size()[0], search_h], [frame.size()[0], finder_h],
) )
.with_focus(true) .with_focus(true)
.with(|f| search.render(state, f)); .with(|f| finder.render(state, f));
} }
} }
} }
pub struct Search { pub struct Finder {
old_cursor: Cursor, old_cursor: Cursor,
buffer: Buffer, buffer: Buffer,
@ -175,7 +195,7 @@ pub struct Search {
results: Vec<usize>, results: Vec<usize>,
} }
impl Search { impl Finder {
fn new( fn new(
old_cursor: Cursor, old_cursor: Cursor,
query: Option<String>, query: Option<String>,
@ -292,7 +312,7 @@ impl Search {
} }
} }
impl Visual for Search { impl Visual for Finder {
fn render(&mut self, state: &State, frame: &mut Rect) { fn render(&mut self, state: &State, frame: &mut Rect) {
let title = format!("{} of {} results", self.selected + 1, self.results.len()); let title = format!("{} of {} results", self.selected + 1, self.results.len());
self.input.render( self.input.render(

View file

@ -38,8 +38,9 @@ impl Input {
pub fn focus(&mut self, coord: [isize; 2]) { pub fn focus(&mut self, coord: [isize; 2]) {
for i in 0..2 { for i in 0..2 {
self.focus[i] = self.focus[i] = self.focus[i]
self.focus[i].clamp(coord[i] - self.last_area.size()[i] as isize + 1, coord[i]); .max(coord[i] - self.last_area.size()[i] as isize + 1)
.min(coord[i]);
} }
} }
@ -188,6 +189,15 @@ impl Input {
Ok(Resp::handled(Some(Event::Bell))) Ok(Resp::handled(Some(Event::Bell)))
} }
} }
Some(Action::Duplicate) => {
buffer.duplicate(cursor_id);
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
Some(Action::Comment) => {
buffer.comment(cursor_id);
Ok(Resp::handled(None))
}
_ => Err(event), _ => Err(event),
} }
} }
@ -198,7 +208,7 @@ impl Input {
title: Option<&str>, title: Option<&str>,
buffer: &Buffer, buffer: &Buffer,
cursor_id: CursorId, cursor_id: CursorId,
search: Option<&Search>, finder: Option<&Finder>,
frame: &mut Rect, frame: &mut Rect,
) { ) {
// Add frame // Add frame
@ -274,10 +284,8 @@ impl Input {
let (fg, c) = match line.get(coord as usize).copied() { let (fg, c) = match line.get(coord as usize).copied() {
Some('\n') if selected => (state.theme.whitespace, '⮠'), Some('\n') if selected => (state.theme.whitespace, '⮠'),
Some(c) => { Some(c) => {
if let Some(fg) = buffer if let Some(fg) = pos
.highlights .and_then(|pos| buffer.highlights.get_at(pos))
.as_ref()
.and_then(|hl| hl.get_at(pos?))
.map(|tok| state.theme.token_color(tok.kind)) .map(|tok| state.theme.token_color(tok.kind))
{ {
(fg, c) (fg, c)
@ -287,7 +295,7 @@ impl Input {
} }
None => (Color::Reset, ' '), None => (Color::Reset, ' '),
}; };
let bg = match search.map(|s| s.contains(pos?)) { let bg = match finder.map(|s| s.contains(pos?)) {
Some(Some(true)) => state.theme.select_bg, Some(Some(true)) => state.theme.select_bg,
Some(Some(false)) => state.theme.search_result_bg, Some(Some(false)) => state.theme.search_result_bg,
Some(None) if line_selected && frame.has_focus() => { Some(None) if line_selected && frame.has_focus() => {

View file

@ -3,14 +3,16 @@ mod input;
mod panes; mod panes;
mod prompt; mod prompt;
mod root; mod root;
mod search;
mod status; mod status;
pub use self::{ pub use self::{
doc::{Doc, Search}, doc::{Doc, Finder},
input::Input, input::Input,
panes::{Pane, Panes}, panes::{Pane, Panes},
prompt::{Confirm, Opener, Prompt, Show, Switcher}, prompt::{Confirm, Opener, Prompt, Show, Switcher},
root::Root, root::Root,
search::Searcher,
status::Status, status::Status,
}; };
@ -156,10 +158,10 @@ impl<T: Clone> Element<T> for Options<T> {
Some(Action::Move(dir, Dist::Char, false, false)) => { Some(Action::Move(dir, Dist::Char, false, false)) => {
match dir { match dir {
Dir::Up => { Dir::Up => {
self.selected = self.selected = (self.selected + self.ranking.len()).saturating_sub(1)
(self.selected + self.ranking.len() - 1) % self.ranking.len() % self.ranking.len().max(1)
} }
Dir::Down => self.selected = (self.selected + 1) % self.ranking.len(), Dir::Down => self.selected = (self.selected + 1) % self.ranking.len().max(1),
_ => return Err(event), _ => return Err(event),
} }
Ok(Resp::handled(None)) Ok(Resp::handled(None))

View file

@ -86,9 +86,12 @@ impl Element for Panes {
self.selected = new_idx; self.selected = new_idx;
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Mouse(_, pos, _)) => { Some(Action::Mouse(action, pos, _)) => {
for (i, pane) in self.panes.iter_mut().enumerate() { for (i, pane) in self.panes.iter_mut().enumerate() {
if pane.last_area.contains(pos).is_some() { if pane.last_area.contains(pos).is_some() {
if matches!(action, MouseAction::Click) {
self.selected = i;
}
match &mut pane.kind { match &mut pane.kind {
PaneKind::Doc(doc) => return doc.handle(state, event), PaneKind::Doc(doc) => return doc.handle(state, event),
PaneKind::Empty => {} PaneKind::Empty => {}

View file

@ -54,6 +54,10 @@ impl Prompt {
- 1; - 1;
Ok(Action::GotoLine(line)) Ok(Action::GotoLine(line))
} }
Some("search") => {
let needle = args.next().ok_or_else(|| "Expected argument".to_string())?;
Ok(Action::BeginSearch(needle.to_string()))
}
Some(cmd) => Err(format!("Unknown command `{cmd}`")), Some(cmd) => Err(format!("Unknown command `{cmd}`")),
None => Err(format!("No command entered")), None => Err(format!("No command entered")),
} }
@ -397,7 +401,7 @@ impl Element<()> for Opener {
self.set_string(&format!("{}/", file.path.display())); self.set_string(&format!("{}/", file.path.display()));
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Ok(Some(file)) => Ok(Resp::end(Some(Action::OpenFile(file.path).into()))), Ok(Some(file)) => Ok(Resp::end(Some(Action::OpenFile(file.path, 0).into()))),
Ok(None) => Ok(Resp::handled(None)), Ok(None) => Ok(Resp::handled(None)),
Err(event) => { Err(event) => {
let res = self let res = self

View file

@ -13,6 +13,7 @@ pub enum Task {
Confirm(Confirm), Confirm(Confirm),
Switcher(Switcher), Switcher(Switcher),
Opener(Opener), Opener(Opener),
Searcher(Searcher),
} }
impl Task { impl Task {
@ -23,6 +24,7 @@ impl Task {
Self::Confirm(c) => c.requested_height(), Self::Confirm(c) => c.requested_height(),
Self::Switcher(s) => s.requested_height(), Self::Switcher(s) => s.requested_height(),
Self::Opener(o) => o.requested_height(), Self::Opener(o) => o.requested_height(),
Self::Searcher(s) => s.requested_height(),
} }
} }
} }
@ -58,6 +60,7 @@ impl Element<()> for Root {
Task::Confirm(c) => c.handle(state, event), Task::Confirm(c) => c.handle(state, event),
Task::Switcher(s) => s.handle(state, event), Task::Switcher(s) => s.handle(state, event),
Task::Opener(o) => o.handle(state, event), Task::Opener(o) => o.handle(state, event),
Task::Searcher(s) => s.handle(state, event),
}; };
match res { match res {
@ -92,12 +95,16 @@ impl Element<()> for Root {
Action::OpenSwitcher => { Action::OpenSwitcher => {
self.tasks.clear(); // Overrides all self.tasks.clear(); // Overrides all
self.tasks self.tasks
.push(Task::Switcher(Switcher::new(state.buffers.keys()))); .push(Task::Switcher(Switcher::new(state.most_recent())));
} }
Action::OpenOpener(path) => { Action::OpenOpener(path) => {
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::OpenSearcher(path, needle) => {
self.tasks.clear(); // Overrides all
self.tasks.push(Task::Searcher(Searcher::new(path, needle)));
}
Action::CommandStart(cmd) => { Action::CommandStart(cmd) => {
self.tasks.clear(); // Prompt overrides all self.tasks.clear(); // Prompt overrides all
self.tasks self.tasks
@ -158,6 +165,7 @@ impl Visual for Root {
Task::Confirm(c) => c.render(state, frame), Task::Confirm(c) => c.render(state, frame),
Task::Switcher(s) => s.render(state, frame), Task::Switcher(s) => s.render(state, frame),
Task::Opener(o) => o.render(state, frame), Task::Opener(o) => o.render(state, frame),
Task::Searcher(s) => s.render(state, frame),
}); });
} }

182
src/ui/search.rs Normal file
View file

@ -0,0 +1,182 @@
use super::*;
use crate::state::{Buffer, CursorId};
use std::fs;
pub struct Searcher {
options: Options<SearchResult>,
path: PathBuf,
needle: String,
// Filter
buffer: Buffer,
cursor_id: CursorId,
input: Input,
}
impl Searcher {
pub fn new(mut path: PathBuf, needle: String) -> Self {
let path = loop {
if let Ok(mut entries) = fs::read_dir(&path)
&& entries.any(|e| {
e.map_or(false, |e| {
e.file_name() == ".git" && e.file_type().map_or(false, |t| t.is_dir())
})
})
{
break path;
} else if !path.pop() {
break std::env::current_dir().expect("No cwd");
}
};
fn search_in(path: &PathBuf, needle: &str, results: &mut Vec<SearchResult>) {
// Cap reached!
if results.len() < 500 {
// Skip hidden files
if path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.starts_with(".")
{
return;
}
if let Ok(file) = fs::File::open(path)
&& let Ok(md) = file.metadata()
// Maximum 1 MB
&& md.len() < 1 << 20
&& let Ok(s) = fs::read_to_string(path)
{
for (line_idx, line_text) in
s.lines().enumerate().filter(|(_, l)| l.contains(needle))
{
results.push(SearchResult {
path: path.clone(),
line_idx,
line_text: line_text.trim().to_string(),
});
}
} else if let Ok(entries) = fs::read_dir(path) {
// Special case, ignore Rust target dir to prevent searching too many places
{
let mut path = path.clone();
path.push("CACHEDIR.TAG");
if path.exists() {
return;
}
}
for entry in entries {
let Ok(entry) = entry else { continue };
search_in(&entry.path(), needle, results);
}
}
}
}
let mut results = Vec::new();
search_in(&path, &needle, &mut results);
let mut buffer = Buffer::default();
let cursor_id = buffer.start_session();
Self {
options: Options::new(results),
path,
needle,
cursor_id,
buffer,
input: Input::filter(),
}
}
pub fn requested_height(&self) -> usize {
self.options.requested_height() + 3
}
fn update_completions(&mut self) {
let filter = self.buffer.text.to_string().to_lowercase();
self.options.apply_scoring(|e| {
let name = format!("{}", e.path.display()).to_lowercase();
if name == filter {
Some((0, name.chars().count()))
} else if name.starts_with(&filter) {
Some((1, name.chars().count()))
} else if name.contains(&filter) {
Some((2, name.chars().count()))
} else {
None
}
});
}
}
impl Element<()> for Searcher {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char))) {
Some(Action::Cancel) => Ok(Resp::end(None)),
_ => match self.options.handle(state, event).map(Resp::into_ended) {
// Selecting a directory enters the directory
Ok(Some(result)) => Ok(Resp::end(Some(Event::Action(Action::OpenFile(
result.path,
result.line_idx,
))))),
Ok(None) => Ok(Resp::handled(None)),
Err(event) => {
let res = self
.input
.handle(&mut self.buffer, self.cursor_id, event)
.map(Resp::into_can_end);
self.update_completions();
res
}
},
}
}
}
#[derive(Clone)]
pub struct SearchResult {
pub path: PathBuf,
pub line_idx: usize,
pub line_text: String,
}
impl Visual for SearchResult {
fn render(&mut self, state: &State, frame: &mut Rect) {
let name = match self.path.file_name().and_then(|n| n.to_str()) {
Some(name) => format!("{name}"),
None => format!("Unknown"),
};
frame
.with_fg(state.theme.option_file)
.text([0, 0], &format!("{name}:{}", self.line_idx + 1));
frame.with_fg(state.theme.margin_line_num).with(|f| {
f.text(
[f.size()[0] as isize / 3, 0],
&format!("{}", self.line_text),
);
});
}
}
impl Visual for Searcher {
fn render(&mut self, state: &State, frame: &mut Rect) {
frame
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
.with(|f| self.options.render(state, f));
frame
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
.with(|f| {
let title = format!(
"{} of {} results for '{}' in {}/",
self.options.selected + 1,
self.options.ranking.len(),
self.needle,
self.path.display()
);
self.input
.render(state, Some(&title), &self.buffer, self.cursor_id, None, f)
});
}
}