gpui-component 0.6.1

GPUI Component: the styled component library of GPUI Kit, with 60+ desktop UI components for GPUI.
Documentation
use std::rc::Rc;

#[cfg(feature = "tree-sitter")]
use gpui_base::input::Rope;
use gpui_base::input::SyntaxContextProvider;

/// A syntax-context provider for `language`, or `None` when no grammar is
/// compiled in (unknown language, plain text, or feature disabled).
#[cfg(feature = "tree-sitter")]
pub(crate) fn syntax_context_provider(language: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
    use crate::highlighter::LanguageRegistry;

    let config = LanguageRegistry::singleton().language(language)?;
    let grammar = config.language?;
    TreeSitterSyntaxContext::new(grammar)
        .map(|provider| Rc::new(provider) as Rc<dyn SyntaxContextProvider>)
}

/// No tree-sitter support: no provider, engine falls back to heuristics.
#[cfg(not(feature = "tree-sitter"))]
pub(crate) fn syntax_context_provider(_language: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
    None
}

/// Tree-sitter backed syntax context for editing decisions.
///
/// Caches the tree for unchanged text and classifies an offset by
/// walking the named node and its ancestors for `string` / `comment` kinds.
/// Generic across grammars: no per-language queries needed for this coarse
/// classification.
#[cfg(feature = "tree-sitter")]
struct TreeSitterSyntaxContext {
    parser: std::cell::RefCell<tree_sitter::Parser>,
    tree: std::cell::RefCell<Option<(String, tree_sitter::Tree)>>,
}

#[cfg(feature = "tree-sitter")]
impl TreeSitterSyntaxContext {
    /// Synchronous parse budget per query; mirrors the highlighter's sync path.
    const PARSE_BUDGET: std::time::Duration = std::time::Duration::from_millis(5);

    fn new(language: tree_sitter::Language) -> Option<Self> {
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&language).ok()?;
        Some(Self {
            parser: std::cell::RefCell::new(parser),
            tree: std::cell::RefCell::new(None),
        })
    }

    fn classify(kind: &str) -> Option<gpui_base::input::SyntaxContext> {
        let lower = kind.to_lowercase();
        if lower == "interpolation" || lower == "template_substitution" {
            Some(gpui_base::input::SyntaxContext::Code)
        } else if lower.contains("comment") {
            Some(gpui_base::input::SyntaxContext::Comment)
        } else if lower.contains("string") {
            Some(gpui_base::input::SyntaxContext::String)
        } else {
            None
        }
    }
}

#[cfg(feature = "tree-sitter")]
impl SyntaxContextProvider for TreeSitterSyntaxContext {
    fn context_at(&self, text: &Rope, offset: usize) -> gpui_base::input::SyntaxContext {
        use std::ops::ControlFlow;

        let source = text.to_string();
        let offset = offset.min(source.len());
        let mut cached = self.tree.borrow_mut();
        if cached
            .as_ref()
            .is_none_or(|(previous, _)| previous != &source)
        {
            let start = std::time::Instant::now();
            let mut progress = |_: &tree_sitter::ParseState| -> ControlFlow<()> {
                if start.elapsed() > Self::PARSE_BUDGET {
                    ControlFlow::Break(())
                } else {
                    ControlFlow::Continue(())
                }
            };
            let options = tree_sitter::ParseOptions::new().progress_callback(&mut progress);
            let mut parser = self.parser.borrow_mut();
            // No InputEdit is available here, so an old tree cannot be reused
            // after the source changes.
            let tree = parser.parse_with_options(
                &mut |byte_offset, _| source.get(byte_offset..).unwrap_or(""),
                None,
                Some(options),
            );
            let Some(tree) = tree else {
                // A timed-out parse must not resume against a different source.
                parser.reset();
                *cached = None;
                return gpui_base::input::SyntaxContext::Code;
            };
            *cached = Some((source, tree));
        }
        let (source, tree) = cached.as_ref().unwrap();
        let mut node = tree.root_node().descendant_for_byte_range(offset, offset);
        // At the very end the range may match nothing; try the last byte.
        if node.is_none() && offset > 0 {
            node = tree
                .root_node()
                .descendant_for_byte_range(offset - 1, offset - 1);
        }
        let mut current = node;
        while let Some(n) = current {
            if n.is_named() {
                if let Some(context) = Self::classify(n.kind()) {
                    // String delimiters delimit the scope: inserting at its start
                    // is outside, while inserting before its end is inside.
                    if context == gpui_base::input::SyntaxContext::String
                        && offset == n.start_byte()
                        && matches!(
                            n.kind(),
                            "string"
                                | "string_literal"
                                | "raw_string_literal"
                                | "template_string"
                                | "string_start"
                        )
                    {
                        return gpui_base::input::SyntaxContext::Code;
                    }
                    return context;
                }
            }
            current = n.parent();
        }
        // A zero-width query at a line comment's end may select the enclosing
        // node. Insertion there still extends the comment, unlike insertion
        // after a block comment's closing delimiter.
        if offset > 0 && !matches!(source.as_bytes()[offset - 1], b'\n' | b'\r') {
            let mut previous = tree
                .root_node()
                .descendant_for_byte_range(offset - 1, offset);
            while let Some(node) = previous {
                if node.end_byte() == offset
                    && Self::classify(node.kind()) == Some(gpui_base::input::SyntaxContext::Comment)
                {
                    let comment = &source[node.start_byte()..node.end_byte()];
                    if node.kind() == "line_comment"
                        || (node.kind() == "comment"
                            && (comment.starts_with('#') || comment.starts_with("//")))
                    {
                        return gpui_base::input::SyntaxContext::Comment;
                    }
                }
                previous = node.parent();
            }
        }
        gpui_base::input::SyntaxContext::Code
    }
}

