mod insert;
mod prompt;
mod visual;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::action::PromptKind;
use crate::finder::{FuzzyKind, IgnoreOpts, workspace_files};
use crate::lsp::{Location, Position, Range, Severity, path_to_uri, uri_to_path};
use crate::mode::Mode;
use crate::buffer_ref::BufferRef;
use super::{App, eval};
impl App {
pub fn handle_paste(&mut self, s: String) {
if self.prompt.is_open() {
let filtered: String = s.chars().filter(|&c| c != '\n' && c != '\r').collect();
for c in filtered.chars() {
let ev = KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE);
let _ = self.handle_prompt_key(ev);
}
return;
}
match self.mode {
Mode::Insert => self.insert_pasted_text(s),
Mode::Normal => {
if s.is_empty() {
return;
}
self.buffer.snapshot();
self.buffer.insert_text_raw(&s);
self.buffer.clamp_col(false);
}
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {}
}
}
pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
if self.prompt.is_open() {
return self.handle_prompt_key(key);
}
if self.jump_state.is_some() {
self.handle_jump_key(key);
return Ok(());
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
self.should_quit = true;
return Ok(());
}
if matches!(self.mode, Mode::Normal) && key.code == KeyCode::Esc && self.toasts.has_fatal()
{
self.toasts.dismiss_fatal();
return Ok(());
}
match self.mode {
Mode::Insert => return self.handle_insert_key(key),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
return self.handle_visual_key(key);
}
Mode::Normal => {}
}
match eval::tokenize(&self.config.keymap, &self.tokens, self.mode, key) {
Some(t) => self.tokens.push(t),
None => {
self.tokens.clear();
return Ok(());
}
}
match eval::classify(&self.tokens) {
eval::Parse::Complete(expr) => {
self.tokens.clear();
self.evaluate(expr, crate::action::Ctx::default())?;
}
eval::Parse::Incomplete => {}
eval::Parse::Invalid => self.tokens.clear(),
}
Ok(())
}
pub(in crate::app) fn enter_mode(&mut self, mode: Mode) {
if mode.is_visual() && !self.mode.is_visual() {
self.visual_anchor = Some(self.buffer.cursor);
} else if !mode.is_visual() {
self.visual_anchor = None;
}
if mode == Mode::Normal {
self.buffer.clamp_col(false);
}
let entering_insert = mode == Mode::Insert && self.mode != Mode::Insert;
self.mode = mode;
if entering_insert {
self.schedule_inline_suggestion();
}
}
pub fn open_prompt(&mut self, kind: PromptKind) {
match kind {
PromptKind::Command => self.prompt.open_command(),
PromptKind::Search { forward } => self.prompt.open_search(forward),
PromptKind::Fuzzy(FuzzyKind::Files { ignore }) => {
self.prompt.open_files(&self.startup_cwd, ignore)
}
PromptKind::Fuzzy(FuzzyKind::Lines) => self.prompt.open_lines(&self.buffer.lines),
PromptKind::Fuzzy(FuzzyKind::Buffers) => self.open_buffer_picker(),
PromptKind::Fuzzy(FuzzyKind::Locations) => {}
PromptKind::Fuzzy(FuzzyKind::WorkspaceSearch) => self.open_workspace_search(),
PromptKind::Fuzzy(FuzzyKind::Diagnostics { workspace }) => {
self.open_diagnostics_picker(workspace)
}
PromptKind::Explorer => self.prompt.open_explorer(
&self.startup_cwd,
IgnoreOpts::DEFAULT,
self.config.editor.compact_folders,
),
}
}
fn open_diagnostics_picker(&mut self, workspace: bool) {
let mut items: Vec<String> = Vec::new();
let mut locations: Vec<Location> = Vec::new();
let root = self.startup_cwd.clone();
if workspace {
for (uri, diags) in self.all_diagnostics() {
let label = relative_uri_label(&uri, &root);
for d in &diags {
items.push(format!(
"[{}] {}:{}",
severity_tag(d.severity),
label,
d.range.start.line + 1,
));
locations.push(Location {
uri: uri.clone(),
range: d.range,
});
}
}
} else {
let uri = match self.buffer.path.as_ref().map(|p| path_to_uri(p)) {
Some(u) => u,
None => {
self.push_toast(crate::app::Toast::info("no diagnostics"));
return;
}
};
let Some(diags) = self.current_diagnostics() else {
self.push_toast(crate::app::Toast::info("no diagnostics"));
return;
};
for d in &diags {
items.push(format!(
"[{}] {}",
severity_tag(d.severity),
d.range.start.line + 1,
));
locations.push(Location {
uri: uri.clone(),
range: d.range,
});
}
}
if items.is_empty() {
self.push_toast(crate::app::Toast::info("no diagnostics"));
return;
}
self.prompt.open_diagnostics(items, locations, workspace);
}
fn open_buffer_picker(&mut self) {
let cwd = &self.startup_cwd;
let current_path = self
.buffer
.path
.as_ref()
.and_then(|p| p.canonicalize().ok());
let on_scratch = self.buffer.path.is_none();
let active_dirty = self.buffer.dirty;
let active_vcs_changed = self.buffer.has_vcs_changes();
let vcs_set = crate::vcs::changed_files(cwd);
let (items, refs): (Vec<_>, Vec<_>) = self
.opened_paths
.iter()
.rev() .map(|r| {
let (label, is_current) = match r {
BufferRef::Scratch(id) => {
let is_current = on_scratch && self.current_scratch_id == Some(*id);
(BufferRef::scratch_label(*id), is_current)
}
BufferRef::File(p) => {
let rel = p
.strip_prefix(cwd)
.unwrap_or(p)
.to_string_lossy()
.to_string();
let is_current = current_path.as_ref() == Some(p);
(rel, is_current)
}
};
let entry_dirty = if is_current {
active_dirty
} else {
self.sleeping.get(r).is_some_and(|b| b.dirty)
};
let entry_vcs = match r {
BufferRef::Scratch(_) => false,
BufferRef::File(p) => {
if is_current {
active_vcs_changed
} else {
vcs_set.contains(p) || entry_dirty
}
}
};
let cur_col = if is_current { '%' } else { ' ' };
let vcs_col = if entry_vcs { '~' } else { ' ' };
let mod_col = if entry_dirty { '+' } else { ' ' };
let display = format!("{}{}{} {}", cur_col, vcs_col, mod_col, label);
(display, r.clone())
})
.unzip();
self.prompt.open_buffers(items, refs);
}
fn open_workspace_search(&mut self) {
let cwd = &self.startup_cwd;
let files = workspace_files(cwd, IgnoreOpts::DEFAULT);
let mut items: Vec<String> = Vec::new();
let mut file_lines: Vec<Vec<String>> = Vec::new();
let mut locations: Vec<Location> = Vec::new();
let mut total_lines: usize = 0;
for rel in files {
if total_lines >= WORKSPACE_SEARCH_MAX_LINES {
break;
}
if !is_searchable_ext(&rel) {
continue;
}
let abs = cwd.join(&rel);
let meta = match std::fs::metadata(&abs) {
Ok(m) => m,
Err(_) => continue,
};
if meta.len() > WORKSPACE_SEARCH_MAX_FILE_BYTES {
continue;
}
let content = match std::fs::read_to_string(&abs) {
Ok(s) => s,
Err(_) => continue,
};
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
if lines.is_empty() {
continue;
}
total_lines += lines.len();
items.push(rel.clone());
file_lines.push(lines);
locations.push(Location {
uri: path_to_uri(&abs),
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
});
}
self.prompt
.open_workspace_search(items, file_lines, locations);
}
}
fn relative_uri_label(uri: &str, root: &std::path::Path) -> String {
let Some(path) = uri_to_path(uri) else {
return uri.to_string();
};
let path_c = path.canonicalize().unwrap_or_else(|_| path.clone());
let root_c = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
path_c
.strip_prefix(&root_c)
.unwrap_or(&path_c)
.to_string_lossy()
.into_owned()
}
fn severity_tag(sev: Severity) -> char {
match sev {
Severity::Error => 'E',
Severity::Warning => 'W',
Severity::Info => 'I',
Severity::Hint => 'H',
}
}
const WORKSPACE_SEARCH_MAX_LINES: usize = 50_000;
const WORKSPACE_SEARCH_MAX_FILE_BYTES: u64 = 1_000_000;
fn is_searchable_ext(rel: &str) -> bool {
let name = rel.rsplit('/').next().unwrap_or(rel);
if matches!(
name,
"Makefile" | "Dockerfile" | "Justfile" | "CMakeLists.txt" | "Cargo.toml" | "Cargo.lock"
) {
return true;
}
let Some(ext) = name.rsplit_once('.').map(|(_, e)| e) else {
return false;
};
matches!(
ext,
"rs" | "go" | "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx"
| "zig" | "v" | "nim" | "d"
| "java" | "kt" | "kts" | "scala" | "groovy" | "clj" | "cljs"
| "swift" | "m" | "mm"
| "py" | "rb" | "php" | "pl" | "lua" | "tcl" | "r"
| "sh" | "bash" | "zsh" | "fish"
| "js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx" | "vue" | "svelte"
| "html" | "htm" | "xml" | "css" | "scss" | "sass" | "less" | "styl"
| "hs" | "ml" | "mli" | "ex" | "exs" | "erl" | "elm" | "fs" | "fsx"
| "toml" | "yaml" | "yml" | "json" | "jsonc" | "ron" | "ini" | "conf"
| "env" | "properties"
| "md" | "mdx" | "rst" | "adoc" | "txt" | "tex"
| "mk" | "cmake" | "ninja" | "bazel" | "bzl" | "gradle"
| "sql" | "graphql" | "gql" | "proto"
)
}