Compare commits
10 commits
cec0ae50e5
...
d45deb2685
| Author | SHA1 | Date | |
|---|---|---|---|
| d45deb2685 | |||
| aee42780be | |||
| 0a13d5c1f1 | |||
| b4008574cd | |||
| e5916526cb | |||
| b0ff49cbb6 | |||
| d13982cc05 | |||
| b0b4de2f64 | |||
| 9cc9729ed9 | |||
| 5fcd147c8c |
13 changed files with 680 additions and 265 deletions
|
|
@ -36,18 +36,22 @@ pub enum Action {
|
|||
OpenOpener(PathBuf), // Open the file opener
|
||||
OpenFinder(Option<String>), // Open the finder, with the given default query
|
||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||
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
|
||||
SelectToken, // Fully select the token under the cursor
|
||||
SelectAll, // Fully select the entire input
|
||||
Save, // Save the current buffer
|
||||
OpenFile(PathBuf, usize), // Open the file (on the given line) and switch the current pane to it
|
||||
CommandStart(&'static str), // Start a new command
|
||||
GotoLine(isize), // Go to the specified file line
|
||||
BeginSearch(String), // Request to begin a search with the given needle
|
||||
OpenSearcher(PathBuf, String), // Start a project-wide search with the given location and needle
|
||||
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)
|
||||
Undo,
|
||||
Redo,
|
||||
Copy,
|
||||
Cut,
|
||||
Paste,
|
||||
Duplicate,
|
||||
Comment,
|
||||
}
|
||||
|
||||
/// 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!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
|
|
@ -314,13 +318,13 @@ impl RawEvent {
|
|||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::OpenOpener(path))
|
||||
Some(Action::OpenOpener(path.clone()))
|
||||
} else {
|
||||
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!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
|
|
@ -330,7 +334,7 @@ impl RawEvent {
|
|||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::OpenFinder(selection))
|
||||
Some(Action::OpenFinder(query))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -347,6 +351,16 @@ impl RawEvent {
|
|||
})
|
||||
) {
|
||||
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 {
|
||||
None
|
||||
}
|
||||
|
|
@ -464,6 +478,18 @@ impl RawEvent {
|
|||
kind: KeyEventKind::Press,
|
||||
..
|
||||
}) => 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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ pub enum TokenKind {
|
|||
Constant,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Highlighter {
|
||||
// regex: meta::Regex,
|
||||
matchers: Vec<Regex>,
|
||||
entries: Vec<TokenKind>,
|
||||
}
|
||||
|
|
@ -55,187 +55,19 @@ impl Highlighter {
|
|||
}
|
||||
|
||||
pub fn with(mut self, token: TokenKind, p: impl AsRef<str>) -> Self {
|
||||
self.entries.push(token);
|
||||
self.matchers
|
||||
.push(Regex::parser().parse(p.as_ref()).unwrap());
|
||||
self
|
||||
self.with_many([(token, p)])
|
||||
}
|
||||
|
||||
pub fn from_file_name(file_name: &Path) -> Option<Self> {
|
||||
match file_name.extension()?.to_str()? {
|
||||
"rs" => Some(Self::rust()),
|
||||
"md" => Some(Self::markdown()),
|
||||
"toml" => Some(Self::toml()),
|
||||
"c" | "h" | "cpp" | "hpp" | "cxx" | "js" | "ts" | "go" => Some(Self::generic_clike()),
|
||||
"glsl" | "vert" | "frag" => Some(Self::glsl()),
|
||||
_ => None,
|
||||
pub fn with_many<P: AsRef<str>>(
|
||||
mut self,
|
||||
patterns: impl IntoIterator<Item = (TokenKind, P)>,
|
||||
) -> Self {
|
||||
for (token, p) in patterns {
|
||||
self.entries.push(token);
|
||||
self.matchers
|
||||
.push(Regex::parser().parse(p.as_ref()).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
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"#[^$]*$"),
|
||||
])
|
||||
self
|
||||
}
|
||||
|
||||
fn highlight_str(&self, mut s: &[char]) -> Vec<Token> {
|
||||
|
|
@ -264,17 +96,14 @@ impl Highlighter {
|
|||
tokens
|
||||
}
|
||||
|
||||
pub fn highlight(self, s: &[char]) -> Highlights {
|
||||
pub fn highlight(&self, s: &[char]) -> Highlights {
|
||||
let tokens = self.highlight_str(s);
|
||||
Highlights {
|
||||
highlighter: self,
|
||||
tokens,
|
||||
}
|
||||
Highlights { tokens }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Highlights {
|
||||
pub highlighter: Highlighter,
|
||||
tokens: Vec<Token>,
|
||||
}
|
||||
|
||||
242
src/lang/mod.rs
Normal file
242
src/lang/mod.rs
Normal 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"#[^$]*$")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
mod action;
|
||||
mod highlight;
|
||||
mod lang;
|
||||
mod state;
|
||||
mod terminal;
|
||||
mod theme;
|
||||
|
|
|
|||
132
src/state.rs
132
src/state.rs
|
|
@ -1,8 +1,4 @@
|
|||
use crate::{
|
||||
Args, Dir, Error,
|
||||
highlight::{Highlighter, Highlights},
|
||||
theme,
|
||||
};
|
||||
use crate::{Args, Dir, Error, highlight::Highlights, lang::LangPack, theme};
|
||||
use clipboard::{ClipboardContext, ClipboardProvider};
|
||||
use slotmap::{HopSlotMap, new_key_type};
|
||||
use std::{
|
||||
|
|
@ -145,13 +141,15 @@ impl Text {
|
|||
pub struct Buffer {
|
||||
pub unsaved: bool,
|
||||
pub text: Text,
|
||||
pub highlights: Option<Highlights>,
|
||||
pub lang: LangPack,
|
||||
pub highlights: Highlights,
|
||||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||
pub dir: Option<PathBuf>,
|
||||
pub path: Option<PathBuf>,
|
||||
pub undo: Vec<Change>,
|
||||
pub redo: Vec<Change>,
|
||||
action_counter: usize,
|
||||
most_recent_rank: usize,
|
||||
}
|
||||
|
||||
pub struct Change {
|
||||
|
|
@ -197,9 +195,11 @@ impl Buffer {
|
|||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
let lang = LangPack::from_file_name(&path);
|
||||
Ok(Self {
|
||||
unsaved,
|
||||
highlights: Highlighter::from_file_name(&path).map(|h| h.highlight(&chars)),
|
||||
highlights: lang.highlighter.highlight(&chars),
|
||||
lang,
|
||||
text: Text { chars },
|
||||
cursors: HopSlotMap::default(),
|
||||
dir,
|
||||
|
|
@ -207,6 +207,7 @@ impl Buffer {
|
|||
undo: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
action_counter: 0,
|
||||
most_recent_rank: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -231,10 +232,7 @@ impl Buffer {
|
|||
}
|
||||
|
||||
fn update_highlights(&mut self) {
|
||||
self.highlights = self
|
||||
.highlights
|
||||
.take()
|
||||
.map(|hl| hl.highlighter.highlight(self.text.chars()));
|
||||
self.highlights = self.lang.highlighter.highlight(self.text.chars());
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
|
|
@ -264,24 +262,20 @@ impl Buffer {
|
|||
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
||||
return;
|
||||
};
|
||||
if let Some(tok) = self
|
||||
.highlights
|
||||
.as_ref()
|
||||
// Choose the longest token that the cursor is touching
|
||||
.and_then(|hl| {
|
||||
let a = hl.get_at(cursor.pos);
|
||||
let b = hl.get_at(cursor.pos.saturating_sub(1));
|
||||
a.zip(b)
|
||||
.map(|(a, b)| {
|
||||
if a.range.end - a.range.start > b.range.end - b.range.start {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
})
|
||||
.or(a)
|
||||
.or(b)
|
||||
|
||||
let a = self.highlights.get_at(cursor.pos);
|
||||
let b = self.highlights.get_at(cursor.pos.saturating_sub(1));
|
||||
if let Some(tok) = a
|
||||
.zip(b)
|
||||
.map(|(a, b)| {
|
||||
if a.range.end - a.range.start > b.range.end - b.range.start {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
})
|
||||
.or(a)
|
||||
.or(b)
|
||||
{
|
||||
cursor.select(tok.range.clone());
|
||||
} 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 {
|
||||
self.cursors.insert(Cursor::default())
|
||||
}
|
||||
|
|
@ -791,6 +852,7 @@ pub struct State {
|
|||
pub buffers: HopSlotMap<BufferId, Buffer>,
|
||||
pub tick: u64,
|
||||
pub theme: theme::Theme,
|
||||
pub most_recent_counter: usize,
|
||||
}
|
||||
|
||||
impl TryFrom<Args> for State {
|
||||
|
|
@ -800,6 +862,7 @@ impl TryFrom<Args> for State {
|
|||
buffers: HopSlotMap::default(),
|
||||
tick: 0,
|
||||
theme: theme::Theme::default(),
|
||||
most_recent_counter: 0,
|
||||
};
|
||||
|
||||
if args.paths.is_empty() {
|
||||
|
|
@ -829,4 +892,17 @@ impl State {
|
|||
pub fn tick(&mut self) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,6 +249,10 @@ impl<'a> Rect<'a> {
|
|||
}
|
||||
self.rect([0, 0], self.size())
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: String) {
|
||||
self.fb.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -256,6 +260,7 @@ pub struct Framebuffer {
|
|||
size: [u16; 2],
|
||||
cells: Vec<Cell>,
|
||||
cursor: Option<([u16; 2], CursorStyle)>,
|
||||
title: String,
|
||||
}
|
||||
|
||||
impl Framebuffer {
|
||||
|
|
@ -284,12 +289,14 @@ impl<'a> Terminal<'a> {
|
|||
fn enter(mut stdout: impl io::Write) {
|
||||
let _ = terminal::enable_raw_mode();
|
||||
let _ = stdout.execute(terminal::EnterAlternateScreen);
|
||||
let _ = stdout.execute(terminal::DisableLineWrap);
|
||||
let _ = stdout.execute(event::EnableMouseCapture);
|
||||
}
|
||||
|
||||
fn leave(mut stdout: impl io::Write) {
|
||||
let _ = terminal::disable_raw_mode();
|
||||
let _ = stdout.execute(terminal::LeaveAlternateScreen);
|
||||
let _ = stdout.execute(terminal::EnableLineWrap);
|
||||
let _ = stdout.execute(cursor::Show);
|
||||
let _ = stdout.execute(event::DisableMouseCapture);
|
||||
}
|
||||
|
|
@ -348,6 +355,10 @@ impl<'a> Terminal<'a> {
|
|||
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 fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
|
|
@ -394,6 +405,9 @@ impl<'a> Terminal<'a> {
|
|||
// Convert non-printable chars
|
||||
let c = match self.fb[0].cells[pos].c {
|
||||
c if c.is_whitespace() => ' ',
|
||||
c if c.is_control() => {
|
||||
char::from_u32(9216 + c as u32).unwrap_or('?')
|
||||
}
|
||||
c => c,
|
||||
};
|
||||
stdout.queue(style::Print(c)).unwrap();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub struct Doc {
|
|||
// Remember the cursor we use for each buffer
|
||||
cursors: HashMap<BufferId, CursorId>,
|
||||
input: Input,
|
||||
search: Option<Search>,
|
||||
finder: Option<Finder>,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
|
|
@ -22,7 +22,7 @@ impl Doc {
|
|||
.into_iter()
|
||||
.collect(),
|
||||
input: Input::default(),
|
||||
search: None,
|
||||
finder: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +36,7 @@ impl Doc {
|
|||
}
|
||||
|
||||
fn switch_buffer(&mut self, state: &mut State, buffer: BufferId) {
|
||||
state.set_most_recent(buffer);
|
||||
self.buffer = buffer;
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return;
|
||||
|
|
@ -53,10 +54,10 @@ impl Element for Doc {
|
|||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
|
||||
if let Some(search) = &mut self.search {
|
||||
let resp = search.handle(state, &mut self.input, self.buffer, cursor_id, event)?;
|
||||
if let Some(finder) = &mut self.finder {
|
||||
let resp = finder.handle(state, &mut self.input, self.buffer, cursor_id, event)?;
|
||||
if resp.is_end() {
|
||||
self.search = None;
|
||||
self.finder = None;
|
||||
}
|
||||
return Ok(Resp::handled(resp.event));
|
||||
}
|
||||
|
|
@ -70,21 +71,18 @@ impl Element for Doc {
|
|||
.to_owned()
|
||||
.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| {
|
||||
e.to_open_switcher()
|
||||
.or_else(|| e.to_open_opener(open_path))
|
||||
.or_else(|| e.to_open_finder(selection))
|
||||
.or_else(|| e.to_open_opener(&open_path))
|
||||
.or_else(|| e.to_open_finder(None))
|
||||
.or_else(|| e.to_move())
|
||||
.or_else(|| e.to_save())
|
||||
}) {
|
||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))),
|
||||
action @ Some(Action::OpenOpener(_)) => Ok(Resp::handled(action.map(Into::into))),
|
||||
action @ Some(Action::OpenSwitcher) | action @ Some(Action::OpenOpener(_)) => {
|
||||
Ok(Resp::handled(action.map(Into::into)))
|
||||
}
|
||||
ref action @ Some(Action::OpenFinder(ref query)) => {
|
||||
self.search = Some(Search::new(
|
||||
self.finder = Some(Finder::new(
|
||||
buffer.cursors[cursor_id],
|
||||
query.clone(),
|
||||
state,
|
||||
|
|
@ -94,13 +92,27 @@ impl Element for Doc {
|
|||
));
|
||||
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)) => {
|
||||
self.switch_buffer(state, new_buffer);
|
||||
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) => {
|
||||
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))
|
||||
}
|
||||
Err(err) => Ok(Resp::handled(Some(
|
||||
|
|
@ -130,40 +142,48 @@ impl Visual for Doc {
|
|||
};
|
||||
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
|
||||
frame
|
||||
.rect(
|
||||
[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| {
|
||||
self.input.render(
|
||||
state,
|
||||
buffer.name().as_deref(),
|
||||
buffer,
|
||||
cursor_id,
|
||||
self.search.as_ref(),
|
||||
self.finder.as_ref(),
|
||||
f,
|
||||
)
|
||||
});
|
||||
|
||||
// Render search
|
||||
if let Some(search) = &mut self.search {
|
||||
// Render finder
|
||||
if let Some(finder) = &mut self.finder {
|
||||
frame
|
||||
.rect(
|
||||
[0, frame.size()[1].saturating_sub(search_h)],
|
||||
[frame.size()[0], search_h],
|
||||
[0, frame.size()[1].saturating_sub(finder_h)],
|
||||
[frame.size()[0], finder_h],
|
||||
)
|
||||
.with_focus(true)
|
||||
.with(|f| search.render(state, f));
|
||||
.with(|f| finder.render(state, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Search {
|
||||
pub struct Finder {
|
||||
old_cursor: Cursor,
|
||||
|
||||
buffer: Buffer,
|
||||
|
|
@ -175,7 +195,7 @@ pub struct Search {
|
|||
results: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Search {
|
||||
impl Finder {
|
||||
fn new(
|
||||
old_cursor: Cursor,
|
||||
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) {
|
||||
let title = format!("{} of {} results", self.selected + 1, self.results.len());
|
||||
self.input.render(
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ impl Input {
|
|||
|
||||
pub fn focus(&mut self, coord: [isize; 2]) {
|
||||
for i in 0..2 {
|
||||
self.focus[i] =
|
||||
self.focus[i].clamp(coord[i] - self.last_area.size()[i] as isize + 1, coord[i]);
|
||||
self.focus[i] = self.focus[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)))
|
||||
}
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
@ -198,7 +208,7 @@ impl Input {
|
|||
title: Option<&str>,
|
||||
buffer: &Buffer,
|
||||
cursor_id: CursorId,
|
||||
search: Option<&Search>,
|
||||
finder: Option<&Finder>,
|
||||
frame: &mut Rect,
|
||||
) {
|
||||
// Add frame
|
||||
|
|
@ -274,10 +284,8 @@ impl Input {
|
|||
let (fg, c) = match line.get(coord as usize).copied() {
|
||||
Some('\n') if selected => (state.theme.whitespace, '⮠'),
|
||||
Some(c) => {
|
||||
if let Some(fg) = buffer
|
||||
.highlights
|
||||
.as_ref()
|
||||
.and_then(|hl| hl.get_at(pos?))
|
||||
if let Some(fg) = pos
|
||||
.and_then(|pos| buffer.highlights.get_at(pos))
|
||||
.map(|tok| state.theme.token_color(tok.kind))
|
||||
{
|
||||
(fg, c)
|
||||
|
|
@ -287,7 +295,7 @@ impl Input {
|
|||
}
|
||||
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(false)) => state.theme.search_result_bg,
|
||||
Some(None) if line_selected && frame.has_focus() => {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ mod input;
|
|||
mod panes;
|
||||
mod prompt;
|
||||
mod root;
|
||||
mod search;
|
||||
mod status;
|
||||
|
||||
pub use self::{
|
||||
doc::{Doc, Search},
|
||||
doc::{Doc, Finder},
|
||||
input::Input,
|
||||
panes::{Pane, Panes},
|
||||
prompt::{Confirm, Opener, Prompt, Show, Switcher},
|
||||
root::Root,
|
||||
search::Searcher,
|
||||
status::Status,
|
||||
};
|
||||
|
||||
|
|
@ -156,10 +158,10 @@ impl<T: Clone> Element<T> for Options<T> {
|
|||
Some(Action::Move(dir, Dist::Char, false, false)) => {
|
||||
match dir {
|
||||
Dir::Up => {
|
||||
self.selected =
|
||||
(self.selected + self.ranking.len() - 1) % self.ranking.len()
|
||||
self.selected = (self.selected + self.ranking.len()).saturating_sub(1)
|
||||
% 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),
|
||||
}
|
||||
Ok(Resp::handled(None))
|
||||
|
|
|
|||
|
|
@ -86,9 +86,12 @@ impl Element for Panes {
|
|||
self.selected = new_idx;
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Mouse(_, pos, _)) => {
|
||||
Some(Action::Mouse(action, pos, _)) => {
|
||||
for (i, pane) in self.panes.iter_mut().enumerate() {
|
||||
if pane.last_area.contains(pos).is_some() {
|
||||
if matches!(action, MouseAction::Click) {
|
||||
self.selected = i;
|
||||
}
|
||||
match &mut pane.kind {
|
||||
PaneKind::Doc(doc) => return doc.handle(state, event),
|
||||
PaneKind::Empty => {}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ impl Prompt {
|
|||
- 1;
|
||||
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}`")),
|
||||
None => Err(format!("No command entered")),
|
||||
}
|
||||
|
|
@ -397,7 +401,7 @@ impl Element<()> for Opener {
|
|||
self.set_string(&format!("{}/", file.path.display()));
|
||||
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)),
|
||||
Err(event) => {
|
||||
let res = self
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub enum Task {
|
|||
Confirm(Confirm),
|
||||
Switcher(Switcher),
|
||||
Opener(Opener),
|
||||
Searcher(Searcher),
|
||||
}
|
||||
|
||||
impl Task {
|
||||
|
|
@ -23,6 +24,7 @@ impl Task {
|
|||
Self::Confirm(c) => c.requested_height(),
|
||||
Self::Switcher(s) => s.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::Switcher(s) => s.handle(state, event),
|
||||
Task::Opener(o) => o.handle(state, event),
|
||||
Task::Searcher(s) => s.handle(state, event),
|
||||
};
|
||||
|
||||
match res {
|
||||
|
|
@ -92,12 +95,16 @@ impl Element<()> for Root {
|
|||
Action::OpenSwitcher => {
|
||||
self.tasks.clear(); // Overrides all
|
||||
self.tasks
|
||||
.push(Task::Switcher(Switcher::new(state.buffers.keys())));
|
||||
.push(Task::Switcher(Switcher::new(state.most_recent())));
|
||||
}
|
||||
Action::OpenOpener(path) => {
|
||||
self.tasks.clear(); // Overrides all
|
||||
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) => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks
|
||||
|
|
@ -158,6 +165,7 @@ impl Visual for Root {
|
|||
Task::Confirm(c) => c.render(state, frame),
|
||||
Task::Switcher(s) => s.render(state, frame),
|
||||
Task::Opener(o) => o.render(state, frame),
|
||||
Task::Searcher(s) => s.render(state, frame),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
182
src/ui/search.rs
Normal file
182
src/ui/search.rs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue