Skip to main content

fresh_core/
text_property.rs

1//! Text properties for embedding metadata in text ranges
2//!
3//! This module provides Emacs-style text properties that allow embedding
4//! arbitrary metadata (like source locations, severity levels, etc.) in
5//! specific ranges of text. This is essential for virtual buffers where
6//! each line might represent a diagnostic, search result, or other structured data.
7
8use crate::api::OverlayOptions;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::ops::Range;
12
13/// A text property that associates metadata with a range of text
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
15#[ts(export)]
16pub struct TextProperty {
17    /// Start byte offset (inclusive)
18    pub start: usize,
19    /// End byte offset (exclusive)
20    pub end: usize,
21    /// Arbitrary properties as key-value pairs
22    #[ts(type = "Record<string, any>")]
23    pub properties: HashMap<String, serde_json::Value>,
24}
25
26impl TextProperty {
27    /// Create a new text property for a range
28    pub fn new(start: usize, end: usize) -> Self {
29        Self {
30            start,
31            end,
32            properties: HashMap::new(),
33        }
34    }
35
36    /// Add a property
37    pub fn with_property(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
38        self.properties.insert(key.into(), value);
39        self
40    }
41
42    /// Set multiple properties at once
43    pub fn with_properties(mut self, props: HashMap<String, serde_json::Value>) -> Self {
44        self.properties.extend(props);
45        self
46    }
47
48    /// Check if this property range contains a byte position
49    pub fn contains(&self, pos: usize) -> bool {
50        pos >= self.start && pos < self.end
51    }
52
53    /// Check if this property range overlaps with another range
54    pub fn overlaps(&self, range: &Range<usize>) -> bool {
55        self.start < range.end && self.end > range.start
56    }
57
58    /// Get a property value by key
59    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
60        self.properties.get(key)
61    }
62
63    /// Get a property as a specific type
64    pub fn get_as<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
65        self.properties
66            .get(key)
67            .and_then(|v| serde_json::from_value(v.clone()).ok())
68    }
69}
70
71/// An inline overlay specifying styling for a sub-range within a text entry
72#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
73#[serde(rename_all = "camelCase")]
74#[ts(export, rename_all = "camelCase")]
75pub struct InlineOverlay {
76    /// Start byte offset within the entry's text
77    pub start: usize,
78    /// End byte offset within the entry's text (exclusive)
79    pub end: usize,
80    /// Styling options for this range
81    #[ts(type = "Partial<OverlayOptions>")]
82    pub style: OverlayOptions,
83    /// Optional properties for this sub-range (e.g., click target metadata)
84    #[ts(type = "Record<string, any>")]
85    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
86    pub properties: HashMap<String, serde_json::Value>,
87}
88
89/// An entry with text and its properties
90#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
91#[serde(rename_all = "camelCase")]
92#[ts(export, rename_all = "camelCase")]
93pub struct TextPropertyEntry {
94    /// The text content
95    pub text: String,
96    /// Properties for this text
97    #[ts(type = "Record<string, any>")]
98    #[serde(default)]
99    pub properties: HashMap<String, serde_json::Value>,
100    /// Optional whole-entry styling
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub style: Option<OverlayOptions>,
103    /// Optional sub-range styling within this entry
104    #[serde(default, skip_serializing_if = "Vec::is_empty")]
105    pub inline_overlays: Vec<InlineOverlay>,
106}
107
108impl TextPropertyEntry {
109    /// Create a new entry with just text
110    pub fn text(text: impl Into<String>) -> Self {
111        Self {
112            text: text.into(),
113            properties: HashMap::new(),
114            style: None,
115            inline_overlays: Vec::new(),
116        }
117    }
118
119    /// Add a property
120    pub fn with_property(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
121        self.properties.insert(key.into(), value);
122        self
123    }
124
125    /// Set multiple properties
126    pub fn with_properties(mut self, props: HashMap<String, serde_json::Value>) -> Self {
127        self.properties = props;
128        self
129    }
130
131    /// Set whole-entry styling
132    pub fn with_style(mut self, style: OverlayOptions) -> Self {
133        self.style = Some(style);
134        self
135    }
136
137    /// Add a sub-range inline overlay
138    pub fn with_inline_overlay(mut self, start: usize, end: usize, style: OverlayOptions) -> Self {
139        self.inline_overlays.push(InlineOverlay {
140            start,
141            end,
142            style,
143            properties: HashMap::new(),
144        });
145        self
146    }
147}