diff --git a/Cargo.lock b/Cargo.lock index 61dd9c401..77a64ecce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,6 +554,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + [[package]] name = "copypasta" version = "0.10.1" @@ -1697,6 +1706,7 @@ dependencies = [ "clap", "code_tools", "colors-transform", + "content_inspector", "copypasta", "dirs 0.1.54", "filter", @@ -1824,9 +1834,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] @@ -3429,6 +3439,7 @@ version = "0.1.54" dependencies = [ "bytecount", "memchr", + "memmap2", "simdutf8", ] diff --git a/Cargo.toml b/Cargo.toml index 81cda03d5..d51714010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ chrono = { version = "0.4", features = ["serde"] } chrono-humanize = "0.2.3" clap = { version = "4.2", features = ["derive"] } colors-transform = "0.2.11" +content_inspector = "0.2.4" criterion = "0.5" directories = "4.0" futures = "0.3" @@ -63,6 +64,7 @@ indicatif = "0.16" itertools = "0.10" lsp = { package = "lsp-types", version = "0.94" } memchr = "2.5" +memmap2 = "0.9.5" num_cpus = "1.13" once_cell = "1.7" parking_lot = "0.12" diff --git a/autoload/clap/picker.vim b/autoload/clap/picker.vim index ba6460f6b..164b53182 100644 --- a/autoload/clap/picker.vim +++ b/autoload/clap/picker.vim @@ -73,7 +73,7 @@ function! clap#picker#update(update_info) abort call clap#sign#ensure_exists() - if has_key(update_info, 'preview') + if has_key(update_info, 'preview') && update_info.preview isnot v:null if !empty(update_info.preview) call clap#picker#update_preview(update_info.preview) endif diff --git a/crates/cli/benches/benchmark.rs b/crates/cli/benches/benchmark.rs index 79e361ffb..f5603f1ce 100644 --- a/crates/cli/benches/benchmark.rs +++ b/crates/cli/benches/benchmark.rs @@ -8,7 +8,7 @@ use rayon::prelude::*; use std::io::BufRead; use std::sync::Arc; use types::ClapItem; -use utils::line_count; +use utils::io::line_count; fn prepare_source_items() -> Vec { let largest_cache = find_largest_cache_digest().expect("Cache is empty"); diff --git a/crates/cli/src/command/blines.rs b/crates/cli/src/command/blines.rs index e3a8746e8..018adc397 100644 --- a/crates/cli/src/command/blines.rs +++ b/crates/cli/src/command/blines.rs @@ -98,7 +98,7 @@ impl Blines { filter::dyn_run( &self.query, filter_context, - SequentialSource::List(blines_item_stream()), + SequentialSource::Iterator(blines_item_stream()), )?; } diff --git a/crates/cli/src/command/cache.rs b/crates/cli/src/command/cache.rs index 987c18880..e77dbe31d 100644 --- a/crates/cli/src/command/cache.rs +++ b/crates/cli/src/command/cache.rs @@ -5,7 +5,7 @@ use maple_core::dirs::Dirs; use std::fs::read_dir; use std::io::Write; use std::path::{PathBuf, MAIN_SEPARATOR}; -use utils::remove_dir_contents; +use utils::io::remove_dir_contents; /// List and remove all the cached contents. #[derive(Subcommand, Debug, Clone)] diff --git a/crates/cli/src/command/ctags/recursive_tags.rs b/crates/cli/src/command/ctags/recursive_tags.rs index 108092285..38f6d517e 100644 --- a/crates/cli/src/command/ctags/recursive_tags.rs +++ b/crates/cli/src/command/ctags/recursive_tags.rs @@ -98,7 +98,7 @@ impl RecursiveTags { filter::dyn_run( self.query.as_deref().unwrap_or_default(), filter_context, - SequentialSource::List(ctags_cmd.tag_item_iter()?.map(|tag_item| { + SequentialSource::Iterator(ctags_cmd.tag_item_iter()?.map(|tag_item| { let item: Arc = Arc::new(tag_item); item })), diff --git a/crates/cli/src/command/filter.rs b/crates/cli/src/command/filter.rs index a6111586e..29dbee6d1 100644 --- a/crates/cli/src/command/filter.rs +++ b/crates/cli/src/command/filter.rs @@ -1,7 +1,7 @@ use crate::app::Args; use anyhow::Result; use clap::Parser; -use filter::{filter_sequential, FilterContext, ParallelSource, SequentialSource}; +use filter::{filter_sequential, FilterContext, ParallelInputSource, SequentialSource}; use maple_core::paths::AbsPathBuf; use matcher::{Bonus, FuzzyAlgorithm, MatchScope, MatcherBuilder}; use printer::Printer; @@ -101,21 +101,21 @@ impl Filter { } } - fn generate_par_source(&self) -> ParallelSource { + fn generate_parallel_input_source(&self) -> ParallelInputSource { if let Some(ref cmd_str) = self.cmd { let exec = if let Some(ref dir) = self.cmd_dir { Exec::shell(cmd_str).cwd(dir) } else { Exec::shell(cmd_str) }; - ParallelSource::Exec(Box::new(exec)) + ParallelInputSource::Exec(Box::new(exec)) } else { let file = self .input .as_ref() .map(|i| i.deref().clone()) .expect("Only File and Exec source can be parallel"); - ParallelSource::File(file) + ParallelInputSource::File(file) } } @@ -165,7 +165,7 @@ impl Filter { filter::par_dyn_run( &self.query, FilterContext::new(icon, number, winwidth, matcher_builder), - self.generate_par_source(), + self.generate_parallel_input_source(), )?; } else { filter::dyn_run::>( diff --git a/crates/cli/src/command/grep/mod.rs b/crates/cli/src/command/grep/mod.rs index 7ff8d4ccb..77775daa4 100644 --- a/crates/cli/src/command/grep/mod.rs +++ b/crates/cli/src/command/grep/mod.rs @@ -4,7 +4,7 @@ mod live_grep; use crate::app::Args; use anyhow::Result; use clap::Parser; -use filter::{ParallelSource, SequentialSource}; +use filter::{ParallelInputSource, SequentialSource}; use maple_core::tools::rg::{refresh_cache, rg_shell_command}; use matcher::MatchScope; use std::path::PathBuf; @@ -109,13 +109,13 @@ impl Grep { if self.par_run { let par_source = if let Some(cache) = maybe_usable_cache { - ParallelSource::File(cache) + ParallelInputSource::File(cache) } else if let Some(ref tempfile) = self.input { - ParallelSource::File(tempfile.clone()) + ParallelInputSource::File(tempfile.clone()) } else if let Some(ref dir) = self.cmd_dir { - ParallelSource::Exec(Box::new(Exec::shell(RG_EXEC_CMD).cwd(dir))) + ParallelInputSource::Exec(Box::new(Exec::shell(RG_EXEC_CMD).cwd(dir))) } else { - ParallelSource::Exec(Box::new(Exec::shell(RG_EXEC_CMD))) + ParallelInputSource::Exec(Box::new(Exec::shell(RG_EXEC_CMD))) }; // TODO: Improve the responsiveness of ripgrep as it can emit the items after some time. diff --git a/crates/cli/src/command/helptags.rs b/crates/cli/src/command/helptags.rs index dbc7a90f7..ea858a2bf 100644 --- a/crates/cli/src/command/helptags.rs +++ b/crates/cli/src/command/helptags.rs @@ -3,7 +3,7 @@ use clap::Parser; use maple_core::helptags::generate_tag_lines; use maple_core::paths::AbsPathBuf; use std::io::Write; -use utils::read_lines; +use utils::io::read_lines; /// Parse and display Vim helptags. #[derive(Parser, Debug, Clone)] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 88c195b79..9b5ed486d 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -10,7 +10,7 @@ use maple_core::process::ShellCommand; use printer::{println_json, println_json_with_length}; use std::path::{Path, PathBuf}; use std::process::Command as StdCommand; -use utils::{line_count, read_first_lines}; +use utils::io::{line_count, read_first_lines}; #[derive(Debug, Clone)] #[allow(unused)] diff --git a/crates/filter/src/lib.rs b/crates/filter/src/lib.rs index a7ab0b58b..8f359d8f3 100644 --- a/crates/filter/src/lib.rs +++ b/crates/filter/src/lib.rs @@ -17,8 +17,8 @@ use std::sync::Arc; use types::{ClapItem, FileNameItem, GrepItem}; pub use self::parallel_worker::{ - par_dyn_run, par_dyn_run_inprocess, par_dyn_run_list, BestItems, ParallelSource, - StdioProgressor, + par_dyn_run, par_dyn_run_inprocess, par_dyn_run_list, ParallelInputSource, StdioProgressor, + TopMatches, }; pub use self::sequential_source::{filter_sequential, SequentialSource}; pub use self::sequential_worker::dyn_run; diff --git a/crates/filter/src/parallel_worker.rs b/crates/filter/src/parallel_worker.rs index 51455fa5f..a349d4f39 100644 --- a/crates/filter/src/parallel_worker.rs +++ b/crates/filter/src/parallel_worker.rs @@ -13,9 +13,9 @@ use std::time::{Duration, Instant}; use subprocess::Exec; use types::{ClapItem, MatchedItem, Query, SearchProgressUpdate}; -/// Parallelable source. +/// Represents a source for parallel processing (e.g, file or command output). #[derive(Debug)] -pub enum ParallelSource { +pub enum ParallelInputSource { File(PathBuf), Exec(Box), } @@ -27,23 +27,23 @@ pub enum ParallelSource { pub fn par_dyn_run( query: &str, filter_context: FilterContext, - par_source: ParallelSource, + input_source: ParallelInputSource, ) -> crate::Result<()> { let query: Query = query.into(); - match par_source { - ParallelSource::File(file) => { - par_dyn_run_inner::, _>( + match input_source { + ParallelInputSource::File(file) => { + run_parallel_filter::, _>( query, filter_context, - ParSourceInner::Lines(std::fs::File::open(file)?), + ParallelSource::Lines(std::fs::File::open(file)?), )?; } - ParallelSource::Exec(exec) => { - par_dyn_run_inner::, _>( + ParallelInputSource::Exec(exec) => { + run_parallel_filter::, _>( query, filter_context, - ParSourceInner::Lines(exec.stream_stdout()?), + ParallelSource::Lines(exec.stream_stdout()?), )?; } } @@ -58,14 +58,15 @@ pub fn par_dyn_run_list<'a, 'b: 'a>( items: impl IntoParallelIterator> + 'b, ) { let query: Query = query.into(); - par_dyn_run_inner::<_, std::io::Empty>(query, filter_context, ParSourceInner::Items(items)) + run_parallel_filter::<_, std::io::Empty>(query, filter_context, ParallelSource::Items(items)) .expect("Matching items in parallel can not fail"); } +/// Manages the top N matches based on scores. #[derive(Debug)] -pub struct BestItems> { +pub struct TopMatches> { /// Time of last notification. - pub past: Instant, + pub last_update_time: Instant, /// Top N items. pub items: Vec, pub last_lines: Vec, @@ -76,7 +77,7 @@ pub struct BestItems> { pub printer: Printer, } -impl> BestItems

{ +impl> TopMatches

{ pub fn new( printer: Printer, max_capacity: usize, @@ -85,7 +86,7 @@ impl> BestItems

{ ) -> Self { Self { printer, - past: Instant::now(), + last_update_time: Instant::now(), items: Vec::with_capacity(max_capacity), last_lines: Vec::with_capacity(max_capacity), last_visible_highlights: Vec::with_capacity(max_capacity), @@ -110,12 +111,12 @@ impl> BestItems

{ self.sort(); let now = Instant::now(); - if now > self.past + self.update_interval { + if now > self.last_update_time + self.update_interval { let display_lines = self.printer.to_display_lines(self.items.clone()); self.progressor .update_all(&display_lines, total_matched, total_processed); self.last_lines = display_lines.lines; - self.past = now; + self.last_update_time = now; } } else { let last = self @@ -131,7 +132,7 @@ impl> BestItems

{ if total_matched % 16 == 0 || total_processed % 16 == 0 { let now = Instant::now(); - if now > self.past + self.update_interval { + if now > self.last_update_time + self.update_interval { let display_lines = self.printer.to_display_lines(self.items.clone()); let visible_highlights = display_lines @@ -157,7 +158,7 @@ impl> BestItems

{ self.progressor.quick_update(total_matched, total_processed) } - self.past = now; + self.last_update_time = now; } } } @@ -235,16 +236,16 @@ impl SearchProgressUpdate for StdioProgressor { } } -enum ParSourceInner>, R: Read + Send> { +enum ParallelSource>, R: Read + Send> { Items(I), Lines(R), } -/// Perform the matching on a stream of [`Source::File`] and `[Source::Exec]` in parallel. -fn par_dyn_run_inner( +/// Runs the core fuzzy matching process on a data source in parallel. +fn run_parallel_filter( query: Query, filter_context: FilterContext, - parallel_source: ParSourceInner, + parallel_source: ParallelSource, ) -> std::io::Result<()> where I: IntoParallelIterator>, @@ -266,30 +267,28 @@ where let processed_count = AtomicUsize::new(0); let printer = Printer::new(winwidth, icon); - let best_items = Mutex::new(BestItems::new( + let top_matches = Mutex::new(TopMatches::new( printer, number, StdioProgressor, Duration::from_millis(200), )); - let process_item = |item: Arc, processed: usize| { + let process_item = |item: Arc| { + let processed = processed_count.fetch_add(1, Ordering::SeqCst); if let Some(matched_item) = matcher.match_item(item) { let matched = matched_count.fetch_add(1, Ordering::SeqCst); // TODO: not use mutex? - let mut best_items = best_items.lock(); - best_items.on_new_match(matched_item, matched, processed); - drop(best_items); + let mut top_matches = top_matches.lock(); + top_matches.on_new_match(matched_item, matched, processed); + drop(top_matches); } }; match parallel_source { - ParSourceInner::Items(items) => items.into_par_iter().for_each(|item| { - let processed = processed_count.fetch_add(1, Ordering::SeqCst); - process_item(item, processed); - }), - ParSourceInner::Lines(reader) => { + ParallelSource::Items(items) => items.into_par_iter().for_each(process_item), + ParallelSource::Lines(reader) => { // To avoid Err(Custom { kind: InvalidData, error: "stream did not contain valid UTF-8" }) // The line stream can contain invalid UTF-8 data. std::io::BufReader::new(reader) @@ -297,9 +296,8 @@ where .map_while(Result::ok) .par_bridge() .for_each(|line: String| { - let processed = processed_count.fetch_add(1, Ordering::SeqCst); if let Some(item) = to_clap_item(matcher.match_scope(), line) { - process_item(item, processed); + process_item(item); } }); } @@ -308,12 +306,12 @@ where let total_matched = matched_count.into_inner(); let total_processed = processed_count.into_inner(); - let BestItems { + let TopMatches { items, progressor, printer, .. - } = best_items.into_inner(); + } = top_matches.into_inner(); let matched_items = items; @@ -328,7 +326,7 @@ where pub fn par_dyn_run_inprocess

( query: &str, filter_context: FilterContext, - par_source: ParallelSource, + input_source: ParallelInputSource, progressor: P, stop_signal: Arc, ) -> std::io::Result<()> @@ -353,7 +351,7 @@ where let processed_count = AtomicUsize::new(0); let printer = Printer::new(winwidth, icon); - let best_items = Mutex::new(BestItems::new( + let top_matches = Mutex::new(TopMatches::new( printer, number, progressor, @@ -365,15 +363,15 @@ where let matched = matched_count.fetch_add(1, Ordering::SeqCst); // TODO: not use mutex? - let mut best_items = best_items.lock(); - best_items.on_new_match(matched_item, matched, processed); - drop(best_items); + let mut top_matches = top_matches.lock(); + top_matches.on_new_match(matched_item, matched, processed); + drop(top_matches); } }; - let read: Box = match par_source { - ParallelSource::File(file) => Box::new(std::fs::File::open(file)?), - ParallelSource::Exec(exec) => Box::new( + let read: Box = match input_source { + ParallelInputSource::File(file) => Box::new(std::fs::File::open(file)?), + ParallelInputSource::Exec(exec) => Box::new( exec.detached() .stream_stdout() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?, @@ -413,12 +411,12 @@ where return Ok(()); } - let BestItems { + let TopMatches { items, progressor, printer, .. - } = best_items.into_inner(); + } = top_matches.into_inner(); let matched_items = items; diff --git a/crates/filter/src/sequential_source.rs b/crates/filter/src/sequential_source.rs index 3992d9107..6e3966d76 100644 --- a/crates/filter/src/sequential_source.rs +++ b/crates/filter/src/sequential_source.rs @@ -10,7 +10,7 @@ use types::{ClapItem, MatchedItem, SourceItem}; /// will be processed sequentially. #[derive(Debug)] pub enum SequentialSource>> { - List(I), + Iterator(I), Stdin, File(PathBuf), Exec(Box), @@ -28,12 +28,13 @@ impl>> From for SequentialSource { } } +/// Filters items from a sequential source using the given matcher. pub fn filter_sequential>>( source: SequentialSource, matcher: Matcher, ) -> crate::Result> { let clap_item_stream: Box>> = match source { - SequentialSource::List(list) => Box::new(list), + SequentialSource::Iterator(iter) => Box::new(iter), SequentialSource::Stdin => Box::new( std::io::stdin() .lock() diff --git a/crates/filter/src/sequential_worker.rs b/crates/filter/src/sequential_worker.rs index 0d13fd827..c18a26c0b 100644 --- a/crates/filter/src/sequential_worker.rs +++ b/crates/filter/src/sequential_worker.rs @@ -341,7 +341,7 @@ pub fn dyn_run>>( let matcher = matcher_builder.build(query); let clap_item_stream: Box>> = match source { - SequentialSource::List(list) => Box::new(list), + SequentialSource::Iterator(list) => Box::new(list), SequentialSource::Stdin => Box::new( std::io::stdin() .lock() @@ -421,7 +421,7 @@ mod tests { dyn_run( "abc", FilterContext::default().number(Some(100)), - SequentialSource::List( + SequentialSource::Iterator( std::iter::repeat_with(|| { bytes = bytes.reverse_bits().rotate_right(3).wrapping_add(1); diff --git a/crates/maple/build.rs b/crates/maple/build.rs index cdb3d840a..4f2093dfc 100644 --- a/crates/maple/build.rs +++ b/crates/maple/build.rs @@ -3,10 +3,10 @@ use std::{env, fs}; fn main() { built::write_built_file() - .unwrap_or_else(|e| panic!("Failed to acquire build-time information: {:?}", e)); + .unwrap_or_else(|e| panic!("Failed to acquire build-time information: {e:?}")); let outdir = env::var("OUT_DIR").unwrap(); - let outfile = format!("{}/compiled_at.txt", outdir); + let outfile = format!("{outdir}/compiled_at.txt"); let mut fh = fs::File::create(outfile).expect("Failed to create compiled_at.txt"); write!(fh, r#""{}""#, chrono::Local::now()).ok(); diff --git a/crates/maple_core/Cargo.toml b/crates/maple_core/Cargo.toml index 37ed13bd6..13ee76e47 100644 --- a/crates/maple_core/Cargo.toml +++ b/crates/maple_core/Cargo.toml @@ -15,6 +15,7 @@ chrono = { workspace = true } chrono-humanize = { workspace = true } clap = { workspace = true } colors-transform = { workspace = true } +content_inspector = { workspace = true } copypasta = { version = "0.10.0", default-features = false, features = [ "x11" ] } futures = { workspace = true } # ripgrep for global search diff --git a/crates/maple_core/src/cache/mod.rs b/crates/maple_core/src/cache/mod.rs index 923a5a9e7..1e1b607b9 100644 --- a/crates/maple_core/src/cache/mod.rs +++ b/crates/maple_core/src/cache/mod.rs @@ -106,7 +106,7 @@ impl CacheInfo { && digest.cached_path.exists() && now.signed_duration_since(digest.last_visit).num_days() < MAX_DAYS // In case the cache was not created completely. - && utils::line_count(&digest.cached_path) + && utils::io::line_count(&digest.cached_path) .map(|total| total == digest.total) .unwrap_or(false) { @@ -210,7 +210,7 @@ pub fn store_cache_digest( shell_cmd: ShellCommand, new_created_cache: PathBuf, ) -> std::io::Result { - let total = utils::line_count(&new_created_cache)?; + let total = utils::io::line_count(&new_created_cache)?; let digest = Digest::new(shell_cmd, total, new_created_cache); diff --git a/crates/maple_core/src/datastore/mod.rs b/crates/maple_core/src/datastore/mod.rs index 2ec7d1266..f20610962 100644 --- a/crates/maple_core/src/datastore/mod.rs +++ b/crates/maple_core/src/datastore/mod.rs @@ -106,7 +106,7 @@ fn load_json>(path: Option

) -> Option fn write_json>(obj: T, path: Option

) -> std::io::Result<()> { if let Some(json_path) = path.as_ref() { - utils::create_or_overwrite(json_path, serde_json::to_string(&obj)?.as_bytes())?; + utils::io::create_or_overwrite(json_path, serde_json::to_string(&obj)?.as_bytes())?; } Ok(()) diff --git a/crates/maple_core/src/helptags.rs b/crates/maple_core/src/helptags.rs index bade2f2e2..808456531 100644 --- a/crates/maple_core/src/helptags.rs +++ b/crates/maple_core/src/helptags.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use utils::read_lines; +use utils::io::read_lines; #[inline] fn strip_trailing_slash(x: &str) -> &str { diff --git a/crates/maple_core/src/previewer/mod.rs b/crates/maple_core/src/previewer/mod.rs index 63543dd1f..c95163606 100644 --- a/crates/maple_core/src/previewer/mod.rs +++ b/crates/maple_core/src/previewer/mod.rs @@ -1,105 +1,6 @@ +pub mod text_file; pub mod vim_help; -use paths::truncate_absolute_path; -use std::fs::File; -use std::io::Read; -use std::path::Path; -use utils::bytelines::ByteLines; -use utils::read_first_lines; - -/// Preview of a file. -#[derive(Clone, Debug)] -pub struct FilePreview { - /// Line number of source file at which the preview starts (exclusive). - pub start: usize, - /// Line number of source file at which the preview ends (inclusive). - pub end: usize, - /// Total lines in the source file. - pub total: usize, - /// 0-based line number of the line that should be highlighted in the preview window. - pub highlight_lnum: usize, - /// [start, end] of the source file. - pub lines: Vec, -} - -/// Returns the lines that can fit into the preview window given its window height. -/// -/// Center the line at `target_line_number` in the preview window if possible. -/// (`target_line` - `size`, `target_line` - `size`). -pub fn get_file_preview>( - path: P, - target_line_number: usize, - winheight: usize, -) -> std::io::Result { - let mid = winheight / 2; - let (start, end, highlight_lnum) = if target_line_number > mid { - (target_line_number - mid, target_line_number + mid, mid) - } else { - (0, winheight, target_line_number) - }; - - let total = utils::line_count(path.as_ref())?; - - let lines = read_preview_lines(path, start, end)?; - let end = end.min(total); - - Ok(FilePreview { - start, - end, - total, - highlight_lnum, - lines, - }) -} - -fn read_preview_lines>( - path: P, - start: usize, - end: usize, -) -> std::io::Result> { - let mut filebuf: Vec = Vec::new(); - - File::open(path) - .and_then(|mut file| { - // XXX: is megabyte enough for any text file? - const MEGABYTE: usize = 32 * 1_048_576; - - let filesize = utils::file_size(&file); - if filesize > MEGABYTE { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "maximum preview file buffer size reached", - )); - } - - filebuf.reserve_exact(filesize); - file.read_to_end(&mut filebuf) - }) - .map(|_| { - ByteLines::new(&filebuf) - .skip(start) - .take(end - start) - // trim_end() to get rid of ^M on Windows. - .map(|l| l.trim_end().to_string()) - .collect() - }) -} - -#[inline] -fn as_absolute_path>(path: P) -> std::io::Result { - if path.as_ref().is_absolute() { - Ok(path.as_ref().to_string_lossy().into()) - } else { - // Somehow the absolute path on Windows is problematic using `canonicalize`: - // C:\Users\liuchengxu\AppData\Local\nvim\init.vim - // \\?\C:\Users\liuchengxu\AppData\Local\nvim\init.vim - Ok(std::fs::canonicalize(path.as_ref())? - .into_os_string() - .to_string_lossy() - .into()) - } -} - /// Truncates the lines that are awfully long as vim can not handle them properly. /// /// Ref https://github.com/liuchengxu/vim-clap/issues/543 @@ -122,89 +23,3 @@ pub fn truncate_lines( } }) } - -pub fn preview_file>( - path: P, - size: usize, - max_width: usize, -) -> std::io::Result<(Vec, String)> { - if !path.as_ref().is_file() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Can not preview if the object is not a file", - )); - } - let abs_path = as_absolute_path(path.as_ref())?; - let lines_iter = read_first_lines(path.as_ref(), size)?; - let lines = std::iter::once(abs_path.clone()) - .chain(truncate_lines(lines_iter, max_width)) - .collect::>(); - - Ok((lines, abs_path)) -} - -pub fn preview_file_with_truncated_title>( - path: P, - size: usize, - max_line_width: usize, - max_title_width: usize, -) -> std::io::Result<(Vec, String)> { - let abs_path = as_absolute_path(path.as_ref())?; - let truncated_abs_path = truncate_absolute_path(&abs_path, max_title_width).into_owned(); - let lines_iter = read_first_lines(path.as_ref(), size)?; - let lines = std::iter::once(truncated_abs_path.clone()) - .chain(truncate_lines(lines_iter, max_line_width)) - .collect::>(); - - Ok((lines, truncated_abs_path)) -} - -pub fn preview_file_at>( - path: P, - winheight: usize, - max_width: usize, - lnum: usize, -) -> std::io::Result<(Vec, usize)> { - tracing::debug!(path = %path.as_ref().display(), lnum, "Previewing file"); - - let FilePreview { - lines, - highlight_lnum, - .. - } = get_file_preview(path.as_ref(), lnum, winheight)?; - - let lines = std::iter::once(format!("{}:{lnum}", path.as_ref().display())) - .chain(truncate_lines(lines.into_iter(), max_width)) - .collect::>(); - - Ok((lines, highlight_lnum)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_file_preview_contains_multi_byte() { - let test_txt = std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .join("test") - .join("testdata") - .join("test_673.txt"); - let FilePreview { lines, .. } = get_file_preview(test_txt, 2, 10).unwrap(); - assert_eq!( - lines, - [ - "test_ddd", - "test_ddd //1����ˤ��ϡ�����1", - "test_ddd //2����ˤ��ϡ�����2", - "test_ddd //3����ˤ��ϡ�����3", - "test_ddd //hello" - ] - ); - } -} diff --git a/crates/maple_core/src/previewer/text_file.rs b/crates/maple_core/src/previewer/text_file.rs new file mode 100644 index 000000000..47121ba5b --- /dev/null +++ b/crates/maple_core/src/previewer/text_file.rs @@ -0,0 +1,213 @@ +use super::truncate_lines; +use paths::truncate_absolute_path; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use utils::bytelines::ByteLines; +use utils::io::{read_first_lines, FileSizeTier}; + +/// Preview of a text file. +#[derive(Clone, Debug)] +pub struct TextPreview { + /// Line number of source file at which the preview starts (exclusive). + pub start: usize, + /// Line number of source file at which the preview ends (inclusive). + pub end: usize, + /// Total lines in the source file. + pub total: usize, + /// 0-based line number of the line that should be highlighted in the preview window. + pub highlight_lnum: usize, + /// [start, end] of the source file. + pub lines: Vec, +} + +/// Returns the lines that can fit into the preview window given its window height. +/// +/// Center the line at `target_line_number` in the preview window if possible. +/// (`target_line` - `size`, `target_line` - `size`). +pub fn generate_text_preview>( + path: P, + target_line_number: usize, + winheight: usize, +) -> std::io::Result { + let mid = winheight / 2; + let (start, end, highlight_lnum) = if target_line_number > mid { + (target_line_number - mid, target_line_number + mid, mid) + } else { + (0, winheight, target_line_number) + }; + + let total = utils::io::line_count(path.as_ref())?; + + let lines = read_lines_in_range(path, start, end)?; + + Ok(TextPreview { + start, + end: end.min(total), + total, + highlight_lnum, + lines, + }) +} + +fn read_lines_in_range>( + path: P, + start: usize, + end: usize, +) -> std::io::Result> { + let mut filebuf: Vec = Vec::new(); + + File::open(path) + .and_then(|mut file| { + // XXX: is megabyte enough for any text file? + const MEGABYTE: usize = 32 * 1_048_576; + + let filesize = utils::io::file_size(&file); + if filesize > MEGABYTE { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "maximum preview file buffer size reached", + )); + } + + filebuf.reserve_exact(filesize); + file.read_to_end(&mut filebuf) + }) + .map(|_| { + ByteLines::new(&filebuf) + .skip(start) + .take(end - start) + // trim_end() to get rid of ^M on Windows. + .map(|l| l.trim_end().to_string()) + .collect() + }) +} + +#[inline] +fn as_absolute_path>(path: P) -> std::io::Result { + if path.as_ref().is_absolute() { + Ok(path.as_ref().to_string_lossy().into()) + } else { + // Somehow the absolute path on Windows is problematic using `canonicalize`: + // C:\Users\liuchengxu\AppData\Local\nvim\init.vim + // \\?\C:\Users\liuchengxu\AppData\Local\nvim\init.vim + Ok(std::fs::canonicalize(path.as_ref())? + .into_os_string() + .to_string_lossy() + .into()) + } +} + +fn generate_preview_lines( + path: impl AsRef, + title_line: String, + max_line_width: usize, + size: usize, + file_size_tier: FileSizeTier, +) -> std::io::Result> { + let lines = match file_size_tier { + FileSizeTier::Empty | FileSizeTier::Small => { + let lines_iter = read_first_lines(path.as_ref(), size)?; + std::iter::once(title_line) + .chain(truncate_lines(lines_iter, max_line_width)) + .collect::>() + } + FileSizeTier::Medium => { + let lines = utils::io::read_lines_from_medium(path.as_ref(), 0, size)?; + std::iter::once(title_line) + .chain(truncate_lines(lines.into_iter(), max_line_width)) + .collect::>() + } + FileSizeTier::Large(size) => { + let size_in_gib = size as f64 / (1024.0 * 1024.0 * 1024.0); + vec![ + title_line, + format!("File too large to preview (size: {size_in_gib:.2} GiB)."), + ] + } + }; + + Ok(lines) +} + +pub struct TextLines { + // Preview lines to display. + pub lines: Vec, + // Path to display, potentially truncated. + pub display_path: String, +} + +pub fn preview_file>( + path: P, + size: usize, + max_line_width: usize, + max_title_width: Option, + file_size_tier: FileSizeTier, +) -> std::io::Result { + if !path.as_ref().is_file() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Can not preview if the object is not a file", + )); + } + + let abs_path = as_absolute_path(path.as_ref())?; + + let abs_path = if let Some(max_title_width) = max_title_width { + truncate_absolute_path(&abs_path, max_title_width).into_owned() + } else { + abs_path + }; + + let lines = + generate_preview_lines(path, abs_path.clone(), max_line_width, size, file_size_tier)?; + + Ok(TextLines { + lines, + display_path: abs_path, + }) +} + +pub fn preview_file_at>( + path: P, + winheight: usize, + max_width: usize, + lnum: usize, +) -> std::io::Result<(Vec, usize)> { + tracing::debug!(path = %path.as_ref().display(), "Generating preview at line {lnum}"); + + let TextPreview { + lines, + highlight_lnum, + .. + } = generate_text_preview(path.as_ref(), lnum, winheight)?; + + let lines = std::iter::once(format!("{}:{lnum}", path.as_ref().display())) + .chain(truncate_lines(lines.into_iter(), max_width)) + .collect::>(); + + Ok((lines, highlight_lnum)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_file_preview_contains_multi_byte() { + let current_dir = std::env::current_dir().unwrap(); + let root_dir = current_dir.parent().unwrap().parent().unwrap(); + let test_txt = root_dir.join("test").join("testdata").join("test_673.txt"); + let TextPreview { lines, .. } = generate_text_preview(test_txt, 2, 10).unwrap(); + assert_eq!( + lines, + [ + "test_ddd", + "test_ddd //1����ˤ��ϡ�����1", + "test_ddd //2����ˤ��ϡ�����2", + "test_ddd //3����ˤ��ϡ�����3", + "test_ddd //hello" + ] + ); + } +} diff --git a/crates/maple_core/src/previewer/vim_help.rs b/crates/maple_core/src/previewer/vim_help.rs index 05145e233..58e11faf4 100644 --- a/crates/maple_core/src/previewer/vim_help.rs +++ b/crates/maple_core/src/previewer/vim_help.rs @@ -1,5 +1,5 @@ use std::path::Path; -use utils::{read_lines, read_lines_from}; +use utils::io::{read_lines, read_lines_from_small}; #[derive(Debug, Clone)] pub struct HelpTagPreview<'a> { @@ -39,7 +39,7 @@ impl<'a> HelpTagPreview<'a> { let p = Path::new(r).join("doc").join(self.doc_filename); if p.exists() { if let Some(line_number) = find_tag_line(&p, &target_tag) { - if let Ok(lines_iter) = read_lines_from(&p, line_number, size) { + if let Ok(lines_iter) = read_lines_from_small(&p, line_number, size) { return Some((format!("{}", p.display()), lines_iter.collect())); } } diff --git a/crates/maple_core/src/process/mod.rs b/crates/maple_core/src/process/mod.rs index b7fe23d29..fd26e9d53 100644 --- a/crates/maple_core/src/process/mod.rs +++ b/crates/maple_core/src/process/mod.rs @@ -113,7 +113,7 @@ impl ShellCommand { } pub fn cache_file_path(&self) -> std::io::Result { - let cached_filename = utils::calculate_hash(self); + let cached_filename = utils::compute_hash(self); generate_cache_file_path(cached_filename.to_string()) } @@ -122,7 +122,7 @@ impl ShellCommand { pub fn write_cache(self, total: usize, cmd_stdout: &[u8]) -> std::io::Result { use std::io::Write; - let cache_filename = utils::calculate_hash(&self); + let cache_filename = utils::compute_hash(&self); let cache_file = generate_cache_file_path(cache_filename.to_string())?; std::fs::File::create(&cache_file)?.write_all(cmd_stdout)?; diff --git a/crates/maple_core/src/searcher/file.rs b/crates/maple_core/src/searcher/file.rs index 458ca243e..e6b9ed2d0 100644 --- a/crates/maple_core/src/searcher/file.rs +++ b/crates/maple_core/src/searcher/file.rs @@ -1,6 +1,6 @@ use crate::searcher::SearchContext; use crate::stdio_server::SearchProgressor; -use filter::BestItems; +use filter::TopMatches; use matcher::{MatchResult, Matcher}; use printer::Printer; use std::borrow::Cow; @@ -99,7 +99,7 @@ pub async fn search( let number = item_pool_size; let progressor = SearchProgressor::new(vim, stop_signal.clone()); - let mut best_items = BestItems::new(printer, number, progressor, Duration::from_millis(200)); + let mut top_matches = TopMatches::new(printer, number, progressor, Duration::from_millis(200)); let (sender, mut receiver) = unbounded_channel(); @@ -126,7 +126,7 @@ pub async fn search( } total_matched += 1; let total_processed = total_processed.load(Ordering::Relaxed); - best_items.on_new_match(matched_item, total_matched, total_processed); + top_matches.on_new_match(matched_item, total_matched, total_processed); } let elapsed = now.elapsed().as_millis(); @@ -136,12 +136,12 @@ pub async fn search( return; } - let BestItems { + let TopMatches { items, progressor, printer, .. - } = best_items; + } = top_matches; let display_lines = printer.to_display_lines(items); let total_processed = total_processed.load(Ordering::SeqCst); diff --git a/crates/maple_core/src/searcher/files.rs b/crates/maple_core/src/searcher/files.rs index 8422ef859..5864256b7 100644 --- a/crates/maple_core/src/searcher/files.rs +++ b/crates/maple_core/src/searcher/files.rs @@ -1,7 +1,7 @@ use super::{walk_parallel, WalkConfig}; use crate::searcher::SearchContext; use crate::stdio_server::SearchProgressor; -use filter::{BestItems, MatchedItem}; +use filter::{MatchedItem, TopMatches}; use ignore::{DirEntry, WalkState}; use matcher::Matcher; use printer::Printer; @@ -104,7 +104,7 @@ pub async fn search(query: String, hidden: bool, matcher: Matcher, search_contex let mut total_matched = 0usize; let printer = Printer::new(line_width, icon); - let mut best_items = BestItems::new(printer, number, progressor, Duration::from_millis(200)); + let mut top_matches = TopMatches::new(printer, number, progressor, Duration::from_millis(200)); let now = std::time::Instant::now(); @@ -114,7 +114,7 @@ pub async fn search(query: String, hidden: bool, matcher: Matcher, search_contex } total_matched += 1; let total_processed = total_processed.load(Ordering::Relaxed); - best_items.on_new_match(matched_item, total_matched, total_processed); + top_matches.on_new_match(matched_item, total_matched, total_processed); } if stop_signal.load(Ordering::SeqCst) { @@ -123,12 +123,12 @@ pub async fn search(query: String, hidden: bool, matcher: Matcher, search_contex let elapsed = now.elapsed().as_millis(); - let BestItems { + let TopMatches { items, progressor, printer, .. - } = best_items; + } = top_matches; let display_lines = printer.to_display_lines(items); let total_processed = total_processed.load(Ordering::SeqCst); diff --git a/crates/maple_core/src/searcher/tagfiles.rs b/crates/maple_core/src/searcher/tagfiles.rs index 907a0cb24..685f9b667 100644 --- a/crates/maple_core/src/searcher/tagfiles.rs +++ b/crates/maple_core/src/searcher/tagfiles.rs @@ -1,7 +1,7 @@ use crate::searcher::SearchContext; use crate::stdio_server::SearchProgressor; use dirs::Dirs; -use filter::BestItems; +use filter::TopMatches; use matcher::Matcher; use printer::Printer; use std::borrow::Cow; @@ -306,7 +306,7 @@ pub async fn search(query: String, cwd: PathBuf, matcher: Matcher, search_contex let number = item_pool_size; let progressor = SearchProgressor::new(vim, stop_signal.clone()); - let mut best_items = BestItems::new(printer, number, progressor, Duration::from_millis(200)); + let mut top_matches = TopMatches::new(printer, number, progressor, Duration::from_millis(200)); let (sender, mut receiver) = unbounded_channel(); @@ -345,7 +345,7 @@ pub async fn search(query: String, cwd: PathBuf, matcher: Matcher, search_contex } total_matched += 1; let total_processed = total_processed.load(Ordering::Relaxed); - best_items.on_new_match(matched_item, total_matched, total_processed); + top_matches.on_new_match(matched_item, total_matched, total_processed); } if stop_signal.load(Ordering::SeqCst) { @@ -354,12 +354,12 @@ pub async fn search(query: String, cwd: PathBuf, matcher: Matcher, search_contex let elapsed = now.elapsed().as_millis(); - let BestItems { + let TopMatches { items, progressor, printer, .. - } = best_items; + } = top_matches; let display_lines = printer.to_display_lines(items); let total_processed = total_processed.load(Ordering::SeqCst); diff --git a/crates/maple_core/src/stdio_server/diagnostics_worker.rs b/crates/maple_core/src/stdio_server/diagnostics_worker.rs index 5ba024828..87c43ced7 100644 --- a/crates/maple_core/src/stdio_server/diagnostics_worker.rs +++ b/crates/maple_core/src/stdio_server/diagnostics_worker.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; #[derive(Default, Serialize)] -struct Stats { +struct DiagnosticStats { error: usize, warn: usize, hint: usize, @@ -19,12 +19,11 @@ struct Stats { #[derive(Debug, Clone)] struct BufferDiagnostics { - /// This flag indicates whether the received results are - /// the first received ones, for having multiple diagnostics - /// sources is very likely, - first_result_arrived: Arc, + /// Indicates whether the first diagnostics result has been received, for it's possibele to + /// have multiple diagnostic sources for a single buffer. + first_result_received: Arc, - /// List of sorted diagnostics. + /// Sorted list of diagnostics. inner: Arc>>, } @@ -41,26 +40,25 @@ impl BufferDiagnostics { /// Constructs a new instance of [`BufferDiagnostics`]. fn new() -> Self { Self { - first_result_arrived: Arc::new(false.into()), + first_result_received: Arc::new(false.into()), inner: Arc::new(RwLock::new(Vec::new())), } } - /// Append new diagnostics and returns the count of latest diagnostics. - fn append(&self, new_diagnostics: Vec) -> Stats { + /// Appends new diagnostics and returns the updated statistics. + fn add_diagnostics(&self, new_diagnostics: Vec) -> DiagnosticStats { let mut diagnostics = self.inner.write(); diagnostics.extend(new_diagnostics); diagnostics.sort_by(|a, b| a.spans[0].line_start.cmp(&b.spans[0].line_start)); - let mut stats = Stats::default(); + let mut stats = DiagnosticStats::default(); for d in diagnostics.iter() { - if d.is_error() { - stats.error += 1; - } else if d.is_warn() { - stats.warn += 1; - } else if d.is_hint() { - stats.hint += 1; + match d.severity { + Severity::Error => stats.error += 1, + Severity::Warning => stats.warn += 1, + Severity::Hint => stats.hint += 1, + _ => {} } } @@ -69,40 +67,30 @@ impl BufferDiagnostics { /// Clear the diagnostics list. fn reset(&self) { - self.first_result_arrived.store(false, Ordering::SeqCst); + self.first_result_received.store(false, Ordering::SeqCst); let mut diagnostics = self.inner.write(); diagnostics.clear(); } - /// Returns a tuple of (line_number, column_start) of the sibling diagnostic. - fn find_sibling_position( + /// Finds the position (line_number, column_start) of a sibling diagnostic based on direction and kind. + fn find_sibling_diagnostic_position( &self, from_line_number: usize, kind: DiagnosticKind, direction: Direction, ) -> Option<(usize, usize)> { - use CmpOrdering::{Greater, Less}; - use DiagnosticKind::{All, Error, Hint, Warn}; - use Direction::{First, Last, Next, Prev}; - let diagnostics = self.inner.read(); - let errors = || { - diagnostics - .iter() - .filter_map(|d| if d.is_error() { d.spans.first() } else { None }) - }; - - let warnings = || { - diagnostics - .iter() - .filter_map(|d| if d.is_warn() { d.spans.first() } else { None }) - }; - - let hints = || { + let relevant_spans = || { diagnostics .iter() - .filter_map(|d| if d.is_hint() { d.spans.first() } else { None }) + .filter(|d| match kind { + DiagnosticKind::All => true, + DiagnosticKind::Error => d.is_error(), + DiagnosticKind::Warn => d.is_warn(), + DiagnosticKind::Hint => d.is_hint(), + }) + .filter_map(|d| d.spans.first()) }; let check_span = |span: &DiagnosticSpan, ordering: CmpOrdering| { @@ -113,43 +101,32 @@ impl BufferDiagnostics { } }; - let spans = || diagnostics.iter().filter_map(|d| d.spans.first()); - - match (kind, direction) { - (All, First) => spans().next().map(|span| span.start_pos()), - (All, Last) => spans().last().map(|span| span.start_pos()), - (All, Next) => spans().find_map(|span| check_span(span, Greater)), - (All, Prev) => spans().rev().find_map(|span| check_span(span, Less)), - (Error, First) => errors().next().map(|span| span.start_pos()), - (Error, Last) => errors().last().map(|span| span.start_pos()), - (Error, Next) => errors().find_map(|span| check_span(span, Greater)), - (Error, Prev) => errors().rev().find_map(|span| check_span(span, Less)), - (Warn, First) => warnings().next().map(|span| span.start_pos()), - (Warn, Last) => warnings().last().map(|span| span.start_pos()), - (Warn, Next) => warnings().find_map(|span| check_span(span, Greater)), - (Warn, Prev) => warnings().rev().find_map(|span| check_span(span, Less)), - (Hint, First) => hints().next().map(|span| span.start_pos()), - (Hint, Last) => hints().last().map(|span| span.start_pos()), - (Hint, Next) => hints().find_map(|span| check_span(span, Greater)), - (Hint, Prev) => hints().rev().find_map(|span| check_span(span, Less)), + match direction { + Direction::First => relevant_spans().next().map(|span| span.start_pos()), + Direction::Last => relevant_spans().last().map(|span| span.start_pos()), + Direction::Next => { + relevant_spans().find_map(|span| check_span(span, CmpOrdering::Greater)) + } + Direction::Prev => relevant_spans() + .rev() + .find_map(|span| check_span(span, CmpOrdering::Less)), } } - async fn display_diagnostics_under_cursor(&self, vim: &Vim) -> VimResult<()> { + async fn show_diagnostics_under_cursor(&self, vim: &Vim) -> VimResult<()> { let lnum = vim.line(".").await?; let col = vim.col(".").await?; let diagnostics = self.inner.read(); - - let current_diagnostics = diagnostics + let line_diagnostics = diagnostics .iter() .filter(|d| d.spans.iter().any(|span| span.line_start == lnum)) .collect::>(); - if current_diagnostics.is_empty() { + if line_diagnostics.is_empty() { vim.bare_exec("clap#plugin#diagnostics#close_top_right")?; } else { - let diagnostic_at_cursor = current_diagnostics + let cursor_diagnostics = line_diagnostics .iter() .filter(|d| { d.spans @@ -160,15 +137,15 @@ impl BufferDiagnostics { // Display the specific diagnostic if the cursor is on it, // otherwise display all the diagnostics in this line. - if diagnostic_at_cursor.is_empty() { + if cursor_diagnostics.is_empty() { vim.exec( "clap#plugin#diagnostics#display_at_top_right", - [current_diagnostics], + [line_diagnostics], )?; } else { vim.exec( "clap#plugin#diagnostics#display_at_top_right", - [diagnostic_at_cursor], + [cursor_diagnostics], )?; } } @@ -187,16 +164,16 @@ fn update_buffer_diagnostics( new_diagnostics.dedup(); let is_first_result = buffer_diagnostics - .first_result_arrived + .first_result_received .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_ok(); - tracing::info!( + tracing::trace!( ?bufnr, ?is_first_result, "[update_buffer_diagnostics] buffer_diagnostics: {buffer_diagnostics:?}" ); - tracing::info!( + tracing::trace!( ?bufnr, "[update_buffer_diagnostics] new_diagnostics: {new_diagnostics:?}" ); @@ -207,7 +184,7 @@ fn update_buffer_diagnostics( (bufnr, &new_diagnostics), ); - buffer_diagnostics.append(new_diagnostics) + buffer_diagnostics.add_diagnostics(new_diagnostics) } else { // Remove the potential duplicated results from multiple diagnostic reporters. let existing = buffer_diagnostics.inner.read(); @@ -229,7 +206,7 @@ fn update_buffer_diagnostics( ); } - buffer_diagnostics.append(followup_diagnostics) + buffer_diagnostics.add_diagnostics(followup_diagnostics) }; let _ = vim.setbufvar(bufnr, "clap_diagnostics", new_stats); @@ -239,9 +216,7 @@ fn update_buffer_diagnostics( let vim = vim.clone(); async move { - let _ = buffer_diagnostics - .display_diagnostics_under_cursor(&vim) - .await; + let _ = buffer_diagnostics.show_diagnostics_under_cursor(&vim).await; } }); @@ -345,7 +320,7 @@ impl BufferDiagnosticsWorker { if let Some(diagnostics) = self.buffer_diagnostics.get(&bufnr) { let lnum = self.vim.line(".").await?; if let Some((lnum, col)) = - diagnostics.find_sibling_position(lnum, kind, direction) + diagnostics.find_sibling_diagnostic_position(lnum, kind, direction) { self.vim.exec("cursor", [lnum, col])?; self.vim.exec("execute", "normal! zz")?; @@ -354,9 +329,7 @@ impl BufferDiagnosticsWorker { } WorkerMessage::ShowDiagnosticsUnderCursorInFloatWin(bufnr) => { if let Some(diagnostics) = self.buffer_diagnostics.get(&bufnr) { - diagnostics - .display_diagnostics_under_cursor(&self.vim) - .await?; + diagnostics.show_diagnostics_under_cursor(&self.vim).await?; } } WorkerMessage::ResetBufferDiagnostics(bufnr) => { @@ -365,7 +338,7 @@ impl BufferDiagnosticsWorker { .and_modify(|v| v.reset()) .or_insert_with(BufferDiagnostics::new); self.vim - .setbufvar(bufnr, "clap_diagnostics", Stats::default())?; + .setbufvar(bufnr, "clap_diagnostics", DiagnosticStats::default())?; self.vim .exec("clap#plugin#diagnostics#toggle_off", [bufnr])?; } @@ -411,7 +384,8 @@ impl BufferDiagnosticsWorker { } } -pub fn start_buffer_diagnostics_worker(vim: Vim) -> UnboundedSender { +/// Initialize the worker for handling buffer diagnostics. +pub fn initialize_diagnostics_worker(vim: Vim) -> UnboundedSender { let (worker_msg_sender, worker_msg_receiver) = unbounded_channel(); tokio::spawn(async move { diff --git a/crates/maple_core/src/stdio_server/mod.rs b/crates/maple_core/src/stdio_server/mod.rs index 84edd32e7..1e9be0950 100644 --- a/crates/maple_core/src/stdio_server/mod.rs +++ b/crates/maple_core/src/stdio_server/mod.rs @@ -89,7 +89,7 @@ struct InitializedService { /// Create a new service, with plugins registered from the config file. fn initialize_service(vim: Vim) -> InitializedService { - use self::diagnostics_worker::start_buffer_diagnostics_worker; + use self::diagnostics_worker::initialize_diagnostics_worker; use self::plugin::{ ActionType, ClapPlugin, ColorizerPlugin, CtagsPlugin, DiagnosticsPlugin, GitPlugin, LinterPlugin, LspPlugin, MarkdownPlugin, SyntaxPlugin, SystemPlugin, WordHighlighterPlugin, @@ -118,7 +118,7 @@ fn initialize_service(vim: Vim) -> InitializedService { let plugin_config = &maple_config::config().plugin; if plugin_config.lsp.enable || plugin_config.linter.enable { - let diagnostics_worker_msg_sender = start_buffer_diagnostics_worker(vim.clone()); + let diagnostics_worker_msg_sender = initialize_diagnostics_worker(vim.clone()); register_plugin( Box::new(DiagnosticsPlugin::new( diff --git a/crates/maple_core/src/stdio_server/plugin/colorizer.rs b/crates/maple_core/src/stdio_server/plugin/colorizer.rs index 2d2e466e5..e144c6d76 100644 --- a/crates/maple_core/src/stdio_server/plugin/colorizer.rs +++ b/crates/maple_core/src/stdio_server/plugin/colorizer.rs @@ -105,7 +105,7 @@ fn find_colors(input_file: impl AsRef) -> std::io::Result { diff --git a/crates/maple_core/src/stdio_server/plugin/linter.rs b/crates/maple_core/src/stdio_server/plugin/linter.rs index 37c84ac43..d4f3ec3b2 100644 --- a/crates/maple_core/src/stdio_server/plugin/linter.rs +++ b/crates/maple_core/src/stdio_server/plugin/linter.rs @@ -53,7 +53,7 @@ impl Linter { let source_file = self.vim.bufabspath(bufnr).await?; let source_file = PathBuf::from(source_file); - let filetype = self.vim.getbufvar::(bufnr, "&filetype").await?; + let filetype = self.vim.filetype(bufnr).await?; let Some(workspace) = code_tools::linting::find_workspace(&filetype, &source_file) else { return Ok(()); @@ -105,7 +105,7 @@ impl Linter { let source_file = self.vim.bufabspath(bufnr).await?; let source_file = PathBuf::from(source_file); - let filetype = self.vim.getbufvar::(bufnr, "&filetype").await?; + let filetype = self.vim.filetype(bufnr).await?; let Some(workspace) = code_tools::linting::find_workspace(&filetype, &source_file) else { return Ok(()); diff --git a/crates/maple_core/src/stdio_server/plugin/lsp.rs b/crates/maple_core/src/stdio_server/plugin/lsp.rs index 6798d23b9..562f5fec1 100644 --- a/crates/maple_core/src/stdio_server/plugin/lsp.rs +++ b/crates/maple_core/src/stdio_server/plugin/lsp.rs @@ -230,7 +230,7 @@ impl LspPlugin { return Ok(()); } - let filetype = self.vim.getbufvar::(bufnr, "&filetype").await?; + let filetype = self.vim.filetype(bufnr).await?; if filetype.is_empty() || self.filetype_blocklist.contains(&filetype) @@ -468,12 +468,12 @@ impl LspPlugin { let maybe_character_index = if lines.is_empty() { // Buffer may not be loaded, read the local file directly. - let Some(line) = utils::read_line_at(filepath, line)? else { + let Some(line) = utils::io::read_line_at(filepath, line)? else { return Ok(None); }; - utils::char_index_for(&line, col - 1) + utils::char_index_at_byte(&line, col - 1) } else { - utils::char_index_for(&lines[0], col - 1) + utils::char_index_at_byte(&lines[0], col - 1) }; let Some(character) = maybe_character_index else { @@ -611,7 +611,7 @@ impl LspPlugin { let path = loc.uri.path(); let row = loc.range.start.line + 1; let column = loc.range.start.character + 1; - let text = utils::read_line_at(path, row as usize) + let text = utils::io::read_line_at(path, row as usize) .ok() .flatten() .unwrap_or_default(); diff --git a/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs b/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs index 55361e0b8..f3c8a6fde 100644 --- a/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs +++ b/crates/maple_core/src/stdio_server/plugin/lsp/handler.rs @@ -101,39 +101,38 @@ impl LanguageServerMessageHandler { } }; - let token_d: &dyn std::fmt::Display = match &token { - NumberOrString::Number(n) => n, - NumberOrString::String(s) => s, - }; - - let status = match parts { - (Some(title), Some(message), Some(percentage)) => { - format!("[{}] {}% {} - {}", token_d, percentage, title, message) - } - (Some(title), None, Some(percentage)) => { - format!("[{}] {}% {}", token_d, percentage, title) - } - (Some(title), Some(message), None) => { - format!("[{}] {} - {}", token_d, title, message) - } - (None, Some(message), Some(percentage)) => { - format!("[{}] {}% {}", token_d, percentage, message) - } - (Some(title), None, None) => { - format!("[{}] {}", token_d, title) - } - (None, Some(message), None) => { - format!("[{}] {}", token_d, message) - } - (None, None, Some(percentage)) => { - format!("[{}] {}%", token_d, percentage) - } - (None, None, None) => format!("[{}]", token_d), - }; - if let WorkDoneProgress::End(_) = work { let _ = self.vim.update_lsp_status(&self.server_name); } else { + let token_d: &dyn std::fmt::Display = match &token { + NumberOrString::Number(n) => n, + NumberOrString::String(s) => s, + }; + + let status = match parts { + (Some(title), Some(message), Some(percentage)) => { + format!("[{token_d}] {percentage}% {title} - {message}") + } + (Some(title), Some(message), None) => { + format!("[{token_d}] {title} - {message}") + } + (Some(title), None, Some(percentage)) => { + format!("[{token_d}] {percentage}% {title}") + } + (Some(title), None, None) => { + format!("[{token_d}] {title}") + } + (None, Some(message), Some(percentage)) => { + format!("[{token_d}] {percentage}% {message}") + } + (None, Some(message), None) => { + format!("[{token_d}] {message}") + } + (None, None, Some(percentage)) => { + format!("[{token_d}] {percentage}%") + } + (None, None, None) => format!("[{token_d}]"), + }; self.update_lsp_status_gentlely(Some(status)); } diff --git a/crates/maple_core/src/stdio_server/plugin/syntax.rs b/crates/maple_core/src/stdio_server/plugin/syntax.rs index 4119315d9..ec96e04ae 100644 --- a/crates/maple_core/src/stdio_server/plugin/syntax.rs +++ b/crates/maple_core/src/stdio_server/plugin/syntax.rs @@ -23,9 +23,9 @@ type RawTsHighlights = BTreeMap>; /// Represents the tree-sitter highlight info of entire buffer. #[derive(Debug, Clone)] -struct BufferHighlights(RawTsHighlights); +struct BufferHighlightData(RawTsHighlights); -impl BufferHighlights { +impl BufferHighlightData { fn syntax_props_at( &self, language: Language, @@ -50,15 +50,16 @@ impl BufferHighlights { } } -impl From for BufferHighlights { +impl From for BufferHighlightData { fn from(inner: RawTsHighlights) -> Self { Self(inner) } } -/// (start, length, highlight_group) -type LineHighlights = Vec<(usize, usize, &'static str)>; -type VimHighlights = Vec<(usize, LineHighlights)>; +/// (column_start, length, highlight_group) +type LineHighlightData = Vec<(usize, usize, &'static str)>; +/// (line_number, line_highlight_data) +type VimHighlightData = Vec<(usize, LineHighlightData)>; #[derive(Debug, Clone)] struct TreeSitterInfo { @@ -66,10 +67,10 @@ struct TreeSitterInfo { /// Used to infer the highlighting render strategy. file_size: FileSize, /// Highlights of entire buffer. - highlights: BufferHighlights, + highlights: BufferHighlightData, /// Current vim highlighting info, note that we only /// highlight the visual lines on the vim side. - vim_highlights: VimHighlights, + vim_highlights: VimHighlightData, } /// File size in bytes. @@ -79,11 +80,11 @@ struct FileSize(usize); impl std::fmt::Display for FileSize { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.0 < 1024 { - write!(f, "{}bytes", self.0) + write!(f, "{} bytes", self.0) } else if self.0 < 1024 * 1024 { - write!(f, "{}KiB", self.0 / 1024) + write!(f, "{} KiB", self.0 / 1024) } else { - write!(f, "{}MiB", self.0 / 1024 / 1024) + write!(f, "{} MiB", self.0 / 1024 / 1024) } } } @@ -137,14 +138,14 @@ impl Syntax { if let Some(language) = maybe_extension.and_then(tree_sitter::Language::try_from_extension) { - self.tree_sitter_highlight(bufnr, false, Some(language)) + self.enable_tree_sitter_highlighting(bufnr, false, Some(language)) .await?; self.toggle.turn_on(); } else { - let filetype = self.vim.getbufvar::(bufnr, "&filetype").await?; + let filetype = self.vim.filetype(bufnr).await?; if let Some(language) = tree_sitter::Language::try_from_filetype(&filetype) { - self.tree_sitter_highlight(bufnr, false, Some(language)) + self.enable_tree_sitter_highlighting(bufnr, false, Some(language)) .await?; self.toggle.turn_on(); } @@ -154,17 +155,19 @@ impl Syntax { Ok(()) } - async fn identify_buffer_language(&self, bufnr: usize, source_file: &Path) -> Option { + async fn detect_buffer_language(&self, bufnr: usize, source_file: &Path) -> Option { if let Some(language) = tree_sitter::Language::try_from_path(source_file) { Some(language) - } else if let Ok(filetype) = self.vim.getbufvar::(bufnr, "&filetype").await { - tree_sitter::Language::try_from_filetype(&filetype) } else { - None + self.vim + .filetype(bufnr) + .await + .ok() + .and_then(|ft| tree_sitter::Language::try_from_filetype(&ft)) } } - async fn tree_sitter_highlight( + async fn enable_tree_sitter_highlighting( &mut self, bufnr: usize, buf_modified: bool, @@ -175,8 +178,7 @@ impl Syntax { let language = match maybe_language { Some(language) => language, None => { - let Some(language) = self.identify_buffer_language(bufnr, &source_file).await - else { + let Some(language) = self.detect_buffer_language(bufnr, &source_file).await else { // No language detected, fallback to the vim regex syntax highlighting. self.vim.exec("execute", "syntax on")?; return Ok(()); @@ -216,7 +218,7 @@ impl Syntax { ); let maybe_vim_highlights = self - .render_ts_highlights(bufnr, language, &raw_highlights, file_size) + .convert_tree_sitter_to_vim_highlights(bufnr, language, &raw_highlights, file_size) .await?; self.ts_bufs.insert( @@ -232,14 +234,16 @@ impl Syntax { Ok(()) } + /// Converts Tree-sitter highlights to Vim highlights based on the rendering strategy. + /// /// Returns Some() if the vim highlights are changed. - async fn render_ts_highlights( + async fn convert_tree_sitter_to_vim_highlights( &self, bufnr: usize, language: Language, raw_ts_highlights: &RawTsHighlights, file_size: FileSize, - ) -> Result, PluginError> { + ) -> Result, PluginError> { use maple_config::RenderStrategy; let render_strategy = &maple_config::config().plugin.syntax.render_strategy; @@ -280,6 +284,11 @@ impl Syntax { .iter() .partition(|item| old_vim_highlights.contains(item)); + // No new highlight changes since the last highlighting operation. + if changed_highlights.is_empty() { + return Ok(None); + } + let unchanged_lines = unchanged_highlights .into_iter() .map(|item| item.0) @@ -290,11 +299,6 @@ impl Syntax { .map(|item| item.0) .collect::>(); - // No new highlight changes since the last highlighting operation. - if changed_lines.is_empty() { - return Ok(None); - } - tracing::trace!( total = new_vim_highlights.len(), unchanged_lines_count = unchanged_lines.len(), @@ -348,7 +352,7 @@ impl Syntax { let file_size = FileSize(source_code.len()); let maybe_new_vim_highlights = self - .render_ts_highlights(bufnr, language, &new_highlights, file_size) + .convert_tree_sitter_to_vim_highlights(bufnr, language, &new_highlights, file_size) .await?; self.ts_bufs.entry(bufnr).and_modify(|i| { @@ -412,23 +416,26 @@ pub fn convert_raw_ts_highlights_to_vim_highlights( raw_ts_highlights: &RawTsHighlights, language: Language, highlight_range: HighlightRange, -) -> VimHighlights { +) -> VimHighlightData { raw_ts_highlights .iter() - .filter(|(line_number, _)| highlight_range.should_highlight(**line_number)) - .map(|(line_number, highlight_items)| { - let line_highlights: Vec<(usize, usize, &str)> = highlight_items - .iter() - .map(|i| { - ( - i.start.column, - i.end.column - i.start.column, - language.highlight_group(i.highlight), - ) - }) - .collect(); - - (*line_number, line_highlights) + .filter_map(|(line_number, highlight_items)| { + if highlight_range.should_highlight(*line_number) { + let line_highlights: Vec<(usize, usize, &str)> = highlight_items + .iter() + .map(|i| { + ( + i.start.column, + i.end.column - i.start.column, + language.highlight_group(i.highlight), + ) + }) + .collect(); + + Some((*line_number, line_highlights)) + } else { + None + } }) .collect::>() } @@ -463,11 +470,12 @@ impl ClapPlugin for Syntax { CursorMoved => { if self.tree_sitter_enabled { if self.vim.bufmodified(bufnr).await? { - self.tree_sitter_highlight(bufnr, true, None).await?; + self.enable_tree_sitter_highlighting(bufnr, true, None) + .await?; } else { let maybe_new_vim_highlights = if let Some(ts_info) = self.ts_bufs.get(&bufnr) { - self.render_ts_highlights( + self.convert_tree_sitter_to_vim_highlights( bufnr, ts_info.language, &ts_info.highlights.0, @@ -501,7 +509,8 @@ impl ClapPlugin for Syntax { match self.parse_action(method)? { SyntaxAction::TreeSitterHighlight => { let bufnr = self.vim.bufnr("").await?; - self.tree_sitter_highlight(bufnr, false, None).await?; + self.enable_tree_sitter_highlighting(bufnr, false, None) + .await?; self.tree_sitter_enabled = true; self.toggle.turn_on(); } diff --git a/crates/maple_core/src/stdio_server/plugin/system.rs b/crates/maple_core/src/stdio_server/plugin/system.rs index 662e9aa2a..d107067b4 100644 --- a/crates/maple_core/src/stdio_server/plugin/system.rs +++ b/crates/maple_core/src/stdio_server/plugin/system.rs @@ -55,7 +55,7 @@ fn parse_vim_which_key_map(config_file: &str) -> HashMap Result<() let context = context.clone(); let rg_cmd = RgTokioCommand::new(context.cwd.to_path_buf()); - let job_id = utils::calculate_hash(&rg_cmd); + let job_id = utils::compute_hash(&rg_cmd); job::try_start( async move { if let Ok(digest) = rg_cmd.create_cache().await { diff --git a/crates/maple_core/src/stdio_server/provider/hooks/on_move.rs b/crates/maple_core/src/stdio_server/provider/hooks/on_move.rs index fa3d04261..b0e5aa639 100644 --- a/crates/maple_core/src/stdio_server/provider/hooks/on_move.rs +++ b/crates/maple_core/src/stdio_server/provider/hooks/on_move.rs @@ -1,6 +1,6 @@ use crate::previewer; +use crate::previewer::text_file::{generate_text_preview, TextPreview}; use crate::previewer::vim_help::HelpTagPreview; -use crate::previewer::{get_file_preview, FilePreview}; use crate::stdio_server::job; use crate::stdio_server::plugin::syntax::convert_raw_ts_highlights_to_vim_highlights; use crate::stdio_server::plugin::syntax::sublime::{ @@ -9,10 +9,11 @@ use crate::stdio_server::plugin::syntax::sublime::{ use crate::stdio_server::provider::{read_dir_entries, Context, ProviderSource}; use crate::stdio_server::vim::{preview_syntax, VimResult}; use crate::tools::ctags::{current_context_tag, BufferTag}; +use maple_config::HighlightEngine; use paths::{expand_tilde, truncate_absolute_path}; use pattern::*; use serde::{Deserialize, Serialize}; -use std::io::{Error, ErrorKind, Result}; +use std::io::{Error, ErrorKind, Read, Result}; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; @@ -20,13 +21,15 @@ use std::time::Duration; use sublime_syntax::TokenHighlight; use tokio::sync::oneshot; use utils::display_width; +use utils::io::SizeChecker; -type SublimeHighlights = Vec<(usize, Vec)>; +type SublimeHighlightData = Vec<(usize, Vec)>; /// (start, length, highlight_group) -type LineHighlights = Vec<(usize, usize, String)>; +type LineHighlightData = Vec<(usize, usize, String)>; + /// (line_number, line_highlights) -type TsHighlights = Vec<(usize, LineHighlights)>; +type TreeSitterHighlightData = Vec<(usize, LineHighlightData)>; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct VimSyntaxInfo { @@ -79,11 +82,11 @@ pub struct Preview { /// Highlights from sublime-syntax highlight engine. #[serde(skip_serializing_if = "Vec::is_empty")] - pub sublime_syntax_highlights: SublimeHighlights, + pub sublime_syntax_highlights: SublimeHighlightData, /// Highlights from tree-sitter highlight engine. #[serde(skip_serializing_if = "Vec::is_empty")] - pub tree_sitter_highlights: TsHighlights, + pub tree_sitter_highlights: TreeSitterHighlightData, #[serde(skip_serializing_if = "Option::is_none")] pub highlight_line: Option, @@ -113,15 +116,34 @@ impl Preview { } } - fn set_highlights(&mut self, sublime_or_ts_highlights: SublimeOrTreeSitter, path: &Path) { - match sublime_or_ts_highlights { - SublimeOrTreeSitter::Sublime(v) => { + fn binary_file_preview(path: impl AsRef) -> Self { + Self::new_file_preview( + vec!["".to_string()], + None, + VimSyntaxInfo::fname(path.as_ref().display().to_string()), + ) + } + + fn large_file_preview(size: u64, path: impl AsRef) -> Self { + let size_in_gib = size as f64 / (1024.0 * 1024.0 * 1024.0); + Self::new_file_preview( + vec![format!( + "File too large to preview (size: {size_in_gib:.2} GiB)." + )], + None, + VimSyntaxInfo::fname(path.as_ref().display().to_string()), + ) + } + + fn set_highlights(&mut self, highlight_source: HighlightSource, path: &Path) { + match highlight_source { + HighlightSource::Sublime(v) => { self.sublime_syntax_highlights = v; } - SublimeOrTreeSitter::TreeSitter(v) => { + HighlightSource::TreeSitter(v) => { self.tree_sitter_highlights = v; } - SublimeOrTreeSitter::Neither => { + HighlightSource::None => { if let Some(syntax) = preview_syntax(path) { self.vim_syntax_info.syntax = syntax.into(); } else { @@ -348,7 +370,7 @@ impl<'a> CachedPreviewImpl<'a> { let elapsed = now.elapsed().as_millis(); if elapsed > 1000 { - tracing::warn!("Fetching preview took too long: {elapsed:?} ms"); + tracing::warn!(preview_target = ?self.preview_target, "Fetching preview took too long: {elapsed:?} ms"); } self.ctx @@ -419,12 +441,23 @@ impl<'a> CachedPreviewImpl<'a> { async fn preview_file>(&self, path: P) -> Result { let path = path.as_ref(); - if !path.is_file() { - return Err(Error::new( - ErrorKind::Other, - format!("Failed to preview as {} is not a file", path.display()), - )); - } + let file_size_tier = match detect_file_class(path)? { + FileClass::NotRegularFile => { + return Err(Error::new( + ErrorKind::Other, + format!("Failed to preview as {} is not a file", path.display()), + )); + } + FileClass::Binary => { + return Ok(Preview::binary_file_preview(path)); + } + FileClass::Text(file_size_tier) => { + if let utils::io::FileSizeTier::Large(size) = file_size_tier { + return Ok(Preview::large_file_preview(size, path)); + } + file_size_tier + } + }; let handle_io_error = |e: &Error| { if e.kind() == ErrorKind::NotFound { @@ -439,59 +472,84 @@ impl<'a> CachedPreviewImpl<'a> { (true, false) => { // Title is not available before nvim 0.9 let max_fname_len = self.ctx.env.display_line_width - 1; - previewer::preview_file_with_truncated_title( + let previewer::text_file::TextLines { + lines, + display_path, + } = previewer::text_file::preview_file( path, self.preview_height, self.max_line_width(), - max_fname_len, + Some(max_fname_len), + file_size_tier, ) - .inspect_err(handle_io_error)? + .inspect_err(handle_io_error)?; + (lines, display_path) } _ => { - let (lines, abs_path) = - previewer::preview_file(path, self.preview_height, self.max_line_width()) - .inspect_err(handle_io_error)?; - // cwd is shown via the popup title, no need to include it again. + let previewer::text_file::TextLines { + lines, + display_path: abs_path, + } = previewer::text_file::preview_file( + path, + self.preview_height, + self.max_line_width(), + None, + file_size_tier, + ) + .inspect_err(handle_io_error)?; + + // cwd is already shown in the popup title, no need to include it again. let cwd_relative = abs_path.replacen(self.ctx.cwd.as_str(), ".", 1); let mut lines = lines; lines[0] = cwd_relative; + (lines, abs_path) } }; - let sublime_or_ts_highlights = SyntaxHighlighter { - lines: lines.clone(), - path: path.to_path_buf(), - line_number_offset: 0, - max_line_width: self.max_line_width(), - range: 0..lines.len(), - maybe_code_context: None, - timeout: 200, - } - .fetch_highlights() - .await; - - let total = utils::line_count(path)?; - let end = lines.len(); - - let scrollbar = if self.ctx.env.should_add_scrollbar(end) { - calculate_scrollbar(self.ctx, 0, end, total) - } else { - None - }; - - if std::fs::metadata(path)?.len() == 0 { + if file_size_tier.is_empty() { let mut lines = lines; lines.push("".to_string()); return Ok(Preview::new_file_preview( lines, - scrollbar, + None, VimSyntaxInfo::fname(fname), )); } + let highlight_source = if file_size_tier.is_small() { + SyntaxHighlighter { + context: HighlightingContext { + lines: lines.clone(), + path: path.to_path_buf(), + line_number_offset: 0, + max_line_width: self.max_line_width(), + range: 0..lines.len(), + maybe_code_context: None, + }, + timeout: 200, + } + .highlight_with_timeout() + .await + } else { + HighlightSource::None + }; + + // Only display the scrollbar when it's not a large file. + let scrollbar = if file_size_tier.can_process() { + let end = lines.len(); + if self.ctx.env.should_add_scrollbar(end) { + let total = utils::io::line_count(path)?; + compute_scrollbar_position(self.ctx, 0, end, total) + } else { + None + } + } else { + None + }; + let mut preview = Preview::new_file_preview(lines, scrollbar, VimSyntaxInfo::default()); - preview.set_highlights(sublime_or_ts_highlights, path); + preview.set_highlights(highlight_source, path); Ok(preview) } @@ -503,7 +561,30 @@ impl<'a> CachedPreviewImpl<'a> { column_range: Option>, container_width: usize, ) -> Preview { - tracing::debug!(path = ?path.display(), lnum, "Previewing file"); + tracing::debug!("Previewing file {}:{lnum}", path.display()); + + match detect_file_class(path) { + Ok(FileClass::NotRegularFile) => { + return Preview::new_file_preview( + vec!["".to_string()], + None, + VimSyntaxInfo::fname(path.display().to_string()), + ); + } + Ok(FileClass::Binary) => return Preview::binary_file_preview(path), + Ok(FileClass::Text(file_size_tier)) => { + if let utils::io::FileSizeTier::Large(size) = file_size_tier { + return Preview::large_file_preview(size, path); + } + } + Err(err) => { + return Preview::new_file_preview( + vec![err.to_string()], + None, + VimSyntaxInfo::fname(path.display().to_string()), + ); + } + }; let fname = path.display().to_string(); @@ -520,8 +601,8 @@ impl<'a> CachedPreviewImpl<'a> { } }; - match get_file_preview(path, lnum, self.preview_height) { - Ok(FilePreview { + match generate_text_preview(path, lnum, self.preview_height) { + Ok(TextPreview { start, end, total, @@ -529,26 +610,28 @@ impl<'a> CachedPreviewImpl<'a> { lines, }) => { let maybe_code_context = - find_code_context(&lines, highlight_lnum, lnum, start, path).await; + fetch_code_context(&lines, highlight_lnum, lnum, start, path).await; // 1 (header line) + 1 (1-based line number) let line_number_offset = 1 + 1 + if maybe_code_context.is_some() { 3 } else { 0 }; - let sublime_or_ts_highlights = SyntaxHighlighter { - lines: lines.clone(), - path: path.to_path_buf(), - line_number_offset, - max_line_width: self.max_line_width(), - range: start..end + 1, - maybe_code_context: maybe_code_context.clone(), + let highlight_source = SyntaxHighlighter { + context: HighlightingContext { + lines: lines.clone(), + path: path.to_path_buf(), + line_number_offset, + max_line_width: self.max_line_width(), + range: start..end + 1, + maybe_code_context: maybe_code_context.clone(), + }, timeout: 200, } - .fetch_highlights() + .highlight_with_timeout() .await; let context_lines = maybe_code_context .map(|code_context| { - code_context.into_context_lines(container_width, self.ctx.env.is_nvim) + code_context.format_for_display(container_width, self.ctx.env.is_nvim) }) .unwrap_or_default(); @@ -569,7 +652,7 @@ impl<'a> CachedPreviewImpl<'a> { start }; - calculate_scrollbar(self.ctx, start, end, total) + compute_scrollbar_position(self.ctx, start, end, total) } else { None }; @@ -584,16 +667,12 @@ impl<'a> CachedPreviewImpl<'a> { ..Default::default() }; - preview.set_highlights(sublime_or_ts_highlights, path); + preview.set_highlights(highlight_source, path); preview } Err(err) => { - tracing::error!( - ?path, - provider_id = %self.ctx.provider_id(), - "Couldn't read first lines: {err:?}", - ); + tracing::error!(?path, provider_id = %self.ctx.provider_id(), "Couldn't read first lines: {err:?}"); let header_line = truncated_preview_header(); let lines = vec![ header_line, @@ -617,7 +696,7 @@ impl<'a> CachedPreviewImpl<'a> { tracing::debug!(?latest_line, ?cache_line, "The cache is probably outdated"); let shell_cmd = crate::tools::rg::rg_shell_command(&self.ctx.cwd); - let job_id = utils::calculate_hash(&shell_cmd); + let job_id = utils::compute_hash(&shell_cmd); if job::reserve(job_id) { let ctx = self.ctx.clone(); @@ -676,7 +755,33 @@ impl<'a> CachedPreviewImpl<'a> { } } -async fn context_tag_with_timeout(path: &Path, lnum: usize) -> Option { +enum FileClass { + NotRegularFile, + Binary, + Text(utils::io::FileSizeTier), +} + +fn detect_file_class(path: &Path) -> std::io::Result { + if !path.is_file() { + return Ok(FileClass::NotRegularFile); + } + + let mut file = std::fs::File::open(path)?; + let metadata = file.metadata()?; + + let mut buf = vec![0u8; 1024]; + let n = file.read(&mut buf)?; + let content_type = content_inspector::inspect(&buf[..n]); + + if content_type.is_binary() { + return Ok(FileClass::Binary); + } + + let file_size_tier = utils::io::FileSizeTier::from_metadata(&metadata); + Ok(FileClass::Text(file_size_tier)) +} + +async fn fetch_context_tag_with_timeout(path: &Path, lnum: usize) -> Option { let (tag_sender, tag_receiver) = oneshot::channel(); const TIMEOUT: Duration = Duration::from_millis(200); @@ -692,7 +797,7 @@ async fn context_tag_with_timeout(path: &Path, lnum: usize) -> Option match tokio::time::timeout(TIMEOUT, tag_receiver).await { Ok(res) => res.ok().flatten(), Err(_) => { - tracing::debug!(timeout = ?TIMEOUT, ?path, lnum, "⏳ Did not get the context tag in time"); + tracing::debug!(timeout = ?TIMEOUT, ?path, lnum, "⏳ Timeout fetching context tag"); None } } @@ -720,10 +825,12 @@ impl CodeContext { // 1 context line + 2 border lines. const CONTEXT_LINES_LEN: usize = 3; + /// Converts the context into series of formatted lines for display. + /// /// ------------------ /// line /// ------------------ - fn into_context_lines(self, container_width: usize, is_nvim: bool) -> Vec { + fn format_for_display(self, container_width: usize, is_nvim: bool) -> Vec { // Vim has a different border width. let border_line = "─".repeat(if is_nvim { container_width @@ -752,7 +859,7 @@ impl CodeContext { } } -async fn find_code_context( +async fn fetch_code_context( lines: &[String], highlight_lnum: usize, lnum: usize, @@ -773,13 +880,10 @@ async fn find_code_context( return None; }; - match context_tag_with_timeout(path, lnum).await { - Some(tag) if tag.line_number < start => { - let pattern = tag.trimmed_pattern(); - Some(CodeContext { - line: pattern.to_string(), - }) - } + match fetch_context_tag_with_timeout(path, lnum).await { + Some(tag) if tag.line_number < start => Some(CodeContext { + line: tag.trimmed_pattern().to_string(), + }), _ => { // No context lines if no tag found prior to the line number. None @@ -787,7 +891,7 @@ async fn find_code_context( } } -fn calculate_scrollbar( +fn compute_scrollbar_position( ctx: &Context, start: usize, end: usize, @@ -806,7 +910,7 @@ fn calculate_scrollbar( let top_position = if ctx.env.preview_border_enabled { length -= if length == preview_winheight { 1 } else { 0 }; - 1usize.max(top_position as usize) + top_position.max(1.0) as usize } else { top_position as usize }; @@ -815,19 +919,23 @@ fn calculate_scrollbar( } } -enum SublimeOrTreeSitter { - Sublime(SublimeHighlights), - TreeSitter(TsHighlights), - Neither, +enum HighlightSource { + Sublime(SublimeHighlightData), + TreeSitter(TreeSitterHighlightData), + None, } -struct SyntaxHighlighter { +struct HighlightingContext { lines: Vec, path: PathBuf, line_number_offset: usize, max_line_width: usize, range: Range, maybe_code_context: Option, +} + +struct SyntaxHighlighter { + context: HighlightingContext, // Timeout in milliseconds. timeout: u64, } @@ -835,49 +943,29 @@ struct SyntaxHighlighter { impl SyntaxHighlighter { // Fetch with highlights with a timeout. // - // `fetch_syntax_highlights` might be slow for larger files (over 100k lines) as tree-sitter will + // `compute_syntax_highlighting` might be slow for larger files (over 100k lines) as tree-sitter will // have to parse the whole file to obtain the highlight info. Therefore, we must run the actual // worker in a separated task to not make the async runtime blocked, otherwise we may run into // the issue of frozen UI. - async fn fetch_highlights(self) -> SublimeOrTreeSitter { + async fn highlight_with_timeout(self) -> HighlightSource { let (result_sender, result_receiver) = oneshot::channel(); - let Self { - lines, - path, - line_number_offset, - max_line_width, - range, - maybe_code_context, - timeout, - } = self; + let Self { context, timeout } = self; + + let path = context.path.clone(); std::thread::spawn({ - let path = path.clone(); move || { - let result = fetch_syntax_highlights( - &lines, - &path, - line_number_offset, - max_line_width, - range, - maybe_code_context.as_ref(), - ); + let result = compute_syntax_highlighting(context); let _ = result_sender.send(result); } }); - let timeout = Duration::from_millis(timeout); - - match tokio::time::timeout(timeout, result_receiver).await { - Ok(res) => res.unwrap_or(SublimeOrTreeSitter::Neither), + match tokio::time::timeout(Duration::from_millis(timeout), result_receiver).await { + Ok(res) => res.unwrap_or(HighlightSource::None), Err(_) => { - tracing::debug!( - ?timeout, - ?path, - "⏳ Did not get the preview highlight in time" - ); - SublimeOrTreeSitter::Neither + tracing::debug!(?timeout, ?path, "⏳ Timeout fetching preview highlights"); + HighlightSource::None } } } @@ -885,142 +973,154 @@ impl SyntaxHighlighter { // TODO: this might be slow for larger files (over 100k lines) as tree-sitter will have to // parse the whole file to obtain the highlight info. We may make the highlighting async. -fn fetch_syntax_highlights( +fn compute_syntax_highlighting(context: HighlightingContext) -> HighlightSource { + let HighlightingContext { + lines, + path, + line_number_offset, + max_line_width, + range, + maybe_code_context, + } = context; + + match maple_config::config().provider.preview_highlight_engine { + HighlightEngine::SublimeSyntax => { + sublime_highlighting(&lines, &path, line_number_offset, max_line_width) + } + HighlightEngine::TreeSitter => { + tree_sitter_highlighting(&path, range, max_line_width, maybe_code_context.as_ref()) + } + HighlightEngine::Vim => HighlightSource::None, + } +} + +fn sublime_highlighting( lines: &[String], path: &Path, line_number_offset: usize, max_line_width: usize, - range: Range, - maybe_code_context: Option<&CodeContext>, -) -> SublimeOrTreeSitter { - use maple_config::HighlightEngine; - use utils::SizeChecker; +) -> HighlightSource { + const THEME: &str = "Visual Studio Dark+"; - let provider_config = &maple_config::config().provider; + let theme = match &maple_config::config().provider.sublime_syntax_color_scheme { + Some(theme) => { + if sublime_theme_exists(theme) { + theme.as_str() + } else { + tracing::warn!("preview color theme {theme} not found, fallback to {THEME}"); + THEME + } + } + None => THEME, + }; - match provider_config.preview_highlight_engine { - HighlightEngine::SublimeSyntax => { - const THEME: &str = "Visual Studio Dark+"; + path.extension() + .and_then(|s| s.to_str()) + .and_then(sublime_syntax_by_extension) + .map(|syntax| { + // Same reason as [`Self::truncate_preview_lines()`], if a line is too + // long and the query is short, the highlights can be enomerous and + // cause the Vim frozen due to the too many highlight works. + let max_len = max_line_width; + let lines = lines.iter().map(|s| { + let len = s.len().min(max_len); + &s[..len] + }); + sublime_syntax_highlight(syntax, lines, line_number_offset, theme) + }) + .map(HighlightSource::Sublime) + .unwrap_or(HighlightSource::None) +} - let theme = match &provider_config.sublime_syntax_color_scheme { - Some(theme) => { - if sublime_theme_exists(theme) { - theme.as_str() - } else { - tracing::warn!( - "preview color theme {theme} not found, fallback to {THEME}" - ); - THEME - } - } - None => THEME, +fn tree_sitter_highlighting( + path: &Path, + visible_range: Range, + max_line_width: usize, + code_context: Option<&CodeContext>, +) -> HighlightSource { + const FILE_SIZE_CHECKER: SizeChecker = SizeChecker::new(1024 * 1024); + + if FILE_SIZE_CHECKER.is_too_large(path).unwrap_or(true) { + return HighlightSource::None; + } + + tree_sitter::Language::try_from_path(path) + .and_then(|language| { + let Ok(source_code) = std::fs::read(path) else { + return None; }; - path.extension() - .and_then(|s| s.to_str()) - .and_then(sublime_syntax_by_extension) - .map(|syntax| { - // Same reason as [`Self::truncate_preview_lines()`], if a line is too - // long and the query is short, the highlights can be enomerous and - // cause the Vim frozen due to the too many highlight works. - let max_len = max_line_width; - let lines = lines.iter().map(|s| { - let len = s.len().min(max_len); - &s[..len] - }); - sublime_syntax_highlight(syntax, lines, line_number_offset, theme) - }) - .map(SublimeOrTreeSitter::Sublime) - .unwrap_or(SublimeOrTreeSitter::Neither) - } - HighlightEngine::TreeSitter => { - const FILE_SIZE_CHECKER: SizeChecker = SizeChecker::new(1024 * 1024); + // TODO: Cache the highlights per one provider session or even globally? + // 1. Check the last modified time. + // 2. If unchanged, try retrieving from the cache. + // 3. Otherwise parse it. + let Ok(raw_highlights) = language.highlight(&source_code) else { + return None; + }; - if FILE_SIZE_CHECKER.is_too_large(path).unwrap_or(true) { - return SublimeOrTreeSitter::Neither; - } + let line_start = visible_range.start; + + let ts_highlights = convert_raw_ts_highlights_to_vim_highlights( + &raw_highlights, + language, + visible_range.into(), + ); + + let mut maybe_context_line_highlight = None; + + let context_lines_offset = if let Some(code_context) = code_context { + if let Ok(highlight_items) = language.highlight_line(code_context.line.as_bytes()) { + let line_highlights = highlight_items + .into_iter() + .filter_map(|i| { + let start = i.start.column; + let length = i.end.column - i.start.column; + // Ignore the invisible highlights. + if start + length > max_line_width { + None + } else { + let group = language.highlight_group(i.highlight); + Some((start, length, group.to_string())) + } + }) + .collect::>(); - tree_sitter::Language::try_from_path(path) - .and_then(|language| { - let Ok(source_code) = std::fs::read(path) else { - return None; - }; + maybe_context_line_highlight + .replace((CodeContext::CONTEXT_LINE_NUMBER, line_highlights)); + } - // TODO: Cache the highlights per one provider session or even globally? - // 1. Check the last modified time. - // 2. If unchanged, try retrieving from the cache. - // 3. Otherwise parse it. - let Ok(raw_highlights) = language.highlight(&source_code) else { - return None; - }; + CodeContext::CONTEXT_LINES_LEN + } else { + 0 + }; - let line_start = range.start; - let ts_highlights = convert_raw_ts_highlights_to_vim_highlights( - &raw_highlights, - language, - range.into(), - ); - - let mut maybe_context_line_highlight = None; - - let context_lines_offset = if let Some(code_context) = maybe_code_context { - if let Ok(highlight_items) = - language.highlight_line(code_context.line.as_bytes()) - { - let line_highlights = highlight_items - .into_iter() - .filter_map(|i| { - let start = i.start.column; - let length = i.end.column - i.start.column; - // Ignore the invisible highlights. - if start + length > max_line_width { - None - } else { - let group = language.highlight_group(i.highlight); - Some((start, length, group.to_string())) - } - }) - .collect::>(); - - maybe_context_line_highlight - .replace((CodeContext::CONTEXT_LINE_NUMBER, line_highlights)); - } - - CodeContext::CONTEXT_LINES_LEN - } else { - 0 - }; + let tree_sitter_highlight_data = ts_highlights + .into_iter() + .map(|(line_number, line_highlights)| { + let line_number_in_preview_win = + line_number - line_start + 1 + context_lines_offset; + + // Workaround the lifetime issue, nice to remove this allocation + // `group.to_string()` as it's essentially `&'static str`. + let line_highlights = line_highlights + .into_iter() + .filter_map(|(start, length, group)| { + // Ignore the invisible highlights. + if start + length > max_line_width { + None + } else { + Some((start, length, group.to_string())) + } + }) + .collect(); - Some( - ts_highlights - .into_iter() - .map(|(line_number, line_highlights)| { - let line_number_in_preview_win = - line_number - line_start + 1 + context_lines_offset; - - // Workaround the lifetime issue, nice to remove this allocation - // `group.to_string()` as it's essentially `&'static str`. - let line_highlights = line_highlights - .into_iter() - .filter_map(|(start, length, group)| { - // Ignore the invisible highlights. - if start + length > max_line_width { - None - } else { - Some((start, length, group.to_string())) - } - }) - .collect(); - - (line_number_in_preview_win, line_highlights) - }) - .chain(maybe_context_line_highlight) - .collect(), - ) + (line_number_in_preview_win, line_highlights) }) - .map(SublimeOrTreeSitter::TreeSitter) - .unwrap_or(SublimeOrTreeSitter::Neither) - } - HighlightEngine::Vim => SublimeOrTreeSitter::Neither, - } + .chain(maybe_context_line_highlight) + .collect::>(); + + Some(tree_sitter_highlight_data) + }) + .map(HighlightSource::TreeSitter) + .unwrap_or(HighlightSource::None) } diff --git a/crates/maple_core/src/stdio_server/provider/impls/dumb_jump/mod.rs b/crates/maple_core/src/stdio_server/provider/impls/dumb_jump/mod.rs index 53601c5a9..7ebc61fd6 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/dumb_jump/mod.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/dumb_jump/mod.rs @@ -168,7 +168,7 @@ impl DumbJumpProvider { } async fn initialize_tags(&self, extension: String, cwd: AbsPathBuf) -> VimResult<()> { - let job_id = utils::calculate_hash(&(&cwd, "dumb_jump")); + let job_id = utils::compute_hash(&(&cwd, "dumb_jump")); if job::reserve(job_id) { let ctags_future = { diff --git a/crates/maple_core/src/stdio_server/provider/impls/generic_provider.rs b/crates/maple_core/src/stdio_server/provider/impls/generic_provider.rs index 6cd196e55..487c30857 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/generic_provider.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/generic_provider.rs @@ -3,7 +3,7 @@ use crate::stdio_server::provider::{ BaseArgs, ClapProvider, Context, ProviderError, ProviderResult as Result, ProviderSource, }; use crate::stdio_server::SearchProgressor; -use filter::{FilterContext, ParallelSource}; +use filter::{FilterContext, ParallelInputSource}; use parking_lot::Mutex; use printer::Printer; use std::path::PathBuf; @@ -58,9 +58,9 @@ fn start_filter_parallel( &query, filter_context, match data_source { - DataSource::File(path) => ParallelSource::File(path), + DataSource::File(path) => ParallelInputSource::File(path), DataSource::Command(command) => { - ParallelSource::Exec(Box::new(Exec::shell(command).cwd(cwd))) + ParallelInputSource::Exec(Box::new(Exec::shell(command).cwd(cwd))) } }, SearchProgressor::new(vim, stop_signal.clone()), diff --git a/crates/maple_core/src/stdio_server/provider/impls/lsp.rs b/crates/maple_core/src/stdio_server/provider/impls/lsp.rs index b75788cfd..eb92adbba 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/lsp.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/lsp.rs @@ -110,7 +110,7 @@ impl LocationItem { // location is 0-based. let start_line = location.range.start.line + 1; let start_character = location.range.start.character; - let line = utils::read_line_at(&file_path, start_line as usize) + let line = utils::io::read_line_at(&file_path, start_line as usize) .ok() .flatten()?; @@ -463,7 +463,7 @@ impl ClapProvider for LspProvider { .into_iter() .filter_map(|line_number| self.fetch_location_at(line_number - 1)) .filter_map(|loc| { - let text = utils::read_line_at(&loc.path, loc.row).ok().flatten()?; + let text = utils::io::read_line_at(&loc.path, loc.row).ok().flatten()?; Some(serde_json::json!({ "filename": loc.path, "lnum": loc.row, diff --git a/crates/maple_core/src/stdio_server/provider/impls/recent_files.rs b/crates/maple_core/src/stdio_server/provider/impls/recent_files.rs index 0ddb23331..e76e19553 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/recent_files.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/recent_files.rs @@ -82,9 +82,20 @@ impl RecentFilesProvider { let preview = match (preview_size, ranked.get(lnum - 1)) { (Some(size), Some(new_entry)) => { let new_curline = new_entry.display_text().to_string(); - if let Ok((lines, fname)) = - crate::previewer::preview_file(new_curline, size, self.printer.line_width) - { + let file_size_tier = utils::io::FileSizeTier::from_metadata( + &std::fs::File::open(&new_curline)?.metadata()?, + ); + if let Ok(text_lines) = crate::previewer::text_file::preview_file( + new_curline, + size, + self.printer.line_width, + None, + file_size_tier, + ) { + let crate::previewer::text_file::TextLines { + lines, + display_path: fname, + } = text_lines; Some(json!({ "lines": lines, "fname": fname })) } else { None diff --git a/crates/maple_core/src/stdio_server/provider/mod.rs b/crates/maple_core/src/stdio_server/provider/mod.rs index ee3230dd1..1d6ec8b37 100644 --- a/crates/maple_core/src/stdio_server/provider/mod.rs +++ b/crates/maple_core/src/stdio_server/provider/mod.rs @@ -166,7 +166,7 @@ impl ScrollFile { fn new(line_start: usize, path: &Path) -> std::io::Result { Ok(Self { line_start, - total_lines: utils::line_count(path)?, + total_lines: utils::io::line_count(path)?, }) } } @@ -493,7 +493,7 @@ impl Context { /// Executes the command `cmd` and returns the raw bytes of stdout. pub fn exec_cmd(&self, cmd: &str) -> std::io::Result> { - let out = utils::execute_at(cmd, Some(&self.cwd))?; + let out = utils::execute_shell_command(cmd, Some(&self.cwd))?; Ok(out.stdout) } @@ -825,7 +825,7 @@ impl ProviderSource { .collect(), ), Self::File { ref path, .. } | Self::CachedFile { ref path, .. } => { - let lines_iter = utils::read_first_lines(path, n).ok()?; + let lines_iter = utils::io::read_first_lines(path, n).ok()?; if provider_id == "blines" { let items = lines_iter .enumerate() diff --git a/crates/maple_core/src/stdio_server/request_handler.rs b/crates/maple_core/src/stdio_server/request_handler.rs index 29bb9ad1e..666f26d56 100644 --- a/crates/maple_core/src/stdio_server/request_handler.rs +++ b/crates/maple_core/src/stdio_server/request_handler.rs @@ -1,3 +1,4 @@ +use crate::previewer::text_file::TextLines; use crate::stdio_server::Error; use rpc::RpcRequest; use serde::Deserialize; @@ -33,7 +34,19 @@ pub async fn preview_file(msg: RpcRequest) -> Result { (display_height, preview_width.unwrap_or(display_width)) }; - let (lines, fname) = crate::previewer::preview_file(fpath, preview_height, preview_width)?; + let file_size_tier = + utils::io::FileSizeTier::from_metadata(&std::fs::File::open(&fpath)?.metadata()?); + + let TextLines { + lines, + display_path: fname, + } = crate::previewer::text_file::preview_file( + fpath, + preview_height, + preview_width, + None, + file_size_tier, + )?; let value = json!({"id": msg_id, "result": json!({"lines": lines, "fname": fname})}); @@ -41,7 +54,7 @@ pub async fn preview_file(msg: RpcRequest) -> Result { } pub async fn preview_quickfix(msg: RpcRequest) -> Result { - use crate::previewer::{preview_file, preview_file_at}; + use crate::previewer::text_file::{preview_file, preview_file_at}; use std::path::PathBuf; let msg_id = msg.id; @@ -68,7 +81,10 @@ pub async fn preview_quickfix(msg: RpcRequest) -> Result { let result = if lnum == 0 { let size = winheight + 5; - let (lines, _) = preview_file(fpath.as_path(), size, winwidth)?; + let file_size_tier = + utils::io::FileSizeTier::from_metadata(&std::fs::File::open(&fpath)?.metadata()?); + let TextLines { lines, .. } = + preview_file(fpath.as_path(), size, winwidth, None, file_size_tier)?; json!({ "event": "on_move", "lines": lines, "fname": fpath }) } else { let (lines, hi_lnum) = preview_file_at(fpath.as_path(), winheight, winwidth, lnum)?; diff --git a/crates/maple_core/src/stdio_server/service.rs b/crates/maple_core/src/stdio_server/service.rs index 0b445faf0..45d91cdfb 100644 --- a/crates/maple_core/src/stdio_server/service.rs +++ b/crates/maple_core/src/stdio_server/service.rs @@ -23,18 +23,6 @@ pub type ProviderSessionId = u64; // Type alias here for readability. type DebouncedProviderEvent = ProviderEvent; -#[derive(Debug)] -pub struct ProviderSession { - ctx: Context, - id: ProviderId, - provider_session_id: ProviderSessionId, - /// Each provider session can have its own message processing logic. - provider: Box, - provider_events: UnboundedReceiver, - /// Whether the provider handler is still busy with processing the last event. - is_busy: Arc, -} - struct DebounceTimer { last_emitted: Option, debounce_period: Duration, @@ -48,19 +36,47 @@ impl DebounceTimer { } } - fn should_emit(&mut self) -> bool { + fn should_emit_and_update(&mut self) -> bool { let now = std::time::Instant::now(); if self.last_emitted.is_none() || now.duration_since(self.last_emitted.expect("Must be Some as checked")) > self.debounce_period { - self.last_emitted.replace(now); + self.last_emitted = Some(now); return true; } false } } +#[derive(Debug)] +pub struct ProviderSession { + ctx: Context, + id: ProviderId, + provider_session_id: ProviderSessionId, + /// Each provider session can have its own message processing logic. + provider: Box, + provider_events: UnboundedReceiver, + /// Whether the provider handler is still busy with processing the last event. + is_busy: Arc, +} + +struct CachedEvents(VecDeque); + +impl CachedEvents { + /// Track the event if it does not exist in the cache yet. + fn push(&mut self, event: ProviderEvent) { + if self.0.iter().any(|e| event.is_same_type(e)) { + return; + } + self.0.push_back(event); + } + + fn pop(&mut self) -> Option { + self.0.pop_front() + } +} + impl ProviderSession { pub fn new( ctx: Context, @@ -86,7 +102,7 @@ impl ProviderSession { let mut on_move_timer = DebounceTimer::new(Duration::from_millis(200)); let mut on_typed_timer = DebounceTimer::new(Duration::from_millis(debounce_delay)); - let mut event_cache = VecDeque::with_capacity(2); + let mut cached_events = CachedEvents(VecDeque::with_capacity(2)); let mut tick_timeout = { let mut interval = tokio::time::interval(Duration::from_millis(100)); @@ -108,21 +124,20 @@ impl ProviderSession { }; let should_emit = match &event { - ProviderEvent::OnMove(..) => on_move_timer.should_emit(), - ProviderEvent::OnTyped(..) => on_typed_timer.should_emit(), + ProviderEvent::OnMove(..) => on_move_timer.should_emit_and_update(), + ProviderEvent::OnTyped(..) => on_typed_timer.should_emit_and_update(), _ => true, }; // Send event after debounce period if the provider is not overloaded. if should_emit { if provider_is_busy.load(Ordering::SeqCst) { - if event_cache.iter().any(|e| event.is_same_type(e)) { - continue; - } - event_cache.push_back(event); + cached_events.push(event); } else if debounced_provider_event_sender.send(event).is_err() { return; } + } else { + cached_events.push(event); } } _ = tick_timeout.tick() => { @@ -130,7 +145,7 @@ impl ProviderSession { return; } - if let Some(event) = event_cache.pop_front() { + if let Some(event) = cached_events.pop() { if debounced_provider_event_sender.send(event).is_err() { return; } @@ -152,7 +167,7 @@ impl ProviderSession { (provider_session, origin_provider_event_sender) } - pub fn start_event_loop(self) { + pub fn run(self) { let debounce_delay = self.ctx.provider_debounce(); tracing::debug!( @@ -164,9 +179,9 @@ impl ProviderSession { tokio::spawn(async move { if debounce_delay > 0 { - self.run_event_loop_with_debounce(debounce_delay).await; + self.run_provider_with_debounce(debounce_delay).await; } else { - self.run_event_loop_without_debounce().await; + self.run_provider_without_debounce().await; } }); } @@ -176,7 +191,7 @@ impl ProviderSession { // Debounce timer delay. 150ms between keystrokes is about 45 WPM, so we // want something that is longer than that, but not too long to // introduce detectable UI delay; 200ms is a decent compromise. - async fn run_event_loop_with_debounce(mut self, debounce_delay: u64) { + async fn run_provider_with_debounce(mut self, debounce_delay: u64) { // If the debounce timer isn't active, it will be set to expire "never", // which is actually just 1 year in the future. const NEVER: Duration = Duration::from_secs(365 * 24 * 60 * 60); @@ -279,7 +294,7 @@ impl ProviderSession { } } - async fn run_event_loop_without_debounce(mut self) { + async fn run_provider_without_debounce(mut self) { while let Some(event) = self.provider_events.recv().await { tracing::trace!(debounce = false, "[{}] Received event: {event:?}", self.id); @@ -404,103 +419,85 @@ pub struct PluginSession { } impl PluginSession { + /// Creates a new [`PluginSession`] and starts its event processing. pub fn create( plugin: Box, maybe_event_delay: Option, ) -> UnboundedSender { let (plugin_event_sender, plugin_event_receiver) = unbounded_channel(); + let plugin_id = plugin.id(); + let plugin_session = PluginSession { plugin, plugin_events: plugin_event_receiver, }; - if let Some(event_delay) = maybe_event_delay { - plugin_session.start_event_loop(event_delay); - } else { - plugin_session.start_event_loop_without_debounce(); - } - - plugin_event_sender - } - - fn start_event_loop_without_debounce(mut self) { - tracing::debug!(debounce = false, id = ?self.plugin.id(), "starting a new plugin"); - tokio::spawn(async move { - loop { - tokio::select! { - maybe_plugin_event = self.plugin_events.recv() => { - if let Some(plugin_event) = maybe_plugin_event { - let res = match plugin_event.clone() { - PluginEvent::Autocmd(autocmd) => self.plugin.handle_autocmd(autocmd).await, - PluginEvent::Action(action) => self.plugin.handle_action(action).await, - }; - if let Err(err) = res { - tracing::error!(?err, id = self.plugin.id(), "Failed to process {plugin_event:?}"); - } - } else { - break; - } - } - } + if let Some(delay) = maybe_event_delay { + tracing::debug!(debounce = ?delay, plugin_id, "Starting plugin with debounce"); + plugin_session.run_with_debounce(delay).await; + } else { + tracing::debug!(plugin_id, "Starting plugin without debounce"); + plugin_session.run_without_debounce().await; } }); - } - fn start_event_loop(mut self, event_delay: Duration) { - let id = self.plugin.id(); + plugin_event_sender + } - tracing::debug!(debounce = ?event_delay, ?id, "starting a new plugin"); + async fn run_without_debounce(mut self) { + while let Some(plugin_event) = self.plugin_events.recv().await { + self.process_event(plugin_event).await; + } + } - tokio::spawn(async move { - // If the debounce timer isn't active, it will be set to expire "never", - // which is actually just 1 year in the future. - const NEVER: Duration = Duration::from_secs(365 * 24 * 60 * 60); + async fn run_with_debounce(mut self, event_delay: Duration) { + // If the debounce timer isn't active, it will be set to expire "never", + // which is actually just 1 year in the future. + const NEVER: Duration = Duration::from_secs(365 * 24 * 60 * 60); - let mut pending_plugin_event = None; - let notification_timer = tokio::time::sleep(NEVER); - tokio::pin!(notification_timer); + let mut pending_plugin_event = None; + let notification_timer = tokio::time::sleep(NEVER); + tokio::pin!(notification_timer); - loop { - tokio::select! { - maybe_plugin_event = self.plugin_events.recv() => { - match maybe_plugin_event { - Some(plugin_event) => { - // tracing::trace!(?plugin_event, "[{id}] Received event"); - - if plugin_event.should_debounce() { - pending_plugin_event.replace(plugin_event); - notification_timer.as_mut().reset(Instant::now() + event_delay); - } else { - let res = match plugin_event.clone() { - PluginEvent::Autocmd(autocmd) => self.plugin.handle_autocmd(autocmd).await, - PluginEvent::Action(action) => self.plugin.handle_action(action).await, - }; - if let Err(err) = res { - tracing::error!(?err, id, "Failed to process {plugin_event:?}"); - } - } + loop { + tokio::select! { + maybe_plugin_event = self.plugin_events.recv() => { + match maybe_plugin_event { + Some(plugin_event) => { + // tracing::trace!(?plugin_event, "[{id}] Received event"); + + if plugin_event.should_debounce() { + pending_plugin_event.replace(plugin_event); + notification_timer.as_mut().reset(Instant::now() + event_delay); + } else { + self.process_event(plugin_event).await; } - None => break, // channel has closed. } + None => break, // channel has closed. } - _ = notification_timer.as_mut(), if pending_plugin_event.is_some() => { - notification_timer.as_mut().reset(Instant::now() + NEVER); - - if let Some(autocmd) = pending_plugin_event.take() { - let res = match autocmd.clone() { - PluginEvent::Autocmd(autocmd) => self.plugin.handle_autocmd(autocmd).await, - PluginEvent::Action(action) => self.plugin.handle_action(action).await, - }; - if let Err(err) = res { - tracing::error!(?err, id, "Failed to process {autocmd:?}"); - } - } + } + _ = notification_timer.as_mut(), if pending_plugin_event.is_some() => { + notification_timer.as_mut().reset(Instant::now() + NEVER); + + if let Some(autocmd) = pending_plugin_event.take() { + self.process_event(autocmd).await; } } } - }); + } + } + + async fn process_event(&mut self, plugin_event: PluginEvent) { + let res = match plugin_event.clone() { + PluginEvent::Action(action) => self.plugin.handle_action(action).await, + PluginEvent::Autocmd(autocmd) => self.plugin.handle_autocmd(autocmd).await, + }; + if let Err(err) = res { + let id = self.plugin.id(); + tracing::error!(?err, "[{id}] Failed to process {plugin_event:?}"); + } } } @@ -534,7 +531,7 @@ impl ServiceManager { let (provider_session, provider_event_sender) = ProviderSession::new(ctx, provider_session_id, provider); - provider_session.start_event_loop(); + provider_session.run(); provider_event_sender .send(ProviderEvent::Internal(InternalProviderEvent::Initialize)) diff --git a/crates/maple_core/src/stdio_server/vim.rs b/crates/maple_core/src/stdio_server/vim.rs index 0ddbec50b..22fa01e50 100644 --- a/crates/maple_core/src/stdio_server/vim.rs +++ b/crates/maple_core/src/stdio_server/vim.rs @@ -468,6 +468,10 @@ impl Vim { self.bare_call("get_cursor_pos").await } + pub async fn filetype(&self, bufnr: usize) -> VimResult { + self.getbufvar::(bufnr, "&filetype").await + } + pub async fn bufmodified(&self, bufnr: impl Serialize) -> VimResult { self.getbufvar::(bufnr, "&modified") .await diff --git a/crates/maple_core/src/tools/ctags/mod.rs b/crates/maple_core/src/tools/ctags/mod.rs index cb8cb6fc1..cb7ab1ac6 100644 --- a/crates/maple_core/src/tools/ctags/mod.rs +++ b/crates/maple_core/src/tools/ctags/mod.rs @@ -209,7 +209,7 @@ impl<'a, P: AsRef + Hash> TagsGenerator<'a, P> { /// The file path of generated tags is determined by the hash of command itself. pub fn tags_path(&self) -> PathBuf { let mut tags_path = CTAGS_TAGS_DIR.deref().clone(); - tags_path.push(utils::calculate_hash(self).to_string()); + tags_path.push(utils::compute_hash(self).to_string()); tags_path } diff --git a/crates/maple_markdown/src/toc.rs b/crates/maple_markdown/src/toc.rs index d32ace3e5..32311ed7a 100644 --- a/crates/maple_markdown/src/toc.rs +++ b/crates/maple_markdown/src/toc.rs @@ -116,7 +116,7 @@ fn parse_toc( line_start: usize, ) -> std::io::Result> { let mut code_fence = None; - Ok(utils::read_lines(input_file)? + Ok(utils::io::read_lines(input_file)? .skip(line_start) .filter_map(Result::ok) .filter(|line| match &code_fence { @@ -176,7 +176,7 @@ pub fn generate_toc( pub fn find_toc_range(input_file: impl AsRef) -> std::io::Result> { let mut start = 0; - for (idx, line) in utils::read_lines(input_file)? + for (idx, line) in utils::io::read_lines(input_file)? .map_while(Result::ok) .enumerate() { diff --git a/crates/printer/src/lib.rs b/crates/printer/src/lib.rs index e985f43dc..fbfff0d9d 100644 --- a/crates/printer/src/lib.rs +++ b/crates/printer/src/lib.rs @@ -306,7 +306,7 @@ pub(crate) mod tests { .build(query.into()); filter_sequential( - SequentialSource::List(std::iter::once(Arc::new(line.into()) as Arc)), + SequentialSource::Iterator(std::iter::once(Arc::new(line.into()) as Arc)), matcher, ) .unwrap() diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 514cab31d..6fbb568e4 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -7,4 +7,5 @@ edition.workspace = true [dependencies] bytecount = { workspace = true } memchr = { workspace = true } +memmap2 = { workspace = true } simdutf8 = { workspace = true } diff --git a/crates/utils/src/io.rs b/crates/utils/src/io.rs index efe71ddc2..bd3fdac0f 100644 --- a/crates/utils/src/io.rs +++ b/crates/utils/src/io.rs @@ -1,7 +1,10 @@ use std::fs::{read_dir, remove_dir_all, remove_file, File}; -use std::io::{BufRead, BufReader, Lines, Result}; +use std::io::{BufRead, BufReader, Lines, Read, Result}; use std::path::Path; +const SMALL_FILE_THRESHOLD: u64 = 1024 * 1024; // 1 MiB +const MEDIUM_FILE_THRESHOLD: u64 = 1024 * 1024 * 1024; // 1 GiB + /// Counts lines in the source `handle`. /// /// # Examples @@ -122,11 +125,114 @@ pub fn read_first_lines>( path: P, number: usize, ) -> Result> { - read_lines_from(path, 0usize, number) + read_lines_from_small(path, 0usize, number) +} + +/// Represents the size category of a file. +#[derive(Debug, Clone, Copy)] +pub enum FileSizeTier { + /// Empty file + Empty, + /// Suitable for immediate processing + Small, + /// Suitable for chunked or memory-mapped processing + Medium, + /// Too large to process efficiently + Large(u64), +} + +impl FileSizeTier { + pub fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } + + pub fn is_small(&self) -> bool { + matches!(self, Self::Small) + } + + pub fn is_large(&self) -> bool { + matches!(self, Self::Large(_)) + } + + pub fn can_process(&self) -> bool { + !self.is_large() + } + + pub fn from_metadata(metadata: &std::fs::Metadata) -> Self { + let file_size = metadata.len(); + + match file_size { + 0 => FileSizeTier::Empty, + 1..SMALL_FILE_THRESHOLD => FileSizeTier::Small, + SMALL_FILE_THRESHOLD..MEDIUM_FILE_THRESHOLD => FileSizeTier::Medium, + _ => FileSizeTier::Large(file_size), + } + } +} + +/// Determines the size tier of a file based on its size. +pub fn determine_file_size_tier(path: impl AsRef) -> Result { + Ok(FileSizeTier::from_metadata(&path.as_ref().metadata()?)) +} + +/// Returns a `number` of lines from a small file starting from the line number `from` (0-based). +pub fn read_lines_from_medium>( + path: P, + from: usize, + number: usize, +) -> Result> { + read_lines_in_chunks(path, from, number) +} + +/// Reads `number` lines starting from `from` in chunks for large files. +fn read_lines_in_chunks>( + path: P, + from: usize, + number: usize, +) -> Result> { + let file = File::open(path)?; + let mut reader = BufReader::new(file); + let mut buffer = vec![0; 8 * 1024 * 1024]; // 8 MiB chunk size + let mut total_lines = Vec::with_capacity(number); + let mut current_line = 0; + + while total_lines.len() < number && reader.read(&mut buffer)? > 0 { + for chunk in buffer.split(|&b| b == b'\n') { + if current_line >= from { + if let Ok(line) = std::str::from_utf8(chunk) { + total_lines.push(line.to_string()); + } + if total_lines.len() == number { + break; + } + } + current_line += 1; + } + } + + Ok(total_lines) +} + +/// Returns a `number` of lines from a large file starting from the line number `from` (0-based). +pub fn read_lines_using_mmap>( + path: P, + from: usize, + number: usize, +) -> Result> { + let file = File::open(&path)?; + let mmap = unsafe { memmap2::Mmap::map(&file)? }; + let buffer = &mmap[..]; + let lines = buffer + .split(|&b| b == b'\n') + .skip(from) + .take(number) + .filter_map(|line_bytes| std::str::from_utf8(line_bytes).ok().map(|s| s.to_string())) + .collect(); + Ok(lines) } -/// Returns a `number` of lines starting from the line number `from` (0-based). -pub fn read_lines_from>( +/// Returns a `number` of lines from a small file starting from the line number `from` (0-based). +pub fn read_lines_from_small>( path: P, from: usize, number: usize, @@ -151,7 +257,10 @@ fn read_preview_lines_utf8>( } else { (0, 2 * size, target_line) }; - Ok((read_lines_from(path, start, end - start)?, highlight_lnum)) + Ok(( + read_lines_from_small(path, start, end - start)?, + highlight_lnum, + )) } #[cfg(test)] diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index c56a2f4c4..910b14a2d 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -7,22 +7,16 @@ use std::path::Path; use std::process::{Command, Output}; pub mod bytelines; -mod io; - -pub use self::io::{ - count_lines, create_or_overwrite, file_size, line_count, read_first_lines, read_line_at, - read_lines, read_lines_from, remove_dir_contents, SizeChecker, -}; +pub mod io; /// Returns the width of displaying `n` on the screen. /// /// Same with `n.to_string().len()` but without allocation. -pub fn display_width(n: usize) -> usize { +pub fn display_width(mut n: usize) -> usize { if n == 0 { return 1; } - let mut n = n; let mut len = 0; while n > 0 { len += 1; @@ -37,14 +31,17 @@ pub fn is_git_repo(dir: &Path) -> bool { dir.join(".git").exists() } -pub fn calculate_hash(t: &T) -> u64 { +pub fn compute_hash(t: &T) -> u64 { let mut s = DefaultHasher::new(); t.hash(&mut s); s.finish() } -/// Converts `shell_cmd` to `Command` with optional working directory. -pub fn as_std_command>(shell_cmd: impl AsRef, dir: Option

) -> Command { +/// Constructs a `Command` for executing a shell command. +pub fn build_shell_command>( + shell_cmd: impl AsRef, + dir: Option

, +) -> Command { let mut cmd = if cfg!(target_os = "windows") { let mut cmd = Command::new("cmd"); cmd.arg("/C").arg(shell_cmd.as_ref()); @@ -63,16 +60,18 @@ pub fn as_std_command>(shell_cmd: impl AsRef, dir: Option< } /// Executes the `shell_cmd` and returns the output. -pub fn execute_at(shell_cmd: S, dir: Option

) -> std::io::Result +pub fn execute_shell_command(shell_cmd: S, dir: Option

) -> std::io::Result where S: AsRef, P: AsRef, { - let mut cmd = as_std_command(shell_cmd, dir); + let mut cmd = build_shell_command(shell_cmd, dir); cmd.output() } -/// Converts the char positions to byte positions as Vim and Neovim highlights is byte-positioned. +/// Converts the char positions to byte positions for use in Vim/NeoVim. +/// +/// Vim and Neovim highlights use byte positions, this utility translate char positions. pub fn char_indices_to_byte_indices(s: &str, char_indices: &[usize]) -> Vec { s.char_indices() .enumerate() @@ -87,7 +86,7 @@ pub fn char_indices_to_byte_indices(s: &str, char_indices: &[usize]) -> Vec Option { +pub fn char_index_at_byte(line: &str, byte_idx: usize) -> Option { line.char_indices().enumerate().find_map( |(c_idx, (b_idx, _c))| { if byte_idx == b_idx { @@ -100,7 +99,7 @@ pub fn char_index_for(line: &str, byte_idx: usize) -> Option { } /// Returns the char at given byte index (0-based) in a line. -pub fn char_at(line: &str, byte_idx: usize) -> Option { +pub fn char_at_byte(line: &str, byte_idx: usize) -> Option { line.char_indices() .find_map(|(b_idx, c)| if byte_idx == b_idx { Some(c) } else { None }) } diff --git a/pythonx/clap/fuzzymatch-rs/Cargo.lock b/pythonx/clap/fuzzymatch-rs/Cargo.lock index c15476871..81525dc25 100644 --- a/pythonx/clap/fuzzymatch-rs/Cargo.lock +++ b/pythonx/clap/fuzzymatch-rs/Cargo.lock @@ -1,6 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -17,12 +32,33 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bstr" version = "1.2.0" @@ -41,6 +77,44 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -48,22 +122,148 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "dumb_analyzer" -version = "0.1.0" +name = "code_tools" +version = "0.1.54" dependencies = [ + "cargo_metadata", + "maple_config", + "maple_lsp", "once_cell", + "paths", + "regex", + "serde", "serde_json", + "tokio", + "toml", + "tracing", + "which", ] +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "0.1.54" +dependencies = [ + "directories", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "extracted_fzy" -version = "0.1.0" +version = "0.1.54" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] [[package]] name = "fuzzy-matcher" @@ -84,6 +284,23 @@ dependencies = [ "types", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "grep-matcher" version = "0.1.6" @@ -108,9 +325,30 @@ dependencies = [ "thread_local", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "icon" -version = "0.1.0" +version = "0.1.54" dependencies = [ "itertools", "pattern", @@ -118,36 +356,30 @@ dependencies = [ ] [[package]] -name = "indoc" -version = "0.3.6" +name = "idna" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "indoc-impl", - "proc-macro-hack", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "indoc-impl" -version = "0.3.6" +name = "indexmap" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", - "unindent", + "equivalent", + "hashbrown", ] [[package]] -name = "instant" -version = "0.1.12" +name = "indoc" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "itertools" @@ -166,9 +398,25 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -189,15 +437,62 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "maple_config" +version = "0.1.54" +dependencies = [ + "dirs 0.1.54", + "once_cell", + "paths", + "serde", + "serde_json", + "toml", + "types", +] + +[[package]] +name = "maple_lsp" +version = "0.1.54" +dependencies = [ + "dirs 0.1.54", + "futures-util", + "lsp-types", + "parking_lot", + "paths", + "rpc", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "tracing", + "which", +] + [[package]] name = "matcher" -version = "0.1.0" +version = "0.1.54" dependencies = [ - "dumb_analyzer", + "code_tools", "extracted_fzy", "fuzzy-matcher", "grep-matcher", "grep-regex", + "norm", + "nucleo-matcher", "pattern", "types", ] @@ -208,69 +503,156 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "norm" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5725a3379c44dc0adf3437af87cf21c10df473ed858d654b12603dea102508" +dependencies = [ + "memchr", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-targets 0.52.6", ] [[package]] -name = "paste" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" -dependencies = [ - "paste-impl", - "proc-macro-hack", -] - -[[package]] -name = "paste-impl" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +name = "paths" +version = "0.1.54" dependencies = [ - "proc-macro-hack", + "dirs 0.1.54", + "dunce", + "itertools", + "serde", + "shellexpand", ] [[package]] name = "pattern" -version = "0.1.0" +version = "0.1.54" dependencies = [ "once_cell", "regex", ] [[package]] -name = "printer" +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "printer" +version = "0.1.54" dependencies = [ "icon", + "pattern", "serde", "serde_json", "types", @@ -278,52 +660,60 @@ dependencies = [ "utils", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.15.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41d50a7271e08c7c8a54cd24af5d62f73ee3a6f6a314215281ebdec421d5752" +checksum = "e484fd2c8b4cb67ab05a318f1fd6fa8f199fcc30819f08f07d200809dba26c15" dependencies = [ "cfg-if", "indoc", "libc", - "parking_lot", - "paste", + "memoffset", + "once_cell", + "portable-atomic", "pyo3-build-config", + "pyo3-ffi", "pyo3-macros", "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.15.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779239fc40b8e18bc8416d3a37d280ca9b9fb04bda54b98037bb6748595c2410" +checksum = "dc0e0469a84f208e20044b98965e1561028180219e35352a2afaf2b942beff3b" dependencies = [ "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1547a7f9966f6f1a0f0227564a9945fe36b90da5a93b3933fc3dc03fae372d" +dependencies = [ + "libc", + "pyo3-build-config", ] [[package]] name = "pyo3-macros" -version = "0.15.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b247e8c664be87998d8628e86f282c25066165f1f8dda66100c48202fdb93a" +checksum = "fdb6da8ec6fa5cedd1626c886fc8749bdcbb09424a86461eb8cdf096b7c33257" dependencies = [ + "proc-macro2", "pyo3-macros-backend", "quote", "syn", @@ -331,10 +721,11 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.15.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8c2812c412e00e641d99eeb79dd478317d981d938aa60325dfa7157b607095" +checksum = "38a385202ff5a92791168b1136afae5059d3ac118457bb7bc304c197c2d33e7d" dependencies = [ + "heck", "proc-macro2", "pyo3-build-config", "quote", @@ -343,20 +734,31 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "bitflags", + "getrandom", + "libredox", + "thiserror", ] [[package]] @@ -382,6 +784,36 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "rpc" +version = "0.1.54" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ryu" version = "1.0.10" @@ -394,20 +826,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +dependencies = [ + "serde", +] + [[package]] name = "serde" -version = "1.0.137" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -425,12 +866,59 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs 5.0.1", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simdutf8" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.8.0" @@ -439,15 +927,41 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "syn" -version = "1.0.96" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.4" @@ -457,20 +971,148 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + [[package]] name = "types" -version = "0.1.0" +version = "0.1.54" dependencies = [ "icon", "pattern", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.9" @@ -479,18 +1121,49 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unindent" -version = "0.1.9" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "url" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] [[package]] name = "utils" -version = "0.1.0" +version = "0.1.54" dependencies = [ "bytecount", "memchr", + "memmap2", "simdutf8", - "types", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -514,3 +1187,160 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] diff --git a/pythonx/clap/fuzzymatch-rs/Cargo.toml b/pythonx/clap/fuzzymatch-rs/Cargo.toml index 76a0957df..cc408f03e 100644 --- a/pythonx/clap/fuzzymatch-rs/Cargo.toml +++ b/pythonx/clap/fuzzymatch-rs/Cargo.toml @@ -13,7 +13,7 @@ name = "fuzzymatch_rs" crate-type = ["cdylib"] [dependencies] -pyo3 = "0.15" +pyo3 = { version = "0.23", features = ["auto-initialize"] } printer = { path = "../../../crates/printer" } matcher = { path = "../../../crates/matcher" } diff --git a/pythonx/clap/fuzzymatch-rs/src/lib.rs b/pythonx/clap/fuzzymatch-rs/src/lib.rs index 95cc764ea..bb50ffa78 100644 --- a/pythonx/clap/fuzzymatch-rs/src/lib.rs +++ b/pythonx/clap/fuzzymatch-rs/src/lib.rs @@ -148,8 +148,8 @@ fn fuzzy_match( /// This module is a python module implemented in Rust. #[pymodule] -fn fuzzymatch_rs(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(fuzzy_match))?; +fn fuzzymatch_rs(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(fuzzy_match, m)?)?; Ok(()) } @@ -161,38 +161,38 @@ mod tests { #[test] fn py_and_rs_subscore_should_work() { use matcher::substring::substr_indices as substr_scorer; + use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::PyModule; - use std::fs; use types::CaseMatching; - let cur_dir = std::env::current_dir().unwrap(); - let py_path = cur_dir.parent().unwrap().join("scorer.py"); - let py_source_code = fs::read_to_string(py_path).unwrap(); - - pyo3::prepare_freethreaded_python(); - - let gil = Python::acquire_gil(); - let py = gil.python(); - let py_scorer = PyModule::from_code(py, &py_source_code, "scorer.py", "scorer").unwrap(); - - let test_cases = vec![ - ("su ou", "substr_scorer_should_work"), - ("su ork", "substr_scorer_should_work"), - ]; - - for (needle, haystack) in test_cases.into_iter() { - let py_result: (i32, Vec) = py_scorer - .getattr("substr_scorer") - .unwrap() - .call1((needle, haystack)) - .unwrap() - .extract() - .map(|(score, positions): (f64, Vec)| (score as i32, positions)) - .unwrap(); - let rs_result = substr_scorer(haystack, needle, CaseMatching::Smart).unwrap(); - assert_eq!(py_result, rs_result); - } + Python::with_gil(|py| { + let py_scorer = PyModule::from_code( + py, + c_str!(include_str!("../../scorer.py")), + c_str!("scorer.py"), + c_str!("scorer"), + ) + .unwrap(); + + let test_cases = vec![ + ("su ou", "substr_scorer_should_work"), + ("su ork", "substr_scorer_should_work"), + ]; + + for (needle, haystack) in test_cases.into_iter() { + let py_result: (i32, Vec) = py_scorer + .getattr("substr_scorer") + .unwrap() + .call1((needle, haystack)) + .unwrap() + .extract() + .map(|(score, positions): (f64, Vec)| (score as i32, positions)) + .unwrap(); + let rs_result = substr_scorer(haystack, needle, CaseMatching::Smart).unwrap(); + assert_eq!(py_result, rs_result); + } + }); } #[test]