Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands/syntax.rs')
| -rw-r--r-- | helix-term/src/commands/syntax.rs | 446 |
1 files changed, 0 insertions, 446 deletions
diff --git a/helix-term/src/commands/syntax.rs b/helix-term/src/commands/syntax.rs deleted file mode 100644 index fec222ce..00000000 --- a/helix-term/src/commands/syntax.rs +++ /dev/null @@ -1,446 +0,0 @@ -use std::{ - collections::HashSet, - iter, - path::{Path, PathBuf}, - sync::Arc, -}; - -use dashmap::DashMap; -use futures_util::FutureExt; -use grep_regex::RegexMatcherBuilder; -use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; -use helix_core::{ - syntax::{Loader, QueryIterEvent}, - Rope, RopeSlice, Selection, Syntax, Uri, -}; -use helix_stdx::{ - path, - rope::{self, RopeSliceExt}, -}; -use helix_view::{ - align_view, - document::{from_reader, SCRATCH_BUFFER_NAME}, - Align, Document, DocumentId, Editor, -}; -use ignore::{DirEntry, WalkBuilder, WalkState}; - -use crate::{ - filter_picker_entry, - ui::{ - overlay::overlaid, - picker::{Injector, PathOrId}, - Picker, PickerColumn, - }, -}; - -use super::Context; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TagKind { - Class, - Constant, - Function, - Interface, - Macro, - Module, - Struct, - Type, -} - -impl TagKind { - fn as_str(&self) -> &'static str { - match self { - Self::Class => "class", - Self::Constant => "constant", - Self::Function => "function", - Self::Interface => "interface", - Self::Macro => "macro", - Self::Module => "module", - Self::Struct => "struct", - Self::Type => "type", - } - } - - fn from_name(name: &str) -> Option<Self> { - match name { - "class" => Some(TagKind::Class), - "constant" => Some(TagKind::Constant), - "function" => Some(TagKind::Function), - "interface" => Some(TagKind::Interface), - "macro" => Some(TagKind::Macro), - "module" => Some(TagKind::Module), - "struct" => Some(TagKind::Struct), - "type" => Some(TagKind::Type), - _ => None, - } - } -} - -// NOTE: Uri is cheap to clone and DocumentId is Copy -#[derive(Debug, Clone)] -enum UriOrDocumentId { - Uri(Uri), - Id(DocumentId), -} - -impl UriOrDocumentId { - fn path_or_id(&self) -> Option<PathOrId<'_>> { - match self { - Self::Id(id) => Some(PathOrId::Id(*id)), - Self::Uri(uri) => uri.as_path().map(PathOrId::Path), - } - } -} - -#[derive(Debug)] -struct Tag { - kind: TagKind, - name: String, - start: usize, - end: usize, - start_line: usize, - end_line: usize, - doc: UriOrDocumentId, -} - -fn tags_iter<'a>( - syntax: &'a Syntax, - loader: &'a Loader, - text: RopeSlice<'a>, - doc: UriOrDocumentId, - pattern: Option<&'a rope::Regex>, -) -> impl Iterator<Item = Tag> + 'a { - let mut tags_iter = syntax.tags(text, loader, ..); - - iter::from_fn(move || loop { - let QueryIterEvent::Match(mat) = tags_iter.next()? else { - continue; - }; - let query = &loader - .tag_query(tags_iter.current_language()) - .expect("must have a tags query to emit matches") - .query; - let Some(kind) = query - .capture_name(mat.capture) - .strip_prefix("definition.") - .and_then(TagKind::from_name) - else { - continue; - }; - let range = mat.node.byte_range(); - if pattern.is_some_and(|pattern| { - !pattern.is_match(text.regex_input_at_bytes(range.start as usize..range.end as usize)) - }) { - continue; - } - let start = text.byte_to_char(range.start as usize); - let end = text.byte_to_char(range.end as usize); - return Some(Tag { - kind, - name: text.slice(start..end).to_string(), - start, - end, - start_line: text.char_to_line(start), - end_line: text.char_to_line(end), - doc: doc.clone(), - }); - }) -} - -pub fn syntax_symbol_picker(cx: &mut Context) { - let doc = doc!(cx.editor); - let Some(syntax) = doc.syntax() else { - cx.editor - .set_error("Syntax tree is not available on this buffer"); - return; - }; - let doc_id = doc.id(); - let text = doc.text().slice(..); - let loader = cx.editor.syn_loader.load(); - let tags = tags_iter(syntax, &loader, text, UriOrDocumentId::Id(doc.id()), None); - - let columns = vec![ - PickerColumn::new("kind", |tag: &Tag, _| tag.kind.as_str().into()), - PickerColumn::new("name", |tag: &Tag, _| tag.name.as_str().into()), - ]; - - let picker = Picker::new( - columns, - 1, // name - tags, - (), - move |cx, tag, action| { - cx.editor.switch(doc_id, action); - let view = view_mut!(cx.editor); - let doc = doc_mut!(cx.editor, &doc_id); - doc.set_selection(view.id, Selection::single(tag.start, tag.end)); - if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center) - } - }, - ) - .with_preview(|_editor, tag| { - Some((tag.doc.path_or_id()?, Some((tag.start_line, tag.end_line)))) - }) - .truncate_start(false); - - cx.push_layer(Box::new(overlaid(picker))); -} - -pub fn syntax_workspace_symbol_picker(cx: &mut Context) { - #[derive(Debug)] - struct SearchState { - searcher_builder: SearcherBuilder, - walk_builder: WalkBuilder, - regex_matcher_builder: RegexMatcherBuilder, - rope_regex_builder: rope::RegexBuilder, - search_root: PathBuf, - /// A cache of files that have been parsed in prior searches. - syntax_cache: DashMap<PathBuf, Option<(Rope, Syntax)>>, - } - - let mut searcher_builder = SearcherBuilder::new(); - searcher_builder.binary_detection(BinaryDetection::quit(b'\x00')); - - // Search from the workspace that the currently focused document is within. This behaves like global - // search most of the time but helps when you have two projects open in splits. - let search_root = if let Some(path) = doc!(cx.editor).path() { - helix_loader::find_workspace_in(path).0 - } else { - helix_loader::find_workspace().0 - }; - - let absolute_root = search_root - .canonicalize() - .unwrap_or_else(|_| search_root.clone()); - - let config = cx.editor.config(); - let dedup_symlinks = config.file_picker.deduplicate_links; - - let mut walk_builder = WalkBuilder::new(&search_root); - walk_builder - .hidden(config.file_picker.hidden) - .parents(config.file_picker.parents) - .ignore(config.file_picker.ignore) - .follow_links(config.file_picker.follow_symlinks) - .git_ignore(config.file_picker.git_ignore) - .git_global(config.file_picker.git_global) - .git_exclude(config.file_picker.git_exclude) - .max_depth(config.file_picker.max_depth) - .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks)) - .add_custom_ignore_filename(helix_loader::config_dir().join("ignore")) - .add_custom_ignore_filename(".helix/ignore"); - - let mut regex_matcher_builder = RegexMatcherBuilder::new(); - regex_matcher_builder.case_smart(config.search.smart_case); - let mut rope_regex_builder = rope::RegexBuilder::new(); - rope_regex_builder.syntax(rope::Config::new().case_insensitive(config.search.smart_case)); - let state = SearchState { - searcher_builder, - walk_builder, - regex_matcher_builder, - rope_regex_builder, - search_root, - syntax_cache: DashMap::default(), - }; - let reg = cx.register.unwrap_or('/'); - cx.editor.registers.last_search_register = reg; - let columns = vec![ - PickerColumn::new("kind", |tag: &Tag, _| tag.kind.as_str().into()), - PickerColumn::new("name", |tag: &Tag, _| tag.name.as_str().into()).without_filtering(), - PickerColumn::new("path", |tag: &Tag, state: &SearchState| { - match &tag.doc { - UriOrDocumentId::Uri(uri) => { - if let Some(path) = uri.as_path() { - let path = if let Ok(stripped) = path.strip_prefix(&state.search_root) { - stripped - } else { - path - }; - path.to_string_lossy().into() - } else { - uri.to_string().into() - } - } - // This picker only uses `Id` for scratch buffers for better display. - UriOrDocumentId::Id(_) => SCRATCH_BUFFER_NAME.into(), - } - }), - ]; - - let get_tags = |query: &str, - editor: &mut Editor, - state: Arc<SearchState>, - injector: &Injector<_, _>| { - if query.len() < 3 { - return async { Ok(()) }.boxed(); - } - // Attempt to find the tag in any open documents. - let pattern = match state.rope_regex_builder.build(query) { - Ok(pattern) => pattern, - Err(err) => return async { Err(anyhow::anyhow!(err)) }.boxed(), - }; - let loader = editor.syn_loader.load(); - for doc in editor.documents() { - let Some(syntax) = doc.syntax() else { continue }; - let text = doc.text().slice(..); - let uri_or_id = doc - .uri() - .map(UriOrDocumentId::Uri) - .unwrap_or_else(|| UriOrDocumentId::Id(doc.id())); - for tag in tags_iter(syntax, &loader, text.slice(..), uri_or_id, Some(&pattern)) { - if injector.push(tag).is_err() { - return async { Ok(()) }.boxed(); - } - } - } - if !state.search_root.exists() { - return async { Err(anyhow::anyhow!("Current working directory does not exist")) } - .boxed(); - } - let matcher = match state.regex_matcher_builder.build(query) { - Ok(matcher) => { - // Clear any "Failed to compile regex" errors out of the statusline. - editor.clear_status(); - matcher - } - Err(err) => { - log::info!( - "Failed to compile search pattern in workspace symbol search: {}", - err - ); - return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed(); - } - }; - let pattern = Arc::new(pattern); - let injector = injector.clone(); - let loader = editor.syn_loader.load(); - let documents: HashSet<_> = editor - .documents() - .filter_map(Document::path) - .cloned() - .collect(); - async move { - let searcher = state.searcher_builder.build(); - state.walk_builder.build_parallel().run(|| { - let mut searcher = searcher.clone(); - let matcher = matcher.clone(); - let injector = injector.clone(); - let loader = loader.clone(); - let documents = &documents; - let pattern = pattern.clone(); - let syntax_cache = &state.syntax_cache; - Box::new(move |entry: Result<DirEntry, ignore::Error>| -> 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 path = entry.path(); - // If this document is open, skip it because we've already processed it above. - if documents.contains(path) { - return WalkState::Continue; - }; - let mut quit = false; - let sink = sinks::UTF8(|_line, _content| { - if !syntax_cache.contains_key(path) { - // Read the file into a Rope and attempt to recognize the language - // and parse it with tree-sitter. Save the Rope and Syntax for future - // queries. - syntax_cache.insert(path.to_path_buf(), syntax_for_path(path, &loader)); - }; - let entry = syntax_cache.get(path).unwrap(); - let Some((text, syntax)) = entry.value() else { - // If the file couldn't be parsed, move on. - return Ok(false); - }; - let uri = Uri::from(path::normalize(path)); - for tag in tags_iter( - syntax, - &loader, - text.slice(..), - UriOrDocumentId::Uri(uri), - Some(&pattern), - ) { - if injector.push(tag).is_err() { - quit = true; - break; - } - } - // Quit after seeing the first regex match. We only care to find files - // that contain the pattern and then we run the tags query within - // those. The location and contents of a match are irrelevant - it's - // only important _if_ a file matches. - Ok(false) - }); - if let Err(err) = searcher.search_path(&matcher, path, sink) { - log::info!("Workspace syntax search error: {}, {}", path.display(), err); - } - if quit { - WalkState::Quit - } else { - WalkState::Continue - } - }) - }); - Ok(()) - } - .boxed() - }; - let picker = Picker::new( - columns, - 1, // name - [], - state, - move |cx, tag, action| { - let doc_id = match &tag.doc { - UriOrDocumentId::Id(id) => *id, - UriOrDocumentId::Uri(uri) => match cx.editor.open(uri.as_path().expect(""), action) { - Ok(id) => id, - Err(e) => { - cx.editor - .set_error(format!("Failed to open file '{uri:?}': {e}")); - return; - } - } - }; - let doc = doc_mut!(cx.editor, &doc_id); - let view = view_mut!(cx.editor); - let len_chars = doc.text().len_chars(); - if tag.start >= len_chars || tag.end > len_chars { - cx.editor.set_error("The location you jumped to does not exist anymore because the file has changed."); - return; - } - doc.set_selection(view.id, Selection::single(tag.start, tag.end)); - if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center) - } - }, - ) - .with_dynamic_query(get_tags, Some(275)) - .with_preview(move |_editor, tag| { - Some(( - tag.doc.path_or_id()?, - Some((tag.start_line, tag.end_line)), - )) - }) - .truncate_start(false); - cx.push_layer(Box::new(overlaid(picker))); -} - -/// Create a Rope and language config for a given existing path without creating a full Document. -fn syntax_for_path(path: &Path, loader: &Loader) -> Option<(Rope, Syntax)> { - let mut file = std::fs::File::open(path).ok()?; - let (rope, _encoding, _has_bom) = from_reader(&mut file, None).ok()?; - let text = rope.slice(..); - let language = loader - .language_for_filename(path) - .or_else(|| loader.language_for_shebang(text))?; - Syntax::new(text, language, loader) - .ok() - .map(|syntax| (rope, syntax)) -} |