Skip to main content

reovim_driver_syntax/
highlight.rs

1//! Syntax highlight categories and spans.
2//!
3//! This module defines the highlight types used by syntax drivers.
4//!
5//! # Annotation Model (#540, #551)
6//!
7//! The [`HighlightCategory`] type is an open string-based category.
8//! Any provider can emit any category. Well-known categories exist
9//! as constants for convenience, not constraint.
10//!
11//! [`Annotation`] is a pure semantic marker: byte range + category.
12//! The server emits only WHAT to annotate; clients decide HOW to render.
13
14use std::{fmt, ops::Range, sync::Arc};
15
16// ============================================================================
17// Annotation Model (#540)
18// ============================================================================
19
20/// Interned string-based highlight category.
21///
22/// Any provider can emit any category. Well-known categories exist
23/// as constants for convenience, not constraint.
24///
25/// Uses `Arc<str>` for cheap cloning (O(1) vs `String`'s O(n)).
26/// Different `Arc<str>` instances with the same content compare
27/// equal via the [`PartialEq`] impl on the underlying `str`.
28///
29/// # Example
30///
31/// ```
32/// use reovim_driver_syntax::HighlightCategory;
33///
34/// let cat = HighlightCategory::new("keyword.function");
35/// assert_eq!(cat.as_str(), "keyword.function");
36///
37/// // Well-known constants
38/// let kw = HighlightCategory::new(HighlightCategory::KEYWORD);
39/// assert_eq!(kw.as_str(), "keyword");
40/// ```
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub struct HighlightCategory(Arc<str>);
43
44impl HighlightCategory {
45    /// Create a new highlight category from a string.
46    #[must_use]
47    pub fn new(s: impl Into<Arc<str>>) -> Self {
48        Self(s.into())
49    }
50
51    /// Get the category string.
52    #[must_use]
53    pub fn as_str(&self) -> &str {
54        &self.0
55    }
56}
57
58impl std::fmt::Display for HighlightCategory {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str(&self.0)
61    }
62}
63
64/// Well-known category constants.
65///
66/// Use these for interop and readability — they are not a closed set.
67impl HighlightCategory {
68    // Keywords
69    pub const KEYWORD: &str = "keyword";
70    pub const KEYWORD_CONTROL: &str = "keyword.control";
71    pub const KEYWORD_OPERATOR: &str = "keyword.operator";
72    pub const KEYWORD_FUNCTION: &str = "keyword.function";
73    pub const KEYWORD_TYPE: &str = "keyword.type";
74
75    // Types
76    pub const TYPE: &str = "type";
77    pub const TYPE_BUILTIN: &str = "type.builtin";
78
79    // Functions
80    pub const FUNCTION: &str = "function";
81    pub const FUNCTION_BUILTIN: &str = "function.builtin";
82    pub const FUNCTION_MACRO: &str = "function.macro";
83    pub const FUNCTION_METHOD: &str = "function.method";
84
85    // Variables
86    pub const VARIABLE: &str = "variable";
87    pub const VARIABLE_BUILTIN: &str = "variable.builtin";
88    pub const VARIABLE_PARAMETER: &str = "variable.parameter";
89    pub const VARIABLE_FIELD: &str = "variable.field";
90    pub const CONSTANT: &str = "constant";
91
92    // Literals
93    pub const STRING: &str = "string";
94    pub const STRING_ESCAPE: &str = "string.escape";
95    pub const CHARACTER: &str = "character";
96    pub const NUMBER: &str = "number";
97    pub const BOOLEAN: &str = "boolean";
98
99    // Comments
100    pub const COMMENT: &str = "comment";
101    pub const COMMENT_DOC: &str = "comment.doc";
102
103    // Punctuation
104    pub const PUNCTUATION: &str = "punctuation";
105    pub const PUNCTUATION_BRACKET: &str = "punctuation.bracket";
106    pub const PUNCTUATION_DELIMITER: &str = "punctuation.delimiter";
107
108    // Operators
109    pub const OPERATOR: &str = "operator";
110
111    // Diagnostics
112    pub const DIAGNOSTIC_ERROR: &str = "diagnostic.error";
113    pub const DIAGNOSTIC_WARNING: &str = "diagnostic.warning";
114    pub const DIAGNOSTIC_INFO: &str = "diagnostic.info";
115    pub const DIAGNOSTIC_HINT: &str = "diagnostic.hint";
116
117    // Namespace / Constructor / Label / Attribute / Tag
118    pub const NAMESPACE: &str = "namespace";
119    pub const CONSTRUCTOR: &str = "constructor";
120    pub const LABEL: &str = "label";
121    pub const ATTRIBUTE: &str = "attribute";
122    pub const TAG: &str = "tag";
123
124    // Markup
125    pub const MARKUP_HEADING: &str = "markup.heading";
126    pub const MARKUP_BOLD: &str = "markup.bold";
127    pub const MARKUP_ITALIC: &str = "markup.italic";
128    pub const MARKUP_STRIKETHROUGH: &str = "markup.strikethrough";
129    pub const MARKUP_LINK: &str = "markup.link";
130    pub const MARKUP_LINK_URL: &str = "markup.link.url";
131    pub const MARKUP_LIST: &str = "markup.list";
132    pub const MARKUP_RAW: &str = "markup.raw";
133    pub const MARKUP_RAW_INLINE: &str = "markup.raw.inline";
134
135    // Embedded / Special
136    pub const EMBEDDED: &str = "embedded";
137    pub const SPECIAL: &str = "special";
138}
139
140/// A single annotation on a buffer range.
141///
142/// Annotations are the unified representation for all semantic markup:
143/// syntax highlights, decorations, diagnostics, search matches, etc.
144///
145/// The server emits only byte ranges and categories (mechanism).
146/// Clients decide how to render each category (policy).
147///
148/// # Example
149///
150/// ```
151/// use reovim_driver_syntax::{Annotation, HighlightCategory};
152///
153/// let ann = Annotation::new(0, 5, HighlightCategory::new("keyword.function"));
154/// assert_eq!(ann.category.as_str(), "keyword.function");
155/// assert_eq!(ann.len(), 5);
156/// ```
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct Annotation {
159    /// Start byte offset (inclusive).
160    pub start_byte: usize,
161    /// End byte offset (exclusive).
162    pub end_byte: usize,
163    /// Category string (e.g., "keyword.function", "markup.heading.1").
164    pub category: HighlightCategory,
165}
166
167impl Annotation {
168    /// Create an annotation.
169    #[must_use]
170    pub const fn new(start_byte: usize, end_byte: usize, category: HighlightCategory) -> Self {
171        Self {
172            start_byte,
173            end_byte,
174            category,
175        }
176    }
177
178    /// Get the length in bytes.
179    #[must_use]
180    pub const fn len(&self) -> usize {
181        self.end_byte - self.start_byte
182    }
183
184    /// Check if the annotation is empty.
185    #[must_use]
186    pub const fn is_empty(&self) -> bool {
187        self.start_byte == self.end_byte
188    }
189
190    /// Check if this annotation overlaps with a byte range.
191    #[must_use]
192    pub const fn overlaps(&self, range: &Range<usize>) -> bool {
193        self.start_byte < range.end && self.end_byte > range.start
194    }
195
196    /// Check if this annotation contains a byte offset.
197    #[must_use]
198    pub const fn contains(&self, byte: usize) -> bool {
199        self.start_byte <= byte && byte < self.end_byte
200    }
201
202    /// Get this annotation as a byte range.
203    #[must_use]
204    pub const fn byte_range(&self) -> Range<usize> {
205        self.start_byte..self.end_byte
206    }
207}
208
209// ============================================================================
210// Syntax Context
211// ============================================================================
212
213/// Syntax context at a byte position.
214///
215/// Used by consumers (e.g., auto-pair) to determine if a position
216/// is inside a string literal, comment, or normal code.
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum SyntaxContext {
219    /// Normal code (default for unparsed content).
220    Code,
221    /// Inside a string literal.
222    String,
223    /// Inside a comment.
224    Comment,
225}
226
227#[cfg_attr(coverage_nightly, coverage(off))]
228impl fmt::Display for SyntaxContext {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            Self::Code => f.write_str("code"),
232            Self::String => f.write_str("string"),
233            Self::Comment => f.write_str("comment"),
234        }
235    }
236}
237
238#[cfg(test)]
239#[path = "highlight_tests.rs"]
240mod tests;