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
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,
}
}

View file

@ -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
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 highlight;
mod lang;
mod state;
mod terminal;
mod theme;

View file

@ -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
}
}

View file

@ -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();

View file

@ -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(

View file

@ -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() => {

View file

@ -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))

View file

@ -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 => {}

View file

@ -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

View file

@ -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
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)
});
}
}