#[cfg(all(test, feature = "tree-sitter"))]
mod tests {
    use super::*;

    fn json_provider() -> Rc<dyn SyntaxContextProvider> {
        syntax_context_provider("json").expect("json grammar must be compiled in")
    }

    #[cfg(all(feature = "tree-sitter-python", feature = "tree-sitter-rust"))]
    #[test]
    fn comment_end_tracks_insertion_context() {
        use gpui_base::input::{Rope, SyntaxContext};
        for (language, source, offset, expected) in [
            ("python", "# note", 6, SyntaxContext::Comment),
            ("python", "# note\n", 6, SyntaxContext::Comment),
            ("python", "# note\n", 7, SyntaxContext::Code),
            ("rust", "// note", 7, SyntaxContext::Comment),
            ("rust", "/// note\n", 8, SyntaxContext::Comment),
            ("rust", "/// note\n", 9, SyntaxContext::Code),
            ("rust", "/* note */", 10, SyntaxContext::Code),
        ] {
            let provider = syntax_context_provider(language).unwrap();
            assert_eq!(
                provider.context_at(&Rope::from_str(source), offset),
                expected,
                "{source:?} at {offset}"
            );
        }
    }

    #[cfg(feature = "tree-sitter-cmake")]
    #[test]
    fn closed_bracket_comment_is_not_a_line_comment() {
        use gpui_base::input::{Rope, SyntaxContext};
        let provider = syntax_context_provider("cmake").unwrap();
        let text = Rope::from_str("#[[ note ]]");
        assert_eq!(provider.context_at(&text, text.len()), SyntaxContext::Code);
    }

    #[test]
    fn json_string_content_is_string_context() {
        use gpui_base::input::{Rope, SyntaxContext};

        let provider = json_provider();
        // {"key": "value"} — offset inside "value".
        let text = Rope::from_str(r#"{"key": "value"}"#);
        assert_eq!(
            provider.context_at(&text, 10),
            SyntaxContext::String,
            "inside string literal"
        );
    }

    #[test]
    fn json_punctuation_is_code_context() {
        use gpui_base::input::{Rope, SyntaxContext};

        let provider = json_provider();
        let text = Rope::from_str(r#"{"key": "value"}"#);
        assert_eq!(
            provider.context_at(&text, 0),
            SyntaxContext::Code,
            "opening brace"
        );
        assert_eq!(
            provider.context_at(&text, 7),
            SyntaxContext::Code,
            "colon between pairs"
        );
    }

    #[test]
    fn test_changed_source_matches_fresh_provider() {
        use gpui_base::input::{Rope, SyntaxContext};
        let provider = json_provider();
        let old = Rope::from_str(r#"{"key": "value"}"#);
        assert_eq!(provider.context_at(&old, 10), SyntaxContext::String);
        let new = Rope::from_str(r#"{"key": 1234567}"#);
        let fresh = json_provider().context_at(&new, 10);
        assert_eq!(fresh, SyntaxContext::Code);
        assert_eq!(provider.context_at(&new, 10), fresh);
    }

    #[test]
    fn unknown_language_has_no_provider() {
        assert!(syntax_context_provider("not-a-language").is_none());
    }
}