use std::rc::Rc;
#[cfg(feature = "tree-sitter")]
use gpui_base::input::Rope;
use gpui_base::input::SyntaxContextProvider;
#[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>)
}
#[cfg(not(feature = "tree-sitter"))]
pub(crate) fn syntax_context_provider(_language: &str) -> Option<Rc<dyn SyntaxContextProvider>> {
None
}
#[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 {
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();
let tree = parser.parse_with_options(
&mut |byte_offset, _| source.get(byte_offset..).unwrap_or(""),
None,
Some(options),
);
let Some(tree) = tree else {
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);
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()) {
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();
}
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();
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());
}
}