Skip to main content

gpui_rhai/
document.rs

1//! Native immutable text documents, syntax tokenization, and two-way diff models.
2
3use std::borrow::Cow;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6use std::ops::Range;
7use std::str::FromStr as _;
8use std::sync::{Arc, OnceLock, RwLock};
9use std::time::{Duration, Instant};
10
11use rhai::{CustomType, Dynamic, Engine, FuncRegistration, ImmutableString, TypeBuilder};
12use similar::{Algorithm, DiffOp, capture_diff_slices_deadline};
13use syntect::easy::ScopeRangeIterator;
14use syntect::highlighting::ScopeSelectors;
15use syntect::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxSet};
16use thiserror::Error;
17use unicode_segmentation::UnicodeSegmentation as _;
18
19use crate::ComponentInstancePath;
20
21pub const DEFAULT_DOCUMENT_MAX_BYTES: usize = 10 * 1024 * 1024;
22pub const DEFAULT_DOCUMENT_MAX_LINES: usize = 500_000;
23pub const DEFAULT_DIFF_MAX_HUNKS: usize = 100_000;
24pub const DEFAULT_DIFF_TIMEOUT: Duration = Duration::from_secs(2);
25const MAX_INLINE_DIFF_BYTES: usize = 256 * 1024;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct DocumentLimits {
29    pub max_document_bytes: usize,
30    pub max_document_lines: usize,
31    pub max_diff_total_bytes: usize,
32    pub max_diff_hunks: usize,
33    pub diff_timeout: Duration,
34}
35
36impl Default for DocumentLimits {
37    fn default() -> Self {
38        Self {
39            max_document_bytes: DEFAULT_DOCUMENT_MAX_BYTES,
40            max_document_lines: DEFAULT_DOCUMENT_MAX_LINES,
41            max_diff_total_bytes: DEFAULT_DOCUMENT_MAX_BYTES * 2,
42            max_diff_hunks: DEFAULT_DIFF_MAX_HUNKS,
43            diff_timeout: DEFAULT_DIFF_TIMEOUT,
44        }
45    }
46}
47
48impl DocumentLimits {
49    /// Validate Host-configured resource limits.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`DocumentError::InvalidLimits`] when any limit is zero.
54    pub fn validate(self) -> Result<(), DocumentError> {
55        if self.max_document_bytes == 0
56            || self.max_document_lines == 0
57            || self.max_diff_total_bytes == 0
58            || self.max_diff_hunks == 0
59            || self.diff_timeout.is_zero()
60        {
61            Err(DocumentError::InvalidLimits)
62        } else {
63            Ok(())
64        }
65    }
66}
67
68#[derive(Clone, Debug)]
69pub struct DocumentRuntimeConfig {
70    limits: Arc<RwLock<DocumentLimits>>,
71}
72
73impl Default for DocumentRuntimeConfig {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl DocumentRuntimeConfig {
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            limits: Arc::new(RwLock::new(DocumentLimits::default())),
84        }
85    }
86
87    #[must_use]
88    pub fn limits(&self) -> DocumentLimits {
89        self.limits
90            .read()
91            .map_or_else(|poisoned| *poisoned.into_inner(), |limits| *limits)
92    }
93
94    /// Replace limits used by future document and diff jobs.
95    ///
96    /// # Errors
97    ///
98    /// Returns an invalid-limit or poisoned-registry error.
99    pub fn set_limits(&self, limits: DocumentLimits) -> Result<(), DocumentError> {
100        limits.validate()?;
101        *self
102            .limits
103            .write()
104            .map_err(|_| DocumentError::DocumentConfigPoisoned)? = limits;
105        Ok(())
106    }
107}
108
109const RHAI_SYNTAX: &str = r#"%YAML 1.2
110---
111name: Rhai
112file_extensions: [rhai]
113scope: source.rhai
114contexts:
115  main:
116    - include: comments
117    - match: '\b(fn|let|const|if|else|switch|for|in|while|loop|do|until|break|continue|return|throw|try|catch|import|export|as|private)\b'
118      scope: keyword.control.rhai
119    - match: '\b(true|false)\b'
120      scope: constant.language.rhai
121    - match: '\b[0-9]+(?:\.[0-9]+)?\b'
122      scope: constant.numeric.rhai
123    - match: '"'
124      push: double-quoted-string
125    - match: "'"
126      push: single-quoted-string
127    - match: '`'
128      push: template-string
129    - match: '\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\()'
130      scope: entity.name.function.rhai
131    - match: '[+\-*/%!=<>|&^~?:]+'
132      scope: keyword.operator.rhai
133  comments:
134    - match: '//.*$'
135      scope: comment.line.double-slash.rhai
136    - match: '/\*'
137      scope: punctuation.definition.comment.begin.rhai
138      push:
139        - meta_scope: comment.block.rhai
140        - match: '\*/'
141          scope: punctuation.definition.comment.end.rhai
142          pop: true
143  double-quoted-string:
144    - meta_scope: string.quoted.double.rhai
145    - match: '\\.'
146      scope: constant.character.escape.rhai
147    - match: '"'
148      pop: true
149  single-quoted-string:
150    - meta_scope: string.quoted.single.rhai
151    - match: '\\.'
152      scope: constant.character.escape.rhai
153    - match: "'"
154      pop: true
155  template-string:
156    - meta_scope: string.quoted.other.rhai
157    - match: '\\.'
158      scope: constant.character.escape.rhai
159    - match: '`'
160      pop: true
161"#;
162
163const TYPESCRIPT_SYNTAX: &str = r#"%YAML 1.2
164---
165name: TypeScript
166file_extensions: [ts, tsx]
167scope: source.ts
168contexts:
169  main:
170    - match: '//.*$'
171      scope: comment.line.double-slash.ts
172    - match: '/\*'
173      push:
174        - meta_scope: comment.block.ts
175        - match: '\*/'
176          pop: true
177    - match: '\b(async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|keyof|let|namespace|new|of|private|protected|public|readonly|return|set|static|super|switch|throw|try|type|typeof|var|void|while|with|yield)\b'
178      scope: keyword.control.ts
179    - match: '\b(any|bigint|boolean|never|number|object|string|symbol|unknown|undefined|null|true|false)\b'
180      scope: storage.type.ts
181    - match: '\b[0-9]+(?:\.[0-9]+)?\b'
182      scope: constant.numeric.ts
183    - match: '"'
184      push: double-string
185    - match: "'"
186      push: single-string
187    - match: '`'
188      push: template-string
189    - match: '\b[A-Za-z_$][A-Za-z0-9_$]*(?=\s*\()'
190      scope: entity.name.function.ts
191    - match: '</?[A-Za-z][A-Za-z0-9:.-]*'
192      scope: entity.name.tag.tsx
193    - match: '[+\-*/%!=<>|&^~?:]+'
194      scope: keyword.operator.ts
195  double-string:
196    - meta_scope: string.quoted.double.ts
197    - match: '\\.'
198      scope: constant.character.escape.ts
199    - match: '"'
200      pop: true
201  single-string:
202    - meta_scope: string.quoted.single.ts
203    - match: '\\.'
204      scope: constant.character.escape.ts
205    - match: "'"
206      pop: true
207  template-string:
208    - meta_scope: string.quoted.other.ts
209    - match: '\\.'
210      scope: constant.character.escape.ts
211    - match: '`'
212      pop: true
213"#;
214
215const JSONC_SYNTAX: &str = r#"%YAML 1.2
216---
217name: JSON with Comments
218file_extensions: [jsonc]
219scope: source.json.comments
220contexts:
221  main:
222    - match: '//.*$'
223      scope: comment.line.double-slash.json
224    - match: '/\*'
225      push:
226        - meta_scope: comment.block.json
227        - match: '\*/'
228          pop: true
229    - match: '"'
230      push: string
231    - match: '-?\b[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b'
232      scope: constant.numeric.json
233    - match: '\b(true|false|null)\b'
234      scope: constant.language.json
235    - match: '[{}\[\],:]'
236      scope: punctuation.separator.json
237  string:
238    - meta_scope: string.quoted.double.json
239    - match: '\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4})'
240      scope: constant.character.escape.json
241    - match: '"'
242      pop: true
243"#;
244
245const TOML_SYNTAX: &str = r#"%YAML 1.2
246---
247name: TOML
248file_extensions: [toml]
249scope: source.toml
250contexts:
251  main:
252    - match: '#.*$'
253      scope: comment.line.number-sign.toml
254    - match: '^\s*\[\[?[^\]]+\]\]?'
255      scope: entity.name.section.toml
256    - match: '^[A-Za-z0-9_.-]+(?=\s*=)'
257      scope: variable.other.key.toml
258    - match: '"""'
259      push: multiline-string
260    - match: "'''"
261      push: multiline-literal
262    - match: '"'
263      push: string
264    - match: "'"
265      push: literal
266    - match: '\b(true|false)\b'
267      scope: constant.language.toml
268    - match: '[+-]?\b(?:0x[0-9A-Fa-f_]+|0o[0-7_]+|0b[01_]+|[0-9][0-9_]*(?:\.[0-9_]+)?)\b'
269      scope: constant.numeric.toml
270    - match: '[=,.{}\[\]]'
271      scope: punctuation.separator.toml
272  string:
273    - meta_scope: string.quoted.double.toml
274    - match: '\\.'
275      scope: constant.character.escape.toml
276    - match: '"'
277      pop: true
278  literal:
279    - meta_scope: string.quoted.single.toml
280    - match: "'"
281      pop: true
282  multiline-string:
283    - meta_scope: string.quoted.triple.toml
284    - match: '"""'
285      pop: true
286  multiline-literal:
287    - meta_scope: string.quoted.triple.toml
288    - match: "'''"
289      pop: true
290"#;
291
292const DOCKERFILE_SYNTAX: &str = r#"%YAML 1.2
293---
294name: Dockerfile
295file_extensions: [Dockerfile, dockerfile]
296scope: source.dockerfile
297contexts:
298  main:
299    - match: '#.*$'
300      scope: comment.line.number-sign.dockerfile
301    - match: '(?i)^\s*(ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)'
302      scope: keyword.control.dockerfile
303    - match: '\$\{?[A-Za-z_][A-Za-z0-9_]*\}?'
304      scope: variable.other.dockerfile
305    - match: '"'
306      push: double-string
307    - match: "'"
308      push: single-string
309  double-string:
310    - meta_scope: string.quoted.double.dockerfile
311    - match: '\\.'
312      scope: constant.character.escape.dockerfile
313    - match: '"'
314      pop: true
315  single-string:
316    - meta_scope: string.quoted.single.dockerfile
317    - match: "'"
318      pop: true
319"#;
320
321const EXTRA_SYNTAXES: &[&str] = &[
322    RHAI_SYNTAX,
323    TYPESCRIPT_SYNTAX,
324    JSONC_SYNTAX,
325    TOML_SYNTAX,
326    DOCKERFILE_SYNTAX,
327];
328
329#[derive(Clone)]
330pub struct NativeTextDocument {
331    snapshot: Arc<NativeTextSnapshot>,
332}
333
334#[derive(Debug, Eq, PartialEq)]
335struct NativeTextSnapshot {
336    identity: String,
337    revision: u64,
338    text: Arc<str>,
339}
340
341impl NativeTextDocument {
342    /// Construct one immutable Host-owned text revision.
343    ///
344    /// # Errors
345    ///
346    /// Returns [`DocumentError::InvalidIdentity`] for an unsafe identity.
347    pub fn new(
348        identity: impl Into<String>,
349        revision: u64,
350        text: impl Into<Arc<str>>,
351    ) -> Result<Self, DocumentError> {
352        let identity = identity.into();
353        validate_identity(&identity)?;
354        Ok(Self {
355            snapshot: Arc::new(NativeTextSnapshot {
356                identity,
357                revision,
358                text: text.into(),
359            }),
360        })
361    }
362
363    #[must_use]
364    pub fn identity(&self) -> &str {
365        &self.snapshot.identity
366    }
367
368    #[must_use]
369    pub fn revision(&self) -> u64 {
370        self.snapshot.revision
371    }
372
373    #[must_use]
374    pub fn text(&self) -> &str {
375        &self.snapshot.text
376    }
377
378    #[must_use]
379    pub fn text_arc(&self) -> Arc<str> {
380        Arc::clone(&self.snapshot.text)
381    }
382}
383
384impl PartialEq for NativeTextDocument {
385    fn eq(&self, other: &Self) -> bool {
386        Arc::ptr_eq(&self.snapshot, &other.snapshot) || self.snapshot == other.snapshot
387    }
388}
389
390impl Eq for NativeTextDocument {}
391
392impl fmt::Debug for NativeTextDocument {
393    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
394        formatter
395            .debug_struct("NativeTextDocument")
396            .field("identity", &self.identity())
397            .field("revision", &self.revision())
398            .field("bytes", &self.text().len())
399            .finish()
400    }
401}
402
403impl CustomType for NativeTextDocument {
404    fn build(mut builder: TypeBuilder<Self>) {
405        builder
406            .with_name("NativeTextDocument")
407            .with_get("identity", |document: &mut Self| {
408                ImmutableString::from(document.identity().to_owned())
409            })
410            .with_get("revision", |document: &mut Self| {
411                i64::try_from(document.revision()).unwrap_or(i64::MAX)
412            })
413            .with_get("len", |document: &mut Self| {
414                i64::try_from(document.text().len()).unwrap_or(i64::MAX)
415            })
416            .with_fn("to_string", |document: &mut Self| {
417                format!(
418                    "NativeTextDocument({}, revision={})",
419                    document.identity(),
420                    document.revision()
421                )
422            });
423    }
424}
425
426pub(crate) fn register_document_api(engine: &mut Engine) {
427    engine.build_type::<NativeTextDocument>();
428    FuncRegistration::new("is_native_text_document")
429        .in_global_namespace()
430        .register_into_engine(engine, |value: Dynamic| value.is::<NativeTextDocument>());
431}
432
433#[derive(Clone, Debug, Default)]
434pub struct NativeTextDocumentRegistry {
435    documents: BTreeMap<String, NativeTextDocument>,
436    readers: BTreeMap<String, BTreeSet<crate::read_dependency::ReadDependency>>,
437}
438
439impl NativeTextDocumentRegistry {
440    #[must_use]
441    pub fn new() -> Self {
442        Self::default()
443    }
444
445    /// Register one Host-owned document before the first render.
446    ///
447    /// # Errors
448    ///
449    /// Returns an identity or duplicate-name error.
450    pub fn register(
451        &mut self,
452        name: impl Into<String>,
453        document: NativeTextDocument,
454    ) -> Result<(), DocumentError> {
455        let name = name.into();
456        validate_identity(&name)?;
457        if self.documents.contains_key(&name) {
458            return Err(DocumentError::DuplicateDocument(name));
459        }
460        self.documents.insert(name, document);
461        Ok(())
462    }
463
464    /// Read one current Host-owned snapshot without subscribing a component.
465    ///
466    /// This is intended for trusted Host transactions that need optimistic
467    /// revision checks before publishing a replacement.
468    ///
469    /// # Errors
470    ///
471    /// Returns [`DocumentError::UnknownDocument`] when `name` is not registered.
472    pub fn get(&self, name: &str) -> Result<NativeTextDocument, DocumentError> {
473        self.documents
474            .get(name)
475            .cloned()
476            .ok_or_else(|| DocumentError::UnknownDocument(name.to_owned()))
477    }
478
479    /// Replace one document revision and return exact subscribed components.
480    ///
481    /// # Errors
482    ///
483    /// Returns [`DocumentError::UnknownDocument`] for an unregistered name.
484    pub fn replace(
485        &mut self,
486        name: &str,
487        document: NativeTextDocument,
488    ) -> Result<BTreeSet<ComponentInstancePath>, DocumentError> {
489        let current = self
490            .documents
491            .get_mut(name)
492            .ok_or_else(|| DocumentError::UnknownDocument(name.to_owned()))?;
493        if current == &document {
494            return Ok(BTreeSet::new());
495        }
496        if current.identity() == document.identity() && document.revision() <= current.revision() {
497            return Err(DocumentError::NonMonotonicRevision {
498                identity: document.identity().to_owned(),
499                current: current.revision(),
500                next: document.revision(),
501            });
502        }
503        *current = document;
504        Ok(crate::read_dependency::owners(
505            self.readers.get(name).cloned().unwrap_or_default(),
506        ))
507    }
508
509    #[cfg(test)]
510    pub(crate) fn read_tracked(
511        &mut self,
512        reader: &ComponentInstancePath,
513        name: &str,
514    ) -> Result<NativeTextDocument, DocumentError> {
515        self.read_dependency(
516            Some(&crate::read_dependency::ReadDependency::component(reader)),
517            name,
518        )
519    }
520
521    /// Read a document, subscribing `reader` when there is one.
522    pub(crate) fn read_dependency(
523        &mut self,
524        reader: Option<&crate::read_dependency::ReadDependency>,
525        name: &str,
526    ) -> Result<NativeTextDocument, DocumentError> {
527        let document = self
528            .documents
529            .get(name)
530            .cloned()
531            .ok_or_else(|| DocumentError::UnknownDocument(name.to_owned()))?;
532        if let Some(reader) = reader {
533            self.readers
534                .entry(name.to_owned())
535                .or_default()
536                .insert(reader.clone());
537        }
538        Ok(document)
539    }
540
541    pub(crate) fn reset_reader(&mut self, reader: &ComponentInstancePath) {
542        self.reset_contribution(&crate::read_dependency::ReadDependency::component(reader));
543    }
544    pub(crate) fn reset_contribution(&mut self, reader: &crate::read_dependency::ReadDependency) {
545        for readers in self.readers.values_mut() {
546            readers.remove(reader);
547        }
548        self.readers.retain(|_, readers| !readers.is_empty());
549    }
550
551    pub(crate) fn retain_reader_scope(
552        &mut self,
553        root: &ComponentInstancePath,
554        active: &BTreeSet<ComponentInstancePath>,
555    ) {
556        for readers in self.readers.values_mut() {
557            readers.retain(|reader| reader.retained_in_owner_scope(root, active));
558        }
559        self.readers.retain(|_, readers| !readers.is_empty());
560    }
561
562    pub(crate) fn remove_reader_scope(&mut self, root: &ComponentInstancePath) {
563        for readers in self.readers.values_mut() {
564            readers.retain(|reader| !reader.owner.is_within(root));
565        }
566        self.readers.retain(|_, readers| !readers.is_empty());
567    }
568
569    pub(crate) fn retain_contributions(
570        &mut self,
571        scope: &ComponentInstancePath,
572        active: &BTreeSet<crate::read_dependency::ReadContribution>,
573    ) {
574        crate::read_dependency::retain_readers(&mut self.readers, |reader| {
575            reader.retained_in_contribution_scope(scope, active)
576        });
577    }
578}
579
580#[derive(Clone)]
581pub struct SyntaxRegistry {
582    syntax_set: Arc<RwLock<Arc<SyntaxSet>>>,
583}
584
585impl fmt::Debug for SyntaxRegistry {
586    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
587        formatter
588            .debug_struct("SyntaxRegistry")
589            .field("syntaxes", &self.snapshot().syntaxes().len())
590            .finish()
591    }
592}
593
594impl Default for SyntaxRegistry {
595    fn default() -> Self {
596        Self::new()
597    }
598}
599
600impl SyntaxRegistry {
601    #[must_use]
602    pub fn new() -> Self {
603        Self {
604            syntax_set: Arc::new(RwLock::new(Arc::clone(default_syntax_set()))),
605        }
606    }
607
608    /// Register a Sublime `.sublime-syntax` definition for this Engine.
609    ///
610    /// # Errors
611    ///
612    /// Returns a syntax parse or poisoned-registry error.
613    pub fn register_sublime_syntax(&self, source: &str) -> Result<(), DocumentError> {
614        let syntax = SyntaxDefinition::load_from_str(source, true, None)
615            .map_err(|error| DocumentError::Syntax(error.to_string()))?;
616        let current = self
617            .syntax_set
618            .read()
619            .map_err(|_| DocumentError::SyntaxRegistryPoisoned)?
620            .clone();
621        let mut builder = current.as_ref().clone().into_builder();
622        builder.add(syntax);
623        let next = Arc::new(builder.build());
624        *self
625            .syntax_set
626            .write()
627            .map_err(|_| DocumentError::SyntaxRegistryPoisoned)? = next;
628        Ok(())
629    }
630
631    #[must_use]
632    pub fn snapshot(&self) -> Arc<SyntaxSet> {
633        self.syntax_set
634            .read()
635            .map_or_else(|poisoned| poisoned.into_inner().clone(), |set| set.clone())
636    }
637}
638
639fn default_syntax_set() -> &'static Arc<SyntaxSet> {
640    static SYNTAXES: OnceLock<Arc<SyntaxSet>> = OnceLock::new();
641    SYNTAXES.get_or_init(|| {
642        let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
643        for source in EXTRA_SYNTAXES {
644            builder.add(
645                SyntaxDefinition::load_from_str(source, true, None)
646                    .expect("built-in syntax is valid"),
647            );
648        }
649        Arc::new(builder.build())
650    })
651}
652
653#[derive(Clone, Debug, Eq, PartialEq)]
654pub enum DocumentSource {
655    Inline(Arc<str>),
656    Native(NativeTextDocument),
657}
658
659impl DocumentSource {
660    #[must_use]
661    pub fn identity(&self) -> Cow<'_, str> {
662        match self {
663            Self::Inline(_) => Cow::Borrowed("inline"),
664            Self::Native(document) => Cow::Borrowed(document.identity()),
665        }
666    }
667
668    #[must_use]
669    pub fn revision(&self) -> u64 {
670        match self {
671            Self::Inline(text) => stable_text_hash(text),
672            Self::Native(document) => document.revision(),
673        }
674    }
675
676    #[must_use]
677    pub fn text(&self) -> Arc<str> {
678        match self {
679            Self::Inline(text) => Arc::clone(text),
680            Self::Native(document) => document.text_arc(),
681        }
682    }
683}
684
685impl From<String> for DocumentSource {
686    fn from(value: String) -> Self {
687        Self::Inline(Arc::from(value))
688    }
689}
690
691impl From<NativeTextDocument> for DocumentSource {
692    fn from(value: NativeTextDocument) -> Self {
693        Self::Native(value)
694    }
695}
696
697#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
698pub enum DocumentWrap {
699    #[default]
700    None,
701    Viewport,
702    Column(usize),
703}
704
705#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
706pub enum DiffViewMode {
707    #[default]
708    Unified,
709    Split,
710}
711
712#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
713pub enum DiffWhitespace {
714    #[default]
715    Exact,
716    IgnoreChanges,
717    IgnoreAll,
718}
719
720#[derive(Clone, Debug, Eq, PartialEq)]
721pub struct DocumentDescriptor {
722    pub source: DocumentSource,
723    pub label: String,
724    pub file_name: Option<String>,
725    pub language: Option<String>,
726}
727
728#[derive(Clone, Copy, Debug, Eq, PartialEq)]
729pub enum SyntaxTokenKind {
730    Comment,
731    String,
732    Number,
733    Keyword,
734    Function,
735    Type,
736    Variable,
737    Constant,
738    Operator,
739    Punctuation,
740    Tag,
741    Attribute,
742}
743
744impl SyntaxTokenKind {
745    #[must_use]
746    pub const fn theme_token(self) -> &'static str {
747        match self {
748            Self::Comment => "syntax.comment",
749            Self::String => "syntax.string",
750            Self::Number => "syntax.number",
751            Self::Keyword => "syntax.keyword",
752            Self::Function => "syntax.function",
753            Self::Type => "syntax.type",
754            Self::Variable => "syntax.variable",
755            Self::Constant => "syntax.constant",
756            Self::Operator => "syntax.operator",
757            Self::Punctuation => "syntax.punctuation",
758            Self::Tag => "syntax.tag",
759            Self::Attribute => "syntax.attribute",
760        }
761    }
762}
763
764#[derive(Clone, Debug, Eq, PartialEq)]
765pub struct SyntaxSpan {
766    pub range: Range<usize>,
767    pub kind: SyntaxTokenKind,
768}
769
770#[derive(Clone, Debug, Eq, PartialEq)]
771pub struct DocumentLine {
772    pub range: Range<usize>,
773    pub syntax: Vec<SyntaxSpan>,
774}
775
776#[derive(Clone, Debug)]
777pub struct PreparedDocument {
778    identity: String,
779    revision: u64,
780    text: Arc<str>,
781    lines: Arc<[DocumentLine]>,
782    language: String,
783    max_line_chars: usize,
784    ends_with_newline: bool,
785    highlight_error: Option<String>,
786}
787
788impl PreparedDocument {
789    #[must_use]
790    pub fn identity(&self) -> &str {
791        &self.identity
792    }
793
794    #[must_use]
795    pub const fn revision(&self) -> u64 {
796        self.revision
797    }
798
799    #[must_use]
800    pub fn text(&self) -> &str {
801        &self.text
802    }
803
804    #[must_use]
805    pub fn text_arc(&self) -> Arc<str> {
806        Arc::clone(&self.text)
807    }
808
809    #[must_use]
810    pub fn lines(&self) -> &[DocumentLine] {
811        &self.lines
812    }
813
814    #[must_use]
815    pub fn line_text(&self, index: usize) -> Option<&str> {
816        let line = self.lines.get(index)?;
817        self.text.get(line.range.clone())
818    }
819
820    #[must_use]
821    pub fn language(&self) -> &str {
822        &self.language
823    }
824
825    #[must_use]
826    pub const fn max_line_chars(&self) -> usize {
827        self.max_line_chars
828    }
829
830    #[must_use]
831    pub const fn ends_with_newline(&self) -> bool {
832        self.ends_with_newline
833    }
834
835    #[must_use]
836    pub fn highlight_error(&self) -> Option<&str> {
837        self.highlight_error.as_deref()
838    }
839}
840
841/// Prepare one immutable text snapshot with default resource limits.
842///
843/// # Errors
844///
845/// Returns document budget errors. Syntax highlighting failures degrade to
846/// plain text and are recorded on the returned snapshot.
847pub fn prepare_document(
848    descriptor: &DocumentDescriptor,
849    syntaxes: &SyntaxRegistry,
850) -> Result<PreparedDocument, DocumentError> {
851    prepare_document_with_limits(descriptor, syntaxes, DocumentLimits::default())
852}
853
854/// Prepare one immutable text snapshot with Host-provided resource limits.
855///
856/// # Errors
857///
858/// Returns when byte or line budgets are exceeded.
859pub fn prepare_document_with_limits(
860    descriptor: &DocumentDescriptor,
861    syntaxes: &SyntaxRegistry,
862    limits: DocumentLimits,
863) -> Result<PreparedDocument, DocumentError> {
864    let text = descriptor.source.text();
865    if text.len() > limits.max_document_bytes {
866        return Err(DocumentError::TooManyBytes {
867            actual: text.len(),
868            limit: limits.max_document_bytes,
869        });
870    }
871    let ranges = document_line_ranges(&text);
872    if ranges.len() > limits.max_document_lines {
873        return Err(DocumentError::TooManyLines {
874            actual: ranges.len(),
875            limit: limits.max_document_lines,
876        });
877    }
878    let syntax_set = syntaxes.snapshot();
879    let syntax = resolve_syntax(
880        &syntax_set,
881        descriptor.language.as_deref(),
882        descriptor.file_name.as_deref(),
883    );
884    let language = syntax.name.clone();
885    let mut parser = ParseState::new(syntax);
886    let mut stack = ScopeStack::new();
887    let selectors = syntax_selectors();
888    let mut lines = Vec::with_capacity(ranges.len());
889    let mut max_line_chars = 0usize;
890    let mut highlight_error = None;
891    let mut highlighting = true;
892    for range in ranges {
893        let line = &text[range.clone()];
894        max_line_chars = max_line_chars.max(line.chars().count());
895        let parse_end = text[range.end..]
896            .find('\n')
897            .map_or(range.end, |offset| range.end + offset + 1);
898        let parse_start = range.start;
899        let parse_line = &text[parse_start..parse_end];
900        let mut spans: Vec<SyntaxSpan> = Vec::new();
901        if highlighting {
902            match parser.parse_line(parse_line, &syntax_set) {
903                Ok(operations) => {
904                    for (token_range, operation) in ScopeRangeIterator::new(&operations, parse_line)
905                    {
906                        if let Err(error) = stack.apply(operation) {
907                            highlight_error.get_or_insert_with(|| error.to_string());
908                            highlighting = false;
909                            spans.clear();
910                            break;
911                        }
912                        let start = token_range.start.min(line.len());
913                        let end = token_range.end.min(line.len());
914                        if start >= end {
915                            continue;
916                        }
917                        if let Some(kind) = selectors.kind(stack.as_slice()) {
918                            if let Some(previous) = spans.last_mut()
919                                && previous.kind == kind
920                                && previous.range.end == start
921                            {
922                                previous.range.end = end;
923                            } else {
924                                spans.push(SyntaxSpan {
925                                    range: start..end,
926                                    kind,
927                                });
928                            }
929                        }
930                    }
931                }
932                Err(error) => {
933                    highlight_error.get_or_insert_with(|| error.to_string());
934                    highlighting = false;
935                }
936            }
937        }
938        lines.push(DocumentLine {
939            range,
940            syntax: spans,
941        });
942    }
943    Ok(PreparedDocument {
944        identity: descriptor.source.identity().into_owned(),
945        revision: descriptor.source.revision(),
946        text,
947        lines: lines.into(),
948        language,
949        max_line_chars,
950        ends_with_newline: descriptor.source.text().ends_with('\n'),
951        highlight_error,
952    })
953}
954
955fn document_line_ranges(text: &str) -> Vec<Range<usize>> {
956    let mut ranges = Vec::new();
957    let mut start = 0usize;
958    for (index, byte) in text.bytes().enumerate() {
959        if byte == b'\n' {
960            let mut end = index;
961            if text.as_bytes().get(index.wrapping_sub(1)) == Some(&b'\r') && index > start {
962                end -= 1;
963            }
964            ranges.push(start..end);
965            start = index + 1;
966        }
967    }
968    if start < text.len() || text.is_empty() || text.ends_with('\n') {
969        ranges.push(start..text.len());
970    }
971    ranges
972}
973
974fn resolve_syntax<'a>(
975    syntaxes: &'a SyntaxSet,
976    language: Option<&str>,
977    file_name: Option<&str>,
978) -> &'a syntect::parsing::SyntaxReference {
979    language
980        .and_then(|language| {
981            let token = language_alias(language);
982            syntaxes.find_syntax_by_token(token.as_ref())
983        })
984        .or_else(|| {
985            file_name.and_then(|file_name| {
986                syntaxes.find_syntax_by_extension(file_name).or_else(|| {
987                    file_name
988                        .rsplit_once('.')
989                        .and_then(|(_, ext)| syntaxes.find_syntax_by_extension(ext))
990                })
991            })
992        })
993        .unwrap_or_else(|| syntaxes.find_syntax_plain_text())
994}
995
996fn language_alias(language: &str) -> Cow<'_, str> {
997    let normalized = language.trim().to_ascii_lowercase();
998    match normalized.as_str() {
999        "plaintext" | "plain_text" | "text" => Cow::Borrowed("txt"),
1000        "javascript" => Cow::Borrowed("js"),
1001        "typescript" => Cow::Borrowed("ts"),
1002        "shell" | "bash" => Cow::Borrowed("sh"),
1003        "python" => Cow::Borrowed("py"),
1004        "markdown" => Cow::Borrowed("md"),
1005        "rust" => Cow::Borrowed("rs"),
1006        "golang" => Cow::Borrowed("go"),
1007        _ => Cow::Owned(normalized),
1008    }
1009}
1010
1011struct SyntaxSelectors {
1012    comment: ScopeSelectors,
1013    string: ScopeSelectors,
1014    number: ScopeSelectors,
1015    keyword: ScopeSelectors,
1016    function: ScopeSelectors,
1017    type_name: ScopeSelectors,
1018    variable: ScopeSelectors,
1019    constant: ScopeSelectors,
1020    operator: ScopeSelectors,
1021    punctuation: ScopeSelectors,
1022    tag: ScopeSelectors,
1023    attribute: ScopeSelectors,
1024}
1025
1026impl SyntaxSelectors {
1027    fn new() -> Self {
1028        let parse = |value: &str| ScopeSelectors::from_str(value).expect("static scope selector");
1029        Self {
1030            comment: parse("comment"),
1031            string: parse("string"),
1032            number: parse("constant.numeric"),
1033            keyword: parse("keyword, storage.modifier"),
1034            function: parse("entity.name.function, support.function"),
1035            type_name: parse("entity.name.type, support.type, storage.type"),
1036            variable: parse("variable"),
1037            constant: parse("constant"),
1038            operator: parse("keyword.operator"),
1039            punctuation: parse("punctuation"),
1040            tag: parse("entity.name.tag"),
1041            attribute: parse("entity.other.attribute-name"),
1042        }
1043    }
1044
1045    fn kind(&self, stack: &[syntect::parsing::Scope]) -> Option<SyntaxTokenKind> {
1046        [
1047            (&self.comment, SyntaxTokenKind::Comment),
1048            (&self.string, SyntaxTokenKind::String),
1049            (&self.number, SyntaxTokenKind::Number),
1050            (&self.function, SyntaxTokenKind::Function),
1051            (&self.type_name, SyntaxTokenKind::Type),
1052            (&self.tag, SyntaxTokenKind::Tag),
1053            (&self.attribute, SyntaxTokenKind::Attribute),
1054            (&self.operator, SyntaxTokenKind::Operator),
1055            (&self.keyword, SyntaxTokenKind::Keyword),
1056            (&self.variable, SyntaxTokenKind::Variable),
1057            (&self.constant, SyntaxTokenKind::Constant),
1058            (&self.punctuation, SyntaxTokenKind::Punctuation),
1059        ]
1060        .into_iter()
1061        .find_map(|(selector, kind)| selector.does_match(stack).map(|_| kind))
1062    }
1063}
1064
1065fn syntax_selectors() -> &'static SyntaxSelectors {
1066    static SELECTORS: OnceLock<SyntaxSelectors> = OnceLock::new();
1067    SELECTORS.get_or_init(SyntaxSelectors::new)
1068}
1069
1070#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1071pub enum DiffRowKind {
1072    Equal,
1073    LeftOnly,
1074    RightOnly,
1075    Modified,
1076}
1077
1078#[derive(Clone, Debug, Eq, PartialEq)]
1079pub struct DiffAlignedRow {
1080    pub left: Option<usize>,
1081    pub right: Option<usize>,
1082    pub kind: DiffRowKind,
1083    pub hunk: Option<usize>,
1084    pub left_inline: Vec<Range<usize>>,
1085    pub right_inline: Vec<Range<usize>>,
1086}
1087
1088#[derive(Clone, Debug, Eq, PartialEq)]
1089pub enum DiffDisplayRow {
1090    Content(usize),
1091    Fold {
1092        id: usize,
1093        full_range: Range<usize>,
1094        hidden_rows: usize,
1095    },
1096}
1097
1098#[derive(Clone, Debug)]
1099pub struct PreparedDiff {
1100    pub left: PreparedDocument,
1101    pub right: PreparedDocument,
1102    pub rows: Arc<[DiffAlignedRow]>,
1103    pub collapsed: Arc<[DiffDisplayRow]>,
1104    pub hunk_count: usize,
1105}
1106
1107/// Prepare a two-way comparison with default resource limits.
1108///
1109/// # Errors
1110///
1111/// Returns document or diff budget errors.
1112pub fn prepare_diff(
1113    left: &DocumentDescriptor,
1114    right: &DocumentDescriptor,
1115    whitespace: DiffWhitespace,
1116    context_lines: Option<usize>,
1117    syntaxes: &SyntaxRegistry,
1118) -> Result<PreparedDiff, DocumentError> {
1119    prepare_diff_with_limits(
1120        left,
1121        right,
1122        whitespace,
1123        context_lines,
1124        syntaxes,
1125        DocumentLimits::default(),
1126    )
1127}
1128
1129/// Prepare a two-way comparison with Host-provided resource limits.
1130///
1131/// # Errors
1132///
1133/// Returns when document, combined-input, or hunk budgets are exceeded.
1134#[allow(clippy::too_many_lines)]
1135pub fn prepare_diff_with_limits(
1136    left: &DocumentDescriptor,
1137    right: &DocumentDescriptor,
1138    whitespace: DiffWhitespace,
1139    context_lines: Option<usize>,
1140    syntaxes: &SyntaxRegistry,
1141    limits: DocumentLimits,
1142) -> Result<PreparedDiff, DocumentError> {
1143    let total_bytes = left
1144        .source
1145        .text()
1146        .len()
1147        .saturating_add(right.source.text().len());
1148    if total_bytes > limits.max_diff_total_bytes {
1149        return Err(DocumentError::TooManyDiffBytes {
1150            actual: total_bytes,
1151            limit: limits.max_diff_total_bytes,
1152        });
1153    }
1154    let left = prepare_document_with_limits(left, syntaxes, limits)?;
1155    let right = prepare_document_with_limits(right, syntaxes, limits)?;
1156    let left_keys = diff_keys(&left, whitespace);
1157    let right_keys = diff_keys(&right, whitespace);
1158    let deadline = Instant::now().checked_add(limits.diff_timeout);
1159    let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_keys, &right_keys, deadline);
1160    let mut rows = Vec::with_capacity(left.lines().len().max(right.lines().len()));
1161    let mut hunk = 0usize;
1162    let mut in_change = false;
1163    for operation in ops {
1164        match operation {
1165            DiffOp::Equal {
1166                old_index,
1167                new_index,
1168                len,
1169            } => {
1170                in_change = false;
1171                rows.extend((0..len).map(|offset| DiffAlignedRow {
1172                    left: Some(old_index + offset),
1173                    right: Some(new_index + offset),
1174                    kind: DiffRowKind::Equal,
1175                    hunk: None,
1176                    left_inline: Vec::new(),
1177                    right_inline: Vec::new(),
1178                }));
1179            }
1180            DiffOp::Delete {
1181                old_index, old_len, ..
1182            } => {
1183                let id = next_hunk(&mut hunk, &mut in_change, limits.max_diff_hunks)?;
1184                rows.extend((0..old_len).map(|offset| DiffAlignedRow {
1185                    left: Some(old_index + offset),
1186                    right: None,
1187                    kind: DiffRowKind::LeftOnly,
1188                    hunk: Some(id),
1189                    left_inline: Vec::new(),
1190                    right_inline: Vec::new(),
1191                }));
1192            }
1193            DiffOp::Insert {
1194                new_index, new_len, ..
1195            } => {
1196                let id = next_hunk(&mut hunk, &mut in_change, limits.max_diff_hunks)?;
1197                rows.extend((0..new_len).map(|offset| DiffAlignedRow {
1198                    left: None,
1199                    right: Some(new_index + offset),
1200                    kind: DiffRowKind::RightOnly,
1201                    hunk: Some(id),
1202                    left_inline: Vec::new(),
1203                    right_inline: Vec::new(),
1204                }));
1205            }
1206            DiffOp::Replace {
1207                old_index,
1208                old_len,
1209                new_index,
1210                new_len,
1211            } => {
1212                let id = next_hunk(&mut hunk, &mut in_change, limits.max_diff_hunks)?;
1213                let aligned = old_len.max(new_len);
1214                for offset in 0..aligned {
1215                    let left_index = (offset < old_len).then_some(old_index + offset);
1216                    let right_index = (offset < new_len).then_some(new_index + offset);
1217                    let (left_inline, right_inline) = match (left_index, right_index) {
1218                        (Some(left_index), Some(right_index)) => inline_ranges(
1219                            left.line_text(left_index).unwrap_or_default(),
1220                            right.line_text(right_index).unwrap_or_default(),
1221                            deadline,
1222                        ),
1223                        _ => (Vec::new(), Vec::new()),
1224                    };
1225                    rows.push(DiffAlignedRow {
1226                        left: left_index,
1227                        right: right_index,
1228                        kind: match (left_index, right_index) {
1229                            (Some(_), Some(_)) => DiffRowKind::Modified,
1230                            (Some(_), None) => DiffRowKind::LeftOnly,
1231                            (None, Some(_)) => DiffRowKind::RightOnly,
1232                            (None, None) => unreachable!(),
1233                        },
1234                        hunk: Some(id),
1235                        left_inline,
1236                        right_inline,
1237                    });
1238                }
1239            }
1240        }
1241    }
1242    let collapsed = collapse_rows(&rows, context_lines);
1243    Ok(PreparedDiff {
1244        left,
1245        right,
1246        rows: rows.into(),
1247        collapsed: collapsed.into(),
1248        hunk_count: hunk,
1249    })
1250}
1251
1252fn next_hunk(
1253    hunks: &mut usize,
1254    in_change: &mut bool,
1255    limit: usize,
1256) -> Result<usize, DocumentError> {
1257    if !*in_change {
1258        *hunks = hunks.checked_add(1).ok_or(DocumentError::TooManyHunks {
1259            actual: usize::MAX,
1260            limit,
1261        })?;
1262        *in_change = true;
1263    }
1264    if *hunks > limit {
1265        return Err(DocumentError::TooManyHunks {
1266            actual: *hunks,
1267            limit,
1268        });
1269    }
1270    Ok(*hunks - 1)
1271}
1272
1273fn diff_keys(document: &PreparedDocument, whitespace: DiffWhitespace) -> Vec<Cow<'_, str>> {
1274    document
1275        .lines()
1276        .iter()
1277        .enumerate()
1278        .map(|(index, _)| {
1279            let line = document.line_text(index).unwrap_or_default();
1280            match whitespace {
1281                DiffWhitespace::Exact => Cow::Borrowed(line),
1282                DiffWhitespace::IgnoreChanges => {
1283                    Cow::Owned(line.split_whitespace().collect::<Vec<_>>().join(" "))
1284                }
1285                DiffWhitespace::IgnoreAll => Cow::Owned(
1286                    line.chars()
1287                        .filter(|character| !character.is_whitespace())
1288                        .collect(),
1289                ),
1290            }
1291        })
1292        .collect()
1293}
1294
1295fn inline_ranges(
1296    left: &str,
1297    right: &str,
1298    deadline: Option<Instant>,
1299) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
1300    if left.len().saturating_add(right.len()) > MAX_INLINE_DIFF_BYTES {
1301        return (
1302            (!left.is_empty())
1303                .then_some(0..left.len())
1304                .into_iter()
1305                .collect(),
1306            (!right.is_empty())
1307                .then_some(0..right.len())
1308                .into_iter()
1309                .collect(),
1310        );
1311    }
1312    let left_graphemes = left.graphemes(true).collect::<Vec<_>>();
1313    let right_graphemes = right.graphemes(true).collect::<Vec<_>>();
1314    let operations = capture_diff_slices_deadline(
1315        Algorithm::Patience,
1316        &left_graphemes,
1317        &right_graphemes,
1318        deadline,
1319    );
1320    let mut left_offset = 0usize;
1321    let mut right_offset = 0usize;
1322    let mut left_ranges = Vec::new();
1323    let mut right_ranges = Vec::new();
1324    for operation in operations {
1325        for change in operation.iter_changes(&left_graphemes, &right_graphemes) {
1326            let len = change.value().len();
1327            match change.tag() {
1328                similar::ChangeTag::Equal => {
1329                    left_offset += len;
1330                    right_offset += len;
1331                }
1332                similar::ChangeTag::Delete => {
1333                    push_range(&mut left_ranges, left_offset..left_offset + len);
1334                    left_offset += len;
1335                }
1336                similar::ChangeTag::Insert => {
1337                    push_range(&mut right_ranges, right_offset..right_offset + len);
1338                    right_offset += len;
1339                }
1340            }
1341        }
1342    }
1343    (left_ranges, right_ranges)
1344}
1345
1346fn push_range(ranges: &mut Vec<Range<usize>>, range: Range<usize>) {
1347    if range.is_empty() {
1348        return;
1349    }
1350    if let Some(previous) = ranges.last_mut()
1351        && previous.end == range.start
1352    {
1353        previous.end = range.end;
1354    } else {
1355        ranges.push(range);
1356    }
1357}
1358
1359fn collapse_rows(rows: &[DiffAlignedRow], context: Option<usize>) -> Vec<DiffDisplayRow> {
1360    let Some(context) = context else {
1361        return (0..rows.len()).map(DiffDisplayRow::Content).collect();
1362    };
1363    if rows.iter().all(|row| row.kind == DiffRowKind::Equal) {
1364        return if rows.is_empty() {
1365            Vec::new()
1366        } else {
1367            vec![DiffDisplayRow::Fold {
1368                id: 0,
1369                full_range: 0..rows.len(),
1370                hidden_rows: rows.len(),
1371            }]
1372        };
1373    }
1374    let mut keep = vec![false; rows.len()];
1375    for (index, row) in rows.iter().enumerate() {
1376        if row.kind != DiffRowKind::Equal {
1377            let start = index.saturating_sub(context);
1378            let end = index
1379                .saturating_add(context)
1380                .saturating_add(1)
1381                .min(rows.len());
1382            keep[start..end].fill(true);
1383        }
1384    }
1385    let mut display = Vec::new();
1386    let mut index = 0usize;
1387    let mut fold = 0usize;
1388    while index < rows.len() {
1389        if keep[index] {
1390            display.push(DiffDisplayRow::Content(index));
1391            index += 1;
1392            continue;
1393        }
1394        let start = index;
1395        while index < rows.len() && !keep[index] {
1396            index += 1;
1397        }
1398        display.push(DiffDisplayRow::Fold {
1399            id: fold,
1400            full_range: start..index,
1401            hidden_rows: index - start,
1402        });
1403        fold += 1;
1404    }
1405    display
1406}
1407
1408fn stable_text_hash(text: &str) -> u64 {
1409    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1410    const PRIME: u64 = 0x0000_0100_0000_01b3;
1411    text.bytes().fold(OFFSET, |hash, byte| {
1412        (hash ^ u64::from(byte)).wrapping_mul(PRIME)
1413    })
1414}
1415
1416fn validate_identity(value: &str) -> Result<(), DocumentError> {
1417    if value.is_empty()
1418        || value.len() > 256
1419        || !value.chars().all(|character| {
1420            character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | '/' | ':')
1421        })
1422    {
1423        Err(DocumentError::InvalidIdentity(value.to_owned()))
1424    } else {
1425        Ok(())
1426    }
1427}
1428
1429#[derive(Clone, Debug, Error, Eq, PartialEq)]
1430pub enum DocumentError {
1431    #[error("document identity `{0}` must contain 1-256 safe ASCII characters")]
1432    InvalidIdentity(String),
1433    #[error("native text document `{0}` is already registered")]
1434    DuplicateDocument(String),
1435    #[error("native text document `{0}` is not registered")]
1436    UnknownDocument(String),
1437    #[error(
1438        "native text document `{identity}` revision must increase: current {current}, next {next}"
1439    )]
1440    NonMonotonicRevision {
1441        identity: String,
1442        current: u64,
1443        next: u64,
1444    },
1445    #[error("document contains {actual} bytes, exceeding the {limit}-byte budget")]
1446    TooManyBytes { actual: usize, limit: usize },
1447    #[error("diff inputs contain {actual} bytes, exceeding the {limit}-byte budget")]
1448    TooManyDiffBytes { actual: usize, limit: usize },
1449    #[error("document contains {actual} lines, exceeding the {limit}-line budget")]
1450    TooManyLines { actual: usize, limit: usize },
1451    #[error("diff contains {actual} hunks, exceeding the {limit}-hunk budget")]
1452    TooManyHunks { actual: usize, limit: usize },
1453    #[error("syntax definition or parse failed: {0}")]
1454    Syntax(String),
1455    #[error("syntax registry lock was poisoned")]
1456    SyntaxRegistryPoisoned,
1457    #[error("document resource limits must all be greater than zero")]
1458    InvalidLimits,
1459    #[error("document runtime configuration lock was poisoned")]
1460    DocumentConfigPoisoned,
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465    use super::*;
1466
1467    fn descriptor(text: &str, language: &str) -> DocumentDescriptor {
1468        DocumentDescriptor {
1469            source: DocumentSource::from(text.to_owned()),
1470            label: language.to_owned(),
1471            file_name: None,
1472            language: Some(language.to_owned()),
1473        }
1474    }
1475
1476    #[test]
1477    fn native_documents_are_revisioned_and_registry_reads_are_exact() {
1478        let first = NativeTextDocument::new("server/a", 1, Arc::<str>::from("port=80\n")).unwrap();
1479        let second =
1480            NativeTextDocument::new("server/a", 2, Arc::<str>::from("port=443\n")).unwrap();
1481        let reader = ComponentInstancePath::root("View", "main");
1482        let mut registry = NativeTextDocumentRegistry::new();
1483        registry.register("config", first).unwrap();
1484        assert_eq!(
1485            registry.read_tracked(&reader, "config").unwrap().revision(),
1486            1
1487        );
1488        assert_eq!(
1489            registry.replace("config", second).unwrap(),
1490            BTreeSet::from([reader])
1491        );
1492        let stale = NativeTextDocument::new("server/a", 2, Arc::<str>::from("stale")).unwrap();
1493        assert!(matches!(
1494            registry.replace("config", stale),
1495            Err(DocumentError::NonMonotonicRevision { .. })
1496        ));
1497    }
1498
1499    #[test]
1500    fn rhai_and_go_are_built_in_and_unknown_languages_fall_back_to_plain_text() {
1501        let registry = SyntaxRegistry::new();
1502        let rhai = prepare_document(&descriptor("fn view() { 42 }\n", "rhai"), &registry).unwrap();
1503        assert_eq!(rhai.language(), "Rhai");
1504        assert!(
1505            rhai.lines()[0]
1506                .syntax
1507                .iter()
1508                .any(|span| span.kind == SyntaxTokenKind::Keyword),
1509            "{:?}",
1510            rhai.lines()[0].syntax
1511        );
1512
1513        let go = prepare_document(
1514            &descriptor("package main\nfunc main() {}\n", "go"),
1515            &registry,
1516        )
1517        .unwrap();
1518        assert_eq!(go.language(), "Go");
1519        let plain = prepare_document(&descriptor("hello\n", "not-a-language"), &registry).unwrap();
1520        assert_eq!(plain.language(), "Plain Text");
1521    }
1522
1523    #[test]
1524    fn public_launch_language_pack_resolves_every_declared_language() {
1525        let registry = SyntaxRegistry::new();
1526        let mut missing = Vec::new();
1527        for language in [
1528            "rhai",
1529            "rust",
1530            "go",
1531            "javascript",
1532            "typescript",
1533            "tsx",
1534            "json",
1535            "jsonc",
1536            "toml",
1537            "yaml",
1538            "markdown",
1539            "bash",
1540            "python",
1541            "html",
1542            "css",
1543            "sql",
1544            "dockerfile",
1545            "RUST",
1546        ] {
1547            let document = prepare_document(&descriptor("value = 1\n", language), &registry)
1548                .unwrap_or_else(|error| panic!("{language}: {error}"));
1549            if document.language() == "Plain Text" {
1550                missing.push(language);
1551            }
1552        }
1553        assert!(missing.is_empty(), "missing {missing:?}");
1554    }
1555
1556    #[test]
1557    fn line_ranges_normalize_crlf_but_retain_terminal_empty_line() {
1558        let registry = SyntaxRegistry::new();
1559        let document = prepare_document(&descriptor("one\r\ntwo\n", "text"), &registry).unwrap();
1560        assert_eq!(document.lines().len(), 3);
1561        assert_eq!(document.line_text(0), Some("one"));
1562        assert_eq!(document.line_text(1), Some("two"));
1563        assert_eq!(document.line_text(2), Some(""));
1564        assert!(document.ends_with_newline());
1565    }
1566
1567    #[test]
1568    fn diff_is_direction_neutral_refines_unicode_and_collapses_context() {
1569        let registry = SyntaxRegistry::new();
1570        let left = descriptor(
1571            "same\n城市 = 东京\nunchanged 1\nunchanged 2\nunchanged 3\nunchanged 4\n",
1572            "rhai",
1573        );
1574        let right = descriptor(
1575            "same\n城市 = 上海\nunchanged 1\nunchanged 2\nunchanged 3\nunchanged 4\n",
1576            "rhai",
1577        );
1578        let diff = prepare_diff(&left, &right, DiffWhitespace::Exact, Some(1), &registry).unwrap();
1579        assert_eq!(diff.hunk_count, 1);
1580        let modified = diff
1581            .rows
1582            .iter()
1583            .find(|row| row.kind == DiffRowKind::Modified)
1584            .unwrap();
1585        assert_ne!(modified.left_inline, [] as [std::ops::Range<usize>; 0]);
1586        assert_ne!(modified.right_inline, [] as [std::ops::Range<usize>; 0]);
1587        assert!(
1588            diff.collapsed
1589                .iter()
1590                .any(|row| matches!(row, DiffDisplayRow::Fold { .. }))
1591        );
1592    }
1593
1594    #[test]
1595    fn whitespace_policy_is_explicit() {
1596        let registry = SyntaxRegistry::new();
1597        let left = descriptor("value = 1\n", "rhai");
1598        let right = descriptor("value   =   1\n", "rhai");
1599        assert_eq!(
1600            prepare_diff(&left, &right, DiffWhitespace::Exact, None, &registry)
1601                .unwrap()
1602                .hunk_count,
1603            1
1604        );
1605        assert_eq!(
1606            prepare_diff(
1607                &left,
1608                &right,
1609                DiffWhitespace::IgnoreChanges,
1610                None,
1611                &registry,
1612            )
1613            .unwrap()
1614            .hunk_count,
1615            0
1616        );
1617    }
1618
1619    #[test]
1620    fn host_limits_fail_explicitly_before_unbounded_work() {
1621        let registry = SyntaxRegistry::new();
1622        let descriptor = descriptor("12345", "text");
1623        let limits = DocumentLimits {
1624            max_document_bytes: 4,
1625            ..DocumentLimits::default()
1626        };
1627        assert!(matches!(
1628            prepare_document_with_limits(&descriptor, &registry, limits),
1629            Err(DocumentError::TooManyBytes {
1630                actual: 5,
1631                limit: 4
1632            })
1633        ));
1634        let runtime = DocumentRuntimeConfig::new();
1635        assert_eq!(runtime.limits(), DocumentLimits::default());
1636        assert_eq!(
1637            runtime.set_limits(DocumentLimits {
1638                max_diff_hunks: 0,
1639                ..DocumentLimits::default()
1640            }),
1641            Err(DocumentError::InvalidLimits)
1642        );
1643    }
1644
1645    #[test]
1646    fn host_registered_syntax_participates_in_background_safe_snapshots() {
1647        let registry = SyntaxRegistry::new();
1648        registry
1649            .register_sublime_syntax(
1650                r"%YAML 1.2
1651---
1652name: Probe
1653file_extensions: [probe]
1654scope: source.probe
1655contexts:
1656  main:
1657    - match: '\bprobe\b'
1658      scope: keyword.control.probe
1659",
1660            )
1661            .unwrap();
1662        let document = prepare_document(&descriptor("probe value\n", "probe"), &registry).unwrap();
1663        assert_eq!(document.language(), "Probe");
1664        assert!(
1665            document.lines()[0]
1666                .syntax
1667                .iter()
1668                .any(|span| span.kind == SyntaxTokenKind::Keyword)
1669        );
1670    }
1671}