changeset 6832:556d271f26b9 draft

Add tags queries/pickers on top of tree-house bindings (grafted from 30da575c7b798c44fce750cde5231cbd6e9666f3)
author Michael Davis <mcarsondavis@gmail.com>
date Thu, 27 Feb 2025 11:51:12 -0500
parents 6ca2a7d9c116
children a1c38f138388
files Cargo.lock helix-core/src/syntax.rs helix-loader/src/lib.rs helix-term/Cargo.toml helix-term/src/commands.rs helix-term/src/commands/syntax.rs helix-term/src/keymap/default.rs languages.toml runtime/queries/_javascript/tags.scm runtime/queries/_typescript/tags.scm runtime/queries/bibtex/tags.scm runtime/queries/c-sharp/tags.scm runtime/queries/c/tags.scm runtime/queries/cpp/tags.scm runtime/queries/elisp/tags.scm runtime/queries/elixir/tags.scm runtime/queries/elm/tags.scm runtime/queries/erlang/tags.scm runtime/queries/gdscript/tags.scm runtime/queries/gjs/tags.scm runtime/queries/go/tags.scm runtime/queries/gts/tags.scm runtime/queries/javascript/tags.scm runtime/queries/jsx/tags.scm runtime/queries/markdown/tags.scm runtime/queries/php-only/tags.scm runtime/queries/php/tags.scm runtime/queries/python/tags.scm runtime/queries/ruby/tags.scm runtime/queries/rust/tags.scm runtime/queries/spicedb/tags.scm runtime/queries/tsx/tags.scm runtime/queries/typescript/tags.scm runtime/queries/typst/tags.scm xtask/src/main.rs
diffstat 34 files changed, 657 insertions(+), 357 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.lock	Tue May 13 20:10:14 2025 -0400
+++ b/Cargo.lock	Thu Feb 27 11:51:12 2025 -0500
@@ -1749,6 +1749,7 @@
  "chrono",
  "content_inspector",
  "crossterm",
+ "dashmap",
  "fern",
  "futures-util",
  "grep-regex",
--- a/helix-core/src/syntax.rs	Tue May 13 20:10:14 2025 -0400
+++ b/helix-core/src/syntax.rs	Thu Feb 27 11:51:12 2025 -0500
@@ -20,7 +20,7 @@
 use ropey::RopeSlice;
 use tree_house::{
     highlighter,
-    query_iter::{QueryIter, QueryIterEvent},
+    query_iter::QueryIter,
     tree_sitter::{
         query::{InvalidPredicateError, UserPredicate},
         Capture, Grammar, InactiveQueryCursor, InputEdit, Node, Pattern, Query, RopeInput, Tree,
@@ -32,6 +32,7 @@
 
 pub use tree_house::{
     highlighter::{Highlight, HighlightEvent},
+    query_iter::QueryIterEvent,
     Error as HighlighterError, LanguageLoader, TreeCursor, TREE_SITTER_MATCH_LIMIT,
 };
 
@@ -42,6 +43,7 @@
     indent_query: OnceCell<Option<IndentQuery>>,
     textobject_query: OnceCell<Option<TextObjectQuery>>,
     rainbow_query: OnceCell<Option<RainbowQuery>>,
+    tag_query: OnceCell<Option<Query>>,
 }
 
 impl LanguageData {
@@ -52,6 +54,7 @@
             indent_query: OnceCell::new(),
             textobject_query: OnceCell::new(),
             rainbow_query: OnceCell::new(),
+            tag_query: OnceCell::new(),
         }
     }
 
@@ -190,6 +193,38 @@
             .as_ref()
     }
 
+    /// Compiles the tags.scm query for a language.
+    /// This function should only be used by this module or the xtask crate.
+    pub fn compile_tag_query(
+        grammar: Grammar,
+        config: &LanguageConfiguration,
+    ) -> Result<Option<Query>> {
+        let name = &config.language_id;
+        let text = read_query(name, "tags.scm");
+        if text.is_empty() {
+            return Ok(None);
+        }
+        let query = Query::new(grammar, &text, |_pattern, predicate| {
+            Err(InvalidPredicateError::unknown(predicate))
+        })
+        .with_context(|| format!("Failed to compile tags.scm query for '{name}'"))?;
+        Ok(Some(query))
+    }
+
+    fn tag_query(&self, loader: &Loader) -> Option<&Query> {
+        self.tag_query
+            .get_or_init(|| {
+                let grammar = self.syntax_config(loader)?.grammar;
+                Self::compile_tag_query(grammar, &self.config)
+                    .map_err(|err| {
+                        log::error!("{err}");
+                    })
+                    .ok()
+                    .flatten()
+            })
+            .as_ref()
+    }
+
     fn reconfigure(&self, scopes: &[String]) {
         if let Some(Some(config)) = self.syntax.get() {
             reconfigure_highlights(config, scopes);
@@ -379,6 +414,10 @@
         self.language(lang).rainbow_query(self)
     }
 
+    pub fn tag_query(&self, lang: Language) -> Option<&Query> {
+        self.language(lang).tag_query(self)
+    }
+
     pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
         &self.language_server_configs
     }
@@ -552,6 +591,15 @@
         QueryIter::new(&self.inner, source, loader, range)
     }
 
+    pub fn tags<'a>(
+        &'a self,
+        source: RopeSlice<'a>,
+        loader: &'a Loader,
+        range: impl RangeBounds<u32>,
+    ) -> QueryIter<'a, 'a, impl FnMut(Language) -> Option<&'a Query> + 'a, ()> {
+        self.query_iter(source, |lang| loader.tag_query(lang), range)
+    }
+
     pub fn rainbow_highlights(
         &self,
         source: RopeSlice,
--- a/helix-loader/src/lib.rs	Tue May 13 20:10:14 2025 -0400
+++ b/helix-loader/src/lib.rs	Thu Feb 27 11:51:12 2025 -0500
@@ -244,7 +244,12 @@
 /// Otherwise (workspace, false) is returned
 pub fn find_workspace() -> (PathBuf, bool) {
     let current_dir = current_working_dir();
-    for ancestor in current_dir.ancestors() {
+    find_workspace_in(current_dir)
+}
+
+pub fn find_workspace_in(dir: impl AsRef<Path>) -> (PathBuf, bool) {
+    let dir = dir.as_ref();
+    for ancestor in dir.ancestors() {
         if ancestor.join(".git").exists()
             || ancestor.join(".hg").exists()
             || ancestor.join(".svn").exists()
@@ -255,7 +260,7 @@
         }
     }
 
-    (current_dir, true)
+    (dir.to_owned(), true)
 }
 
 fn default_config_file() -> PathBuf {
--- a/helix-term/Cargo.toml	Tue May 13 20:10:14 2025 -0400
+++ b/helix-term/Cargo.toml	Thu Feb 27 11:51:12 2025 -0500
@@ -92,6 +92,8 @@
 grep-regex = "0.1.13"
 grep-searcher = "0.1.14"
 
+dashmap = "6.0"
+
 [target.'cfg(not(windows))'.dependencies]  # https://github.com/vorner/signal-hook/issues/100
 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
 libc = "0.2.172"
--- a/helix-term/src/commands.rs	Tue May 13 20:10:14 2025 -0400
+++ b/helix-term/src/commands.rs	Thu Feb 27 11:51:12 2025 -0500
@@ -1,5 +1,6 @@
 pub(crate) mod dap;
 pub(crate) mod lsp;
+pub(crate) mod syntax;
 pub(crate) mod typed;
 
 pub use dap::*;
@@ -11,6 +12,7 @@
 };
 use helix_vcs::{FileChange, Hunk};
 pub use lsp::*;
+pub use syntax::*;
 use tui::{
     text::{Span, Spans},
     widgets::Cell,
@@ -602,6 +604,10 @@
         extend_to_word, "Extend to a two-character label",
         goto_next_tabstop, "goto next snippet placeholder",
         goto_prev_tabstop, "goto next snippet placeholder",
+        syntax_symbol_picker, "Open symbol picker from syntax information",
+        syntax_workspace_symbol_picker, "Open workspace symbol picker from syntax information",
+        lsp_or_syntax_symbol_picker, "Open symbol picker from LSP or syntax information",
+        lsp_or_syntax_workspace_symbol_picker, "Open workspace symbol picker from LSP or syntax information",
     );
 }
 
@@ -6881,3 +6887,34 @@
     }
     jump_to_label(cx, words, behaviour)
 }
+
+fn lsp_or_syntax_symbol_picker(cx: &mut Context) {
+    let doc = doc!(cx.editor);
+
+    if doc
+        .language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
+        .next()
+        .is_some()
+    {
+        lsp::symbol_picker(cx);
+    } else if doc.syntax().is_some() {
+        syntax_symbol_picker(cx);
+    } else {
+        cx.editor
+            .set_error("No language server supporting document symbols or syntax info available");
+    }
+}
+
+fn lsp_or_syntax_workspace_symbol_picker(cx: &mut Context) {
+    let doc = doc!(cx.editor);
+
+    if doc
+        .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
+        .next()
+        .is_some()
+    {
+        lsp::workspace_symbol_picker(cx);
+    } else {
+        syntax_workspace_symbol_picker(cx);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/helix-term/src/commands/syntax.rs	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,441 @@
+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, PartialOrd, Ord)]
+enum TagKind {
+    Function,
+    Macro,
+    Module,
+    Constant,
+    Struct,
+    Interface,
+    Type,
+    Class,
+}
+
+impl TagKind {
+    fn as_str(&self) -> &'static str {
+        match self {
+            Self::Function => "function",
+            Self::Macro => "macro",
+            Self::Module => "module",
+            Self::Constant => "constant",
+            Self::Struct => "struct",
+            Self::Interface => "interface",
+            Self::Type => "type",
+            Self::Class => "class",
+        }
+    }
+
+    fn from_name(name: &str) -> Option<Self> {
+        match name {
+            "function" => Some(TagKind::Function),
+            "macro" => Some(TagKind::Macro),
+            "module" => Some(TagKind::Module),
+            "constant" => Some(TagKind::Constant),
+            "struct" => Some(TagKind::Struct),
+            "interface" => Some(TagKind::Interface),
+            "type" => Some(TagKind::Type),
+            "class" => Some(TagKind::Class),
+            _ => 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");
+        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,
+        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 state = SearchState {
+        searcher_builder,
+        walk_builder,
+        regex_matcher_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 rope::Regex::new(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::from(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))
+}
--- a/helix-term/src/keymap/default.rs	Tue May 13 20:10:14 2025 -0400
+++ b/helix-term/src/keymap/default.rs	Thu Feb 27 11:51:12 2025 -0500
@@ -227,8 +227,8 @@
             "E" => file_explorer_in_current_buffer_directory,
             "b" => buffer_picker,
             "j" => jumplist_picker,
-            "s" => symbol_picker,
-            "S" => workspace_symbol_picker,
+            "s" => lsp_or_syntax_symbol_picker,
+            "S" => lsp_or_syntax_workspace_symbol_picker,
             "d" => diagnostics_picker,
             "D" => workspace_diagnostics_picker,
             "g" => document_change_picker,
--- a/languages.toml	Tue May 13 20:10:14 2025 -0400
+++ b/languages.toml	Thu Feb 27 11:51:12 2025 -0500
@@ -1986,7 +1986,10 @@
 shebangs = ["escript"]
 comment-token = "%%"
 indent = { tab-width = 4, unit = "    " }
-language-servers = [ "erlang-ls", "elp" ]
+language-servers = [
+  { name = "erlang-ls", except-features = ["document-symbols", "workspace-symbols"] },
+  { name = "elp", except-features = ["document-symbols", "workspace-symbols"] }
+]
 
 [[grammar]]
 name = "erlang"
--- a/runtime/queries/_javascript/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-(
-  (comment)* @doc
-  .
-  (method_definition
-    name: (property_identifier) @name) @definition.method
-  (#not-eq? @name "constructor")
-  (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
-  (#select-adjacent! @doc @definition.method)
-)
-
-(
-  (comment)* @doc
-  .
-  [
-    (class
-      name: (_) @name)
-    (class_declaration
-      name: (_) @name)
-  ] @definition.class
-  (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
-  (#select-adjacent! @doc @definition.class)
-)
-
-(
-  (comment)* @doc
-  .
-  [
-    (function
-      name: (identifier) @name)
-    (function_declaration
-      name: (identifier) @name)
-    (generator_function
-      name: (identifier) @name)
-    (generator_function_declaration
-      name: (identifier) @name)
-  ] @definition.function
-  (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
-  (#select-adjacent! @doc @definition.function)
-)
-
-(
-  (comment)* @doc
-  .
-  (lexical_declaration
-    (variable_declarator
-      name: (identifier) @name
-      value: [(arrow_function) (function)]) @definition.function)
-  (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
-  (#select-adjacent! @doc @definition.function)
-)
-
-(
-  (comment)* @doc
-  .
-  (variable_declaration
-    (variable_declarator
-      name: (identifier) @name
-      value: [(arrow_function) (function)]) @definition.function)
-  (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
-  (#select-adjacent! @doc @definition.function)
-)
-
-(assignment_expression
-  left: [
-    (identifier) @name
-    (member_expression
-      property: (property_identifier) @name)
-  ]
-  right: [(arrow_function) (function)]
-) @definition.function
-
-(pair
-  key: (property_identifier) @name
-  value: [(arrow_function) (function)]) @definition.function
-
-(
-  (call_expression
-    function: (identifier) @name) @reference.call
-  (#not-match? @name "^(require)$")
-)
-
-(call_expression
-  function: (member_expression
-    property: (property_identifier) @name)
-  arguments: (_) @reference.call)
-
-(new_expression
-  constructor: (_) @name) @reference.class
--- a/runtime/queries/_typescript/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-(function_signature
-  name: (identifier) @name) @definition.function
-
-(method_signature
-  name: (property_identifier) @name) @definition.method
-
-(abstract_method_signature
-  name: (property_identifier) @name) @definition.method
-
-(abstract_class_declaration
-  name: (type_identifier) @name) @definition.class
-
-(module
-  name: (identifier) @name) @definition.module
-
-(interface_declaration
-  name: (type_identifier) @name) @definition.interface
-
-(type_annotation
-  (type_identifier) @name) @reference.type
-
-(new_expression
-  constructor: (identifier) @name) @reference.class
--- a/runtime/queries/c-sharp/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-(class_declaration name: (identifier) @name) @definition.class
-
-(class_declaration (base_list (_) @name)) @reference.class
-
-(interface_declaration name: (identifier) @name) @definition.interface
-
-(interface_declaration (base_list (_) @name)) @reference.interface
-
-(method_declaration name: (identifier) @name) @definition.method
-
-(object_creation_expression type: (identifier) @name) @reference.class
-
-(type_parameter_constraints_clause (identifier) @name) @reference.class
-
-(type_parameter_constraint (type type: (identifier) @name)) @reference.class
-
-(variable_declaration type: (identifier) @name) @reference.class
-
-(invocation_expression function: (member_access_expression name: (identifier) @name)) @reference.send
-
-(namespace_declaration name: (identifier) @name) @definition.module
-
-(namespace_declaration name: (identifier) @name) @module
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/c/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,9 @@
+(function_declarator
+  declarator: [(identifier) (field_identifier)] @definition.function)
+
+(preproc_function_def name: (identifier) @definition.function)
+
+(type_definition
+  declarator: (type_identifier) @definition.type)
+
+(preproc_def name: (identifier) @definition.constant)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/cpp/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,12 @@
+; inherits: c
+
+(function_declarator
+  declarator: (qualified_identifier name: (identifier) @definition.function))
+
+(struct_specifier
+  name: (type_identifier) @definition.struct
+  body: (field_declaration_list))
+
+(class_specifier
+  name: (type_identifier) @definition.class
+  body: (field_declaration_list))
--- a/runtime/queries/elisp/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-;; defun/defsubst
-(function_definition name: (symbol) @name) @definition.function
-
-;; Treat macros as function definitions for the sake of TAGS.
-(macro_definition name: (symbol) @name) @definition.function
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/elixir/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,10 @@
+((call
+   target: (identifier) @_keyword
+   (arguments
+     [
+       (call target: (identifier) @definition.function)
+       ; function has a guard
+       (binary_operator
+         left: (call target: (identifier) @definition.function))
+     ]))
+ (#any-of? @_keyword "def" "defdelegate" "defguard" "defguardp" "defmacro" "defmacrop" "defn" "defnp" "defp"))
--- a/runtime/queries/elm/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-(value_declaration (function_declaration_left (lower_case_identifier) @name)) @definition.function
-
-(function_call_expr (value_expr (value_qid) @name)) @reference.function
-(exposed_value (lower_case_identifier) @name) @reference.function
-(type_annotation ((lower_case_identifier) @name) (colon)) @reference.function
-
-(type_declaration ((upper_case_identifier) @name) ) @definition.type
-
-(type_ref (upper_case_qid (upper_case_identifier) @name)) @reference.type
-(exposed_type (upper_case_identifier) @name) @reference.type
-
-(type_declaration (union_variant (upper_case_identifier) @name)) @definition.union
-
-(value_expr (upper_case_qid (upper_case_identifier) @name)) @reference.union
-
-
-(module_declaration 
-    (upper_case_qid (upper_case_identifier)) @name
-) @definition.module
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/erlang/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,45 @@
+; Modules
+(attribute
+  name: (atom) @_attr
+  (arguments (atom) @definition.module)
+ (#eq? @_attr "module"))
+
+; Constants
+((attribute
+    name: (atom) @_attr
+    (arguments
+      .
+      [
+        (atom) @definition.constant
+        (call function: [(variable) (atom)] @definition.macro)
+      ]))
+ (#eq? @_attr "define"))
+
+; Record definitions
+((attribute
+   name: (atom) @_attr
+   (arguments
+     .
+     (atom) @definition.struct))
+ (#eq? @_attr "record"))
+
+; Function specs
+((attribute
+    name: (atom) @_attr
+    (stab_clause name: (atom) @definition.interface))
+ (#eq? @_attr "spec"))
+
+; Types
+((attribute
+    name: (atom) @_attr
+    (arguments
+      (binary_operator
+        left: [
+          (atom) @definition.type
+          (call function: (atom) @definition.type)
+        ]
+        operator: "::")))
+ (#any-of? @_attr "type" "opaque"))
+
+; Functions
+(function_clause name: (atom) @definition.function)
--- a/runtime/queries/gdscript/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-(class_definition (name) @name) @definition.class
-
-(function_definition (name) @name) @definition.function
-
-(call (name) @name) @reference.call
\ No newline at end of file
--- a/runtime/queries/gjs/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-; inherits: _gjs,_javascript,ecma
--- a/runtime/queries/go/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-(
-  (comment)* @doc
-  .
-  (function_declaration
-    name: (identifier) @name) @definition.function
-  (#strip! @doc "^//\\s*")
-  (#set-adjacent! @doc @definition.function)
-)
-
-(
-  (comment)* @doc
-  .
-  (method_declaration
-    name: (field_identifier) @name) @definition.method
-  (#strip! @doc "^//\\s*")
-  (#set-adjacent! @doc @definition.method)
-)
-
-(call_expression
-  function: [
-    (identifier) @name
-    (parenthesized_expression (identifier) @name)
-    (selector_expression field: (field_identifier) @name)
-    (parenthesized_expression (selector_expression field: (field_identifier) @name))
-  ]) @reference.call
-
-(type_spec
-  name: (type_identifier) @name) @definition.type
-
-(type_identifier) @name @reference.type
--- a/runtime/queries/gts/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-; inherits: _gjs,_typescript,ecma
--- a/runtime/queries/javascript/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-; See runtime/queries/ecma/README.md for more info.
-
-; inherits: _javascript,ecma
--- a/runtime/queries/jsx/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-; See runtime/queries/ecma/README.md for more info.
-
-; inherits: _jsx,_javascript,ecma
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/markdown/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,2 @@
+; TODO: have symbol types for markup?
+(atx_heading) @definition.class
--- a/runtime/queries/php-only/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-(namespace_definition
-  name: (namespace_name) @name) @module
-
-(interface_declaration
-  name: (name) @name) @definition.interface
-
-(trait_declaration
-  name: (name) @name) @definition.interface
-
-(class_declaration
-  name: (name) @name) @definition.class
-
-(class_interface_clause [(name) (qualified_name)] @name) @impl
-
-(property_declaration
-  (property_element (variable_name (name) @name))) @definition.field
-
-(function_definition
-  name: (name) @name) @definition.function
-
-(method_declaration
-  name: (name) @name) @definition.function
-
-(object_creation_expression
-  [
-    (qualified_name (name) @name)
-    (variable_name (name) @name)
-  ]) @reference.class
-
-(function_call_expression
-  function: [
-    (qualified_name (name) @name)
-    (variable_name (name)) @name
-  ]) @reference.call
-
-(scoped_call_expression
-  name: (name) @name) @reference.call
-
-(member_call_expression
-  name: (name) @name) @reference.call
--- a/runtime/queries/php/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-(class_declaration
-  name: (name) @name) @definition.class
-
-(function_definition
-  name: (name) @name) @definition.function
-
-(method_declaration
-  name: (name) @name) @definition.function
-
-(object_creation_expression
-  [
-    (qualified_name (name) @name)
-    (variable_name (name) @name)
-  ]) @reference.class
-
-(function_call_expression
-  function: [
-    (qualified_name (name) @name)
-    (variable_name (name)) @name
-  ]) @reference.call
-
-(scoped_call_expression
-  name: (name) @name) @reference.call
-
-(member_call_expression
-  name: (name) @name) @reference.call
--- a/runtime/queries/python/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ b/runtime/queries/python/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -1,12 +1,5 @@
-(class_definition
-  name: (identifier) @name) @definition.class
-
 (function_definition
-  name: (identifier) @name) @definition.function
+  name: (identifier) @definition.function)
 
-(call
-  function: [
-      (identifier) @name
-      (attribute
-        attribute: (identifier) @name)
-  ]) @reference.call
+(class_definition
+  name: (identifier) @definition.class)
--- a/runtime/queries/ruby/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-; Method definitions
-
-(
-  (comment)* @doc
-  .
-  [
-    (method
-      name: (_) @name) @definition.method
-    (singleton_method
-      name: (_) @name) @definition.method
-  ]
-  (#strip! @doc "^#\\s*")
-  (#select-adjacent! @doc @definition.method)
-)
-
-(alias
-  name: (_) @name) @definition.method
-
-(setter
-  (identifier) @ignore)
-
-; Class definitions
-
-(
-  (comment)* @doc
-  .
-  [
-    (class
-      name: [
-        (constant) @name
-        (scope_resolution
-          name: (_) @name)
-      ]) @definition.class
-    (singleton_class
-      value: [
-        (constant) @name
-        (scope_resolution
-          name: (_) @name)
-      ]) @definition.class
-  ]
-  (#strip! @doc "^#\\s*")
-  (#select-adjacent! @doc @definition.class)
-)
-
-; Module definitions
-
-(
-  (module
-    name: [
-      (constant) @name
-      (scope_resolution
-        name: (_) @name)
-    ]) @definition.module
-)
-
-; Calls
-
-(call method: (identifier) @name) @reference.call
-
-(
-  [(identifier) (constant)] @name @reference.call
-  (#is-not? local)
-  (#not-match? @name "^(lambda|load|require|require_relative|__FILE__|__LINE__)$")
-)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/rust/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,26 @@
+(struct_item
+  name: (type_identifier) @definition.struct)
+
+(const_item
+  name: (identifier) @definition.constant)
+
+(trait_item
+  name: (type_identifier) @definition.interface)
+
+(function_item
+  name: (identifier) @definition.function)
+
+(function_signature_item
+  name: (identifier) @definition.function)
+
+(enum_item
+  name: (type_identifier) @definition.type)
+
+(enum_variant
+  name: (identifier) @definition.struct)
+
+(mod_item
+  name: (identifier) @definition.module)
+
+(macro_definition
+  name: (identifier) @definition.macro)
--- a/runtime/queries/spicedb/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-(object_definition
-  name: (type_identifier) @name) @definition.type
-
-(type_identifier) @name @reference.type
--- a/runtime/queries/tsx/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-; See runtime/queries/ecma/README.md for more info.
-
-; inherits: _jsx,_typescript,ecma
--- a/runtime/queries/typescript/tags.scm	Tue May 13 20:10:14 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-; See runtime/queries/ecma/README.md for more info.
-
-; inherits: _typescript,ecma
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/queries/typst/tags.scm	Thu Feb 27 11:51:12 2025 -0500
@@ -0,0 +1,6 @@
+; should be a heading
+(heading (text) @definition.class)
+
+; should be a label/reference/tag
+(heading (label) @definition.function )
+(content (label) @definition.function)
--- a/xtask/src/main.rs	Tue May 13 20:10:14 2025 -0400
+++ b/xtask/src/main.rs	Thu Feb 27 11:51:12 2025 -0500
@@ -37,6 +37,7 @@
             LanguageData::compile_indent_query(grammar, config)?;
             LanguageData::compile_textobject_query(grammar, config)?;
             LanguageData::compile_rainbow_query(grammar, config)?;
+            LanguageData::compile_tag_query(grammar, config)?;
         }
 
         println!("Query check succeeded");