diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cf6bb2703..d9f041911 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2337,6 +2337,229 @@ fn global_search(cx: &mut Context) { ); } +fn search_in_directory(cx: &mut Context, search_root: PathBuf) { + #[derive(Debug)] + struct FileResult { + path: PathBuf, + /// 0 indexed lines + line_num: usize, + } + + impl FileResult { + fn new(path: &Path, line_num: usize) -> Self { + Self { + path: path.to_path_buf(), + line_num, + } + } + } + + impl ui::menu::Item for FileResult { + type Data = Option; + + fn format(&self, current_path: &Self::Data) -> Row { + let relative_path = helix_core::path::get_relative_path(&self.path) + .to_string_lossy() + .into_owned(); + if current_path + .as_ref() + .map(|p| p == &self.path) + .unwrap_or(false) + { + format!("{} (*)", relative_path).into() + } else { + relative_path.into() + } + } + } + + let config = cx.editor.config(); + let smart_case = config.search.smart_case; + let file_picker_config = config.file_picker.clone(); + + let reg = cx.register.unwrap_or('/'); + let completions = search_completions(cx, Some(reg)); + ui::regex_prompt( + cx, + "global-search:".into(), + Some(reg), + move |_editor: &Editor, input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, + move |cx, regex, event| { + if event != PromptEvent::Validate { + return; + } + cx.editor.registers.last_search_register = reg; + + let current_path = doc_mut!(cx.editor).path().cloned(); + let documents: Vec<_> = cx + .editor + .documents() + .map(|doc| (doc.path().cloned(), doc.text().to_owned())) + .collect(); + + if let Ok(matcher) = RegexMatcherBuilder::new() + .case_smart(smart_case) + .build(regex.as_str()) + { + let search_root = search_root.clone(); + + if !search_root.exists() { + cx.editor + .set_error("Current working directory does not exist"); + return; + } + + let (picker, injector) = Picker::stream(current_path); + + let dedup_symlinks = file_picker_config.deduplicate_links; + let absolute_root = search_root + .canonicalize() + .unwrap_or_else(|_| search_root.clone()); + let injector_ = injector.clone(); + + std::thread::spawn(move || { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + WalkBuilder::new(search_root) + .hidden(file_picker_config.hidden) + .parents(file_picker_config.parents) + .ignore(file_picker_config.ignore) + .follow_links(file_picker_config.follow_symlinks) + .git_ignore(file_picker_config.git_ignore) + .git_global(file_picker_config.git_global) + .git_exclude(file_picker_config.git_exclude) + .max_depth(file_picker_config.max_depth) + .filter_entry(move |entry| { + filter_picker_entry(entry, &absolute_root, dedup_symlinks) + }) + .build_parallel() + .run(|| { + let mut searcher = searcher.clone(); + let matcher = matcher.clone(); + let injector = injector_.clone(); + let documents = &documents; + Box::new(move |entry: Result| -> WalkState { + let entry = match entry { + Ok(entry) => entry, + Err(_) => return WalkState::Continue, + }; + + match entry.file_type() { + Some(entry) if entry.is_file() => {} + // skip everything else + _ => return WalkState::Continue, + }; + + let mut stop = false; + let sink = sinks::UTF8(|line_num, _| { + stop = injector + .push(FileResult::new(entry.path(), line_num as usize - 1)) + .is_err(); + + Ok(!stop) + }); + let doc = documents.iter().find(|&(doc_path, _)| { + doc_path + .as_ref() + .map_or(false, |doc_path| doc_path == entry.path()) + }); + + let result = if let Some((_, doc)) = doc { + // there is already a buffer for this file + // search the buffer instead of the file because it's faster + // and captures new edits without requiring a save + if searcher.multi_line_with_matcher(&matcher) { + // in this case a continous buffer is required + // convert the rope to a string + let text = doc.to_string(); + searcher.search_slice(&matcher, text.as_bytes(), sink) + } else { + searcher.search_reader( + &matcher, + RopeReader::new(doc.slice(..)), + sink, + ) + } + } else { + searcher.search_path(&matcher, entry.path(), sink) + }; + + if let Err(err) = result { + log::error!( + "Global search error: {}, {}", + entry.path().display(), + err + ); + } + if stop { + WalkState::Quit + } else { + WalkState::Continue + } + }) + }); + }); + + cx.jobs.callback(async move { + let call = move |_: &mut Editor, compositor: &mut Compositor| { + let picker = Picker::with_stream( + picker, + injector, + move |cx, FileResult { path, line_num }, action| { + let doc = match cx.editor.open(path, action) { + Ok(id) => doc_mut!(cx.editor, &id), + Err(e) => { + cx.editor.set_error(format!( + "Failed to open file '{}': {}", + path.display(), + e + )); + return; + } + }; + + let line_num = *line_num; + let view = view_mut!(cx.editor); + let text = doc.text(); + if line_num >= text.len_lines() { + cx.editor.set_error( + "The line you jumped to does not exist anymore because the file has changed.", + ); + return; + } + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); + + doc.set_selection(view.id, Selection::single(start, end)); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); + } + }, + ) + .with_preview( + |_editor, FileResult { path, line_num }| { + Some((path.clone().into(), Some((*line_num, *line_num)))) + }, + ); + compositor.push(Box::new(overlaid(picker))) + }; + Ok(Callback::EditorCompositor(Box::new(call))) + }) + } else { + // Otherwise do nothing + // log::warn!("Global Search Invalid Pattern") + } + }, + ); +} + enum Extend { Above, Below, diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index 93c06a328..7e7230231 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -64,23 +64,6 @@ impl ScriptingEngine { } } - pub fn get_keybindings() -> Option> { - let mut map = HashMap::new(); - - // Overlay these in reverse, so the precedence applies correctly - for kind in PLUGIN_PRECEDENCE.iter().rev() { - if let Some(keybindings) = manual_dispatch!(kind, get_keybindings()) { - map.extend(keybindings); - } - } - - if map.is_empty() { - None - } else { - Some(map) - } - } - pub fn handle_keymap_event( editor: &mut ui::EditorView, mode: Mode, @@ -169,12 +152,6 @@ pub trait PluginSystem { /// run anything here that could modify the context before the main editor is available. fn run_initialization_script(&self, _cx: &mut Context) {} - /// Fetch the keybindings so that these can be loaded in to the keybinding map. These are - /// keybindings that overwrite the default ones. - fn get_keybindings(&self) -> Option> { - None - } - /// Allow the engine to directly handle a keymap event. This is some of the tightest integration /// with the engine, directly intercepting any keymap events. By default, this just delegates to the /// editors default keybindings. diff --git a/helix-term/src/commands/engine/scheme.rs b/helix-term/src/commands/engine/scheme.rs index f88000009..7f1100aa7 100644 --- a/helix-term/src/commands/engine/scheme.rs +++ b/helix-term/src/commands/engine/scheme.rs @@ -1,9 +1,11 @@ use helix_core::{ extensions::steel_implementations::{rope_module, SteelRopeSlice}, graphemes, + path::expand_tilde, shellwords::Shellwords, Range, Selection, Tendril, }; +use helix_loader::{current_working_dir, set_current_working_dir}; use helix_view::{ document::Mode, editor::{Action, ConfigEvent}, @@ -16,7 +18,7 @@ use serde_json::Value; use steel::{ gc::unsafe_erased_pointers::CustomReference, rerrs::ErrorKind, - rvals::{as_underlying_type, AsRefMutSteelValFromRef, FromSteelVal, IntoSteelVal}, + rvals::{as_underlying_type, AsRefMutSteelValFromRef, FromSteelVal, IntoSteelVal, SteelString}, steel_vm::{engine::Engine, register_fn::RegisterFn}, SteelErr, SteelVal, }; @@ -97,6 +99,8 @@ thread_local! { pub static REVERSE_BUFFER_MAP: SteelVal = SteelVal::boxed(SteelVal::empty_hashmap()); + + pub static GLOBAL_KEYBINDING_MAP: SteelVal = get_keymap().into_steelval().unwrap(); } fn load_keymap_api(engine: &mut Engine, api: KeyMapApi) { @@ -119,6 +123,11 @@ fn load_keymap_api(engine: &mut Engine, api: KeyMapApi) { REVERSE_BUFFER_MAP.with(|x| x.clone()), ); + module.register_value( + "*global-keybinding-map*", + GLOBAL_KEYBINDING_MAP.with(|x| x.clone()), + ); + engine.register_module(module); } @@ -198,10 +207,6 @@ impl super::PluginSystem for SteelScriptingEngine { run_initialization_script(cx); } - fn get_keybindings(&self) -> Option> { - crate::commands::engine::scheme::SharedKeyBindingsEventQueue::get() - } - fn handle_keymap_event( &self, editor: &mut ui::EditorView, @@ -423,7 +428,8 @@ impl SteelScriptingEngine { } } - None + // Refer to the global keybinding map for the rest + Some(GLOBAL_KEYBINDING_MAP.with(|x| x.clone())) } } @@ -654,8 +660,8 @@ fn run_initialization_script(cx: &mut Context) { }); } -pub static KEYBINDING_QUEUE: Lazy = - Lazy::new(|| SharedKeyBindingsEventQueue::new()); +// pub static KEYBINDING_QUEUE: Lazy = +// Lazy::new(|| SharedKeyBindingsEventQueue::new()); pub static CALLBACK_QUEUE: Lazy = Lazy::new(|| CallbackQueue::new()); @@ -725,44 +731,44 @@ impl CallbackQueue { /// In order to send events from the engine back to the configuration, we can created a shared /// queue that the engine and the config push and pull from. Alternatively, we could use a channel /// directly, however this was easy enough to set up. -pub struct SharedKeyBindingsEventQueue { - raw_bindings: Arc>>, -} +// pub struct SharedKeyBindingsEventQueue { +// raw_bindings: Arc>>, +// } -impl SharedKeyBindingsEventQueue { - pub fn new() -> Self { - Self { - raw_bindings: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), - } - } +// impl SharedKeyBindingsEventQueue { +// pub fn new() -> Self { +// Self { +// raw_bindings: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), +// } +// } - pub fn merge(other_as_json: String) { - KEYBINDING_QUEUE - .raw_bindings - .lock() - .unwrap() - .push(other_as_json); - } +// pub fn merge(other_as_json: String) { +// KEYBINDING_QUEUE +// .raw_bindings +// .lock() +// .unwrap() +// .push(other_as_json); +// } - pub fn get() -> Option> { - let guard = KEYBINDING_QUEUE.raw_bindings.lock().unwrap(); +// pub fn get() -> Option> { +// let guard = KEYBINDING_QUEUE.raw_bindings.lock().unwrap(); - if let Some(first) = guard.get(0).clone() { - let mut initial = serde_json::from_str(first).unwrap(); +// if let Some(first) = guard.get(0).clone() { +// let mut initial = serde_json::from_str(first).unwrap(); - // while let Some(remaining_event) = guard.pop_front() { - for remaining_event in guard.iter() { - let bindings = serde_json::from_str(remaining_event).unwrap(); +// // while let Some(remaining_event) = guard.pop_front() { +// for remaining_event in guard.iter() { +// let bindings = serde_json::from_str(remaining_event).unwrap(); - merge_keys(&mut initial, bindings); - } +// merge_keys(&mut initial, bindings); +// } - return Some(initial); - } +// return Some(initial); +// } - None - } -} +// None +// } +// } impl Custom for PromptEvent {} @@ -1122,7 +1128,7 @@ fn configure_engine() -> std::rc::Rc std::rc::Rccurrent-file", current_path); @@ -1721,6 +1732,13 @@ pub fn cx_pos_within_text(cx: &mut Context) -> usize { pos } +pub fn get_helix_cwd(cx: &mut Context) -> Option { + helix_loader::current_working_dir() + .as_os_str() + .to_str() + .map(|x| x.into()) +} + // Special newline... pub fn custom_insert_newline(cx: &mut Context, indent: String) { let (view, doc) = current_ref!(cx.editor); @@ -1819,3 +1837,8 @@ pub fn custom_insert_newline(cx: &mut Context, indent: String) { let (view, doc) = current!(cx.editor); doc.apply(&transaction, view.id); } + +fn search_in_directory(cx: &mut Context, directory: String) { + let search_path = expand_tilde(&PathBuf::from(directory)); + crate::commands::search_in_directory(cx, search_path); +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 623769d77..5310ee0ad 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3111,13 +3111,15 @@ pub(super) fn command_mode(cx: &mut Context) { // .collect() fuzzy_match( input, - TYPABLE_COMMAND_LIST.iter().map(|command| Cow::from(command.name)).chain(crate::commands::engine::ScriptingEngine::available_commands()), + TYPABLE_COMMAND_LIST + .iter() + .map(|command| Cow::from(command.name)) + .chain(crate::commands::engine::ScriptingEngine::available_commands()), false, ) .into_iter() .map(|(name, _)| (0.., name.into())) .collect() - } else { // Otherwise, use the command's completer and the last shellword // as completion input. diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index e083d911f..a0873c16c 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,5 +1,5 @@ use crate::keymap; -use crate::keymap::{merge_keys, KeyTrie, Keymaps}; +use crate::keymap::{merge_keys, KeyTrie}; use helix_loader::merge_toml_values; use helix_view::document::Mode; use serde::Deserialize; @@ -59,7 +59,6 @@ impl Config { pub fn load( global: Result, local: Result, - engine_overlay: Option>, ) -> Result { let global_config: Result = global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); @@ -76,10 +75,6 @@ impl Config { merge_keys(&mut keys, local_keys) } - if let Some(overlay) = engine_overlay { - merge_keys(&mut keys, overlay); - } - let editor = match (global.editor, local.editor) { (None, None) => helix_view::editor::Config::default(), (None, Some(val)) | (Some(val), None) => { @@ -107,10 +102,6 @@ impl Config { merge_keys(&mut keys, keymap); } - if let Some(overlay) = engine_overlay { - merge_keys(&mut keys, overlay); - } - Config { theme: config.theme, keys, @@ -134,9 +125,7 @@ impl Config { let local_config = fs::read_to_string(helix_loader::workspace_config_file()) .map_err(ConfigLoadError::Error); - let bindings = crate::commands::engine::ScriptingEngine::get_keybindings(); - - Config::load(global_config, local_config, bindings) + Config::load(global_config, local_config) } } @@ -146,7 +135,7 @@ mod tests { impl Config { fn load_test(config: &str) -> Config { - Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default()), None).unwrap() + Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() } }