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<ComponentInstancePath>>,
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    /// Replace one document revision and return exact subscribed components.
465    ///
466    /// # Errors
467    ///
468    /// Returns [`DocumentError::UnknownDocument`] for an unregistered name.
469    pub fn replace(
470        &mut self,
471        name: &str,
472        document: NativeTextDocument,
473    ) -> Result<BTreeSet<ComponentInstancePath>, DocumentError> {
474        let current = self
475            .documents
476            .get_mut(name)
477            .ok_or_else(|| DocumentError::UnknownDocument(name.to_owned()))?;
478        if current == &document {
479            return Ok(BTreeSet::new());
480        }
481        if current.identity() == document.identity() && document.revision() <= current.revision() {
482            return Err(DocumentError::NonMonotonicRevision {
483                identity: document.identity().to_owned(),
484                current: current.revision(),
485                next: document.revision(),
486            });
487        }
488        *current = document;
489        Ok(self.readers.get(name).cloned().unwrap_or_default())
490    }
491
492    pub(crate) fn read_tracked(
493        &mut self,
494        reader: &ComponentInstancePath,
495        name: &str,
496    ) -> Result<NativeTextDocument, DocumentError> {
497        let document = self
498            .documents
499            .get(name)
500            .cloned()
501            .ok_or_else(|| DocumentError::UnknownDocument(name.to_owned()))?;
502        self.readers
503            .entry(name.to_owned())
504            .or_default()
505            .insert(reader.clone());
506        Ok(document)
507    }
508
509    pub(crate) fn reset_reader(&mut self, reader: &ComponentInstancePath) {
510        for readers in self.readers.values_mut() {
511            readers.remove(reader);
512        }
513        self.readers.retain(|_, readers| !readers.is_empty());
514    }
515
516    pub(crate) fn retain_reader_scope(
517        &mut self,
518        root: &ComponentInstancePath,
519        active: &BTreeSet<ComponentInstancePath>,
520    ) {
521        for readers in self.readers.values_mut() {
522            readers.retain(|reader| {
523                !reader.is_within(root) || reader == root || active.contains(reader)
524            });
525        }
526        self.readers.retain(|_, readers| !readers.is_empty());
527    }
528
529    pub(crate) fn remove_reader_scope(&mut self, root: &ComponentInstancePath) {
530        for readers in self.readers.values_mut() {
531            readers.retain(|reader| !reader.is_within(root));
532        }
533        self.readers.retain(|_, readers| !readers.is_empty());
534    }
535}
536
537#[derive(Clone)]
538pub struct SyntaxRegistry {
539    syntax_set: Arc<RwLock<Arc<SyntaxSet>>>,
540}
541
542impl fmt::Debug for SyntaxRegistry {
543    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
544        formatter
545            .debug_struct("SyntaxRegistry")
546            .field("syntaxes", &self.snapshot().syntaxes().len())
547            .finish()
548    }
549}
550
551impl Default for SyntaxRegistry {
552    fn default() -> Self {
553        Self::new()
554    }
555}
556
557impl SyntaxRegistry {
558    #[must_use]
559    pub fn new() -> Self {
560        Self {
561            syntax_set: Arc::new(RwLock::new(Arc::clone(default_syntax_set()))),
562        }
563    }
564
565    /// Register a Sublime `.sublime-syntax` definition for this Engine.
566    ///
567    /// # Errors
568    ///
569    /// Returns a syntax parse or poisoned-registry error.
570    pub fn register_sublime_syntax(&self, source: &str) -> Result<(), DocumentError> {
571        let syntax = SyntaxDefinition::load_from_str(source, true, None)
572            .map_err(|error| DocumentError::Syntax(error.to_string()))?;
573        let current = self
574            .syntax_set
575            .read()
576            .map_err(|_| DocumentError::SyntaxRegistryPoisoned)?
577            .clone();
578        let mut builder = current.as_ref().clone().into_builder();
579        builder.add(syntax);
580        let next = Arc::new(builder.build());
581        *self
582            .syntax_set
583            .write()
584            .map_err(|_| DocumentError::SyntaxRegistryPoisoned)? = next;
585        Ok(())
586    }
587
588    #[must_use]
589    pub fn snapshot(&self) -> Arc<SyntaxSet> {
590        self.syntax_set
591            .read()
592            .map_or_else(|poisoned| poisoned.into_inner().clone(), |set| set.clone())
593    }
594}
595
596fn default_syntax_set() -> &'static Arc<SyntaxSet> {
597    static SYNTAXES: OnceLock<Arc<SyntaxSet>> = OnceLock::new();
598    SYNTAXES.get_or_init(|| {
599        let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
600        for source in EXTRA_SYNTAXES {
601            builder.add(
602                SyntaxDefinition::load_from_str(source, true, None)
603                    .expect("built-in syntax is valid"),
604            );
605        }
606        Arc::new(builder.build())
607    })
608}
609
610#[derive(Clone, Debug, Eq, PartialEq)]
611pub enum DocumentSource {
612    Inline(Arc<str>),
613    Native(NativeTextDocument),
614}
615
616impl DocumentSource {
617    #[must_use]
618    pub fn identity(&self) -> Cow<'_, str> {
619        match self {
620            Self::Inline(_) => Cow::Borrowed("inline"),
621            Self::Native(document) => Cow::Borrowed(document.identity()),
622        }
623    }
624
625    #[must_use]
626    pub fn revision(&self) -> u64 {
627        match self {
628            Self::Inline(text) => stable_text_hash(text),
629            Self::Native(document) => document.revision(),
630        }
631    }
632
633    #[must_use]
634    pub fn text(&self) -> Arc<str> {
635        match self {
636            Self::Inline(text) => Arc::clone(text),
637            Self::Native(document) => document.text_arc(),
638        }
639    }
640}
641
642impl From<String> for DocumentSource {
643    fn from(value: String) -> Self {
644        Self::Inline(Arc::from(value))
645    }
646}
647
648impl From<NativeTextDocument> for DocumentSource {
649    fn from(value: NativeTextDocument) -> Self {
650        Self::Native(value)
651    }
652}
653
654#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
655pub enum DocumentWrap {
656    #[default]
657    None,
658    Viewport,
659    Column(usize),
660}
661
662#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
663pub enum DiffViewMode {
664    #[default]
665    Unified,
666    Split,
667}
668
669#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
670pub enum DiffWhitespace {
671    #[default]
672    Exact,
673    IgnoreChanges,
674    IgnoreAll,
675}
676
677#[derive(Clone, Debug, Eq, PartialEq)]
678pub struct DocumentDescriptor {
679    pub source: DocumentSource,
680    pub label: String,
681    pub file_name: Option<String>,
682    pub language: Option<String>,
683}
684
685#[derive(Clone, Copy, Debug, Eq, PartialEq)]
686pub enum SyntaxTokenKind {
687    Comment,
688    String,
689    Number,
690    Keyword,
691    Function,
692    Type,
693    Variable,
694    Constant,
695    Operator,
696    Punctuation,
697    Tag,
698    Attribute,
699}
700
701impl SyntaxTokenKind {
702    #[must_use]
703    pub const fn theme_token(self) -> &'static str {
704        match self {
705            Self::Comment => "syntax.comment",
706            Self::String => "syntax.string",
707            Self::Number => "syntax.number",
708            Self::Keyword => "syntax.keyword",
709            Self::Function => "syntax.function",
710            Self::Type => "syntax.type",
711            Self::Variable => "syntax.variable",
712            Self::Constant => "syntax.constant",
713            Self::Operator => "syntax.operator",
714            Self::Punctuation => "syntax.punctuation",
715            Self::Tag => "syntax.tag",
716            Self::Attribute => "syntax.attribute",
717        }
718    }
719}
720
721#[derive(Clone, Debug, Eq, PartialEq)]
722pub struct SyntaxSpan {
723    pub range: Range<usize>,
724    pub kind: SyntaxTokenKind,
725}
726
727#[derive(Clone, Debug, Eq, PartialEq)]
728pub struct DocumentLine {
729    pub range: Range<usize>,
730    pub syntax: Vec<SyntaxSpan>,
731}
732
733#[derive(Clone, Debug)]
734pub struct PreparedDocument {
735    identity: String,
736    revision: u64,
737    text: Arc<str>,
738    lines: Arc<[DocumentLine]>,
739    language: String,
740    max_line_chars: usize,
741    ends_with_newline: bool,
742    highlight_error: Option<String>,
743}
744
745impl PreparedDocument {
746    #[must_use]
747    pub fn identity(&self) -> &str {
748        &self.identity
749    }
750
751    #[must_use]
752    pub const fn revision(&self) -> u64 {
753        self.revision
754    }
755
756    #[must_use]
757    pub fn text(&self) -> &str {
758        &self.text
759    }
760
761    #[must_use]
762    pub fn text_arc(&self) -> Arc<str> {
763        Arc::clone(&self.text)
764    }
765
766    #[must_use]
767    pub fn lines(&self) -> &[DocumentLine] {
768        &self.lines
769    }
770
771    #[must_use]
772    pub fn line_text(&self, index: usize) -> Option<&str> {
773        let line = self.lines.get(index)?;
774        self.text.get(line.range.clone())
775    }
776
777    #[must_use]
778    pub fn language(&self) -> &str {
779        &self.language
780    }
781
782    #[must_use]
783    pub const fn max_line_chars(&self) -> usize {
784        self.max_line_chars
785    }
786
787    #[must_use]
788    pub const fn ends_with_newline(&self) -> bool {
789        self.ends_with_newline
790    }
791
792    #[must_use]
793    pub fn highlight_error(&self) -> Option<&str> {
794        self.highlight_error.as_deref()
795    }
796}
797
798/// Prepare one immutable text snapshot with default resource limits.
799///
800/// # Errors
801///
802/// Returns document budget errors. Syntax highlighting failures degrade to
803/// plain text and are recorded on the returned snapshot.
804pub fn prepare_document(
805    descriptor: &DocumentDescriptor,
806    syntaxes: &SyntaxRegistry,
807) -> Result<PreparedDocument, DocumentError> {
808    prepare_document_with_limits(descriptor, syntaxes, DocumentLimits::default())
809}
810
811/// Prepare one immutable text snapshot with Host-provided resource limits.
812///
813/// # Errors
814///
815/// Returns when byte or line budgets are exceeded.
816pub fn prepare_document_with_limits(
817    descriptor: &DocumentDescriptor,
818    syntaxes: &SyntaxRegistry,
819    limits: DocumentLimits,
820) -> Result<PreparedDocument, DocumentError> {
821    let text = descriptor.source.text();
822    if text.len() > limits.max_document_bytes {
823        return Err(DocumentError::TooManyBytes {
824            actual: text.len(),
825            limit: limits.max_document_bytes,
826        });
827    }
828    let ranges = document_line_ranges(&text);
829    if ranges.len() > limits.max_document_lines {
830        return Err(DocumentError::TooManyLines {
831            actual: ranges.len(),
832            limit: limits.max_document_lines,
833        });
834    }
835    let syntax_set = syntaxes.snapshot();
836    let syntax = resolve_syntax(
837        &syntax_set,
838        descriptor.language.as_deref(),
839        descriptor.file_name.as_deref(),
840    );
841    let language = syntax.name.clone();
842    let mut parser = ParseState::new(syntax);
843    let mut stack = ScopeStack::new();
844    let selectors = syntax_selectors();
845    let mut lines = Vec::with_capacity(ranges.len());
846    let mut max_line_chars = 0usize;
847    let mut highlight_error = None;
848    let mut highlighting = true;
849    for range in ranges {
850        let line = &text[range.clone()];
851        max_line_chars = max_line_chars.max(line.chars().count());
852        let parse_end = text[range.end..]
853            .find('\n')
854            .map_or(range.end, |offset| range.end + offset + 1);
855        let parse_start = range.start;
856        let parse_line = &text[parse_start..parse_end];
857        let mut spans: Vec<SyntaxSpan> = Vec::new();
858        if highlighting {
859            match parser.parse_line(parse_line, &syntax_set) {
860                Ok(operations) => {
861                    for (token_range, operation) in ScopeRangeIterator::new(&operations, parse_line)
862                    {
863                        if let Err(error) = stack.apply(operation) {
864                            highlight_error.get_or_insert_with(|| error.to_string());
865                            highlighting = false;
866                            spans.clear();
867                            break;
868                        }
869                        let start = token_range.start.min(line.len());
870                        let end = token_range.end.min(line.len());
871                        if start >= end {
872                            continue;
873                        }
874                        if let Some(kind) = selectors.kind(stack.as_slice()) {
875                            if let Some(previous) = spans.last_mut()
876                                && previous.kind == kind
877                                && previous.range.end == start
878                            {
879                                previous.range.end = end;
880                            } else {
881                                spans.push(SyntaxSpan {
882                                    range: start..end,
883                                    kind,
884                                });
885                            }
886                        }
887                    }
888                }
889                Err(error) => {
890                    highlight_error.get_or_insert_with(|| error.to_string());
891                    highlighting = false;
892                }
893            }
894        }
895        lines.push(DocumentLine {
896            range,
897            syntax: spans,
898        });
899    }
900    Ok(PreparedDocument {
901        identity: descriptor.source.identity().into_owned(),
902        revision: descriptor.source.revision(),
903        text,
904        lines: lines.into(),
905        language,
906        max_line_chars,
907        ends_with_newline: descriptor.source.text().ends_with('\n'),
908        highlight_error,
909    })
910}
911
912fn document_line_ranges(text: &str) -> Vec<Range<usize>> {
913    let mut ranges = Vec::new();
914    let mut start = 0usize;
915    for (index, byte) in text.bytes().enumerate() {
916        if byte == b'\n' {
917            let mut end = index;
918            if text.as_bytes().get(index.wrapping_sub(1)) == Some(&b'\r') && index > start {
919                end -= 1;
920            }
921            ranges.push(start..end);
922            start = index + 1;
923        }
924    }
925    if start < text.len() || text.is_empty() || text.ends_with('\n') {
926        ranges.push(start..text.len());
927    }
928    ranges
929}
930
931fn resolve_syntax<'a>(
932    syntaxes: &'a SyntaxSet,
933    language: Option<&str>,
934    file_name: Option<&str>,
935) -> &'a syntect::parsing::SyntaxReference {
936    language
937        .and_then(|language| {
938            let token = language_alias(language);
939            syntaxes.find_syntax_by_token(token.as_ref())
940        })
941        .or_else(|| {
942            file_name.and_then(|file_name| {
943                syntaxes.find_syntax_by_extension(file_name).or_else(|| {
944                    file_name
945                        .rsplit_once('.')
946                        .and_then(|(_, ext)| syntaxes.find_syntax_by_extension(ext))
947                })
948            })
949        })
950        .unwrap_or_else(|| syntaxes.find_syntax_plain_text())
951}
952
953fn language_alias(language: &str) -> Cow<'_, str> {
954    let normalized = language.trim().to_ascii_lowercase();
955    match normalized.as_str() {
956        "plaintext" | "plain_text" | "text" => Cow::Borrowed("txt"),
957        "javascript" => Cow::Borrowed("js"),
958        "typescript" => Cow::Borrowed("ts"),
959        "shell" | "bash" => Cow::Borrowed("sh"),
960        "python" => Cow::Borrowed("py"),
961        "markdown" => Cow::Borrowed("md"),
962        "rust" => Cow::Borrowed("rs"),
963        "golang" => Cow::Borrowed("go"),
964        _ => Cow::Owned(normalized),
965    }
966}
967
968struct SyntaxSelectors {
969    comment: ScopeSelectors,
970    string: ScopeSelectors,
971    number: ScopeSelectors,
972    keyword: ScopeSelectors,
973    function: ScopeSelectors,
974    type_name: ScopeSelectors,
975    variable: ScopeSelectors,
976    constant: ScopeSelectors,
977    operator: ScopeSelectors,
978    punctuation: ScopeSelectors,
979    tag: ScopeSelectors,
980    attribute: ScopeSelectors,
981}
982
983impl SyntaxSelectors {
984    fn new() -> Self {
985        let parse = |value: &str| ScopeSelectors::from_str(value).expect("static scope selector");
986        Self {
987            comment: parse("comment"),
988            string: parse("string"),
989            number: parse("constant.numeric"),
990            keyword: parse("keyword, storage.modifier"),
991            function: parse("entity.name.function, support.function"),
992            type_name: parse("entity.name.type, support.type, storage.type"),
993            variable: parse("variable"),
994            constant: parse("constant"),
995            operator: parse("keyword.operator"),
996            punctuation: parse("punctuation"),
997            tag: parse("entity.name.tag"),
998            attribute: parse("entity.other.attribute-name"),
999        }
1000    }
1001
1002    fn kind(&self, stack: &[syntect::parsing::Scope]) -> Option<SyntaxTokenKind> {
1003        [
1004            (&self.comment, SyntaxTokenKind::Comment),
1005            (&self.string, SyntaxTokenKind::String),
1006            (&self.number, SyntaxTokenKind::Number),
1007            (&self.function, SyntaxTokenKind::Function),
1008            (&self.type_name, SyntaxTokenKind::Type),
1009            (&self.tag, SyntaxTokenKind::Tag),
1010            (&self.attribute, SyntaxTokenKind::Attribute),
1011            (&self.operator, SyntaxTokenKind::Operator),
1012            (&self.keyword, SyntaxTokenKind::Keyword),
1013            (&self.variable, SyntaxTokenKind::Variable),
1014            (&self.constant, SyntaxTokenKind::Constant),
1015            (&self.punctuation, SyntaxTokenKind::Punctuation),
1016        ]
1017        .into_iter()
1018        .find_map(|(selector, kind)| selector.does_match(stack).map(|_| kind))
1019    }
1020}
1021
1022fn syntax_selectors() -> &'static SyntaxSelectors {
1023    static SELECTORS: OnceLock<SyntaxSelectors> = OnceLock::new();
1024    SELECTORS.get_or_init(SyntaxSelectors::new)
1025}
1026
1027#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1028pub enum DiffRowKind {
1029    Equal,
1030    LeftOnly,
1031    RightOnly,
1032    Modified,
1033}
1034
1035#[derive(Clone, Debug, Eq, PartialEq)]
1036pub struct DiffAlignedRow {
1037    pub left: Option<usize>,
1038    pub right: Option<usize>,
1039    pub kind: DiffRowKind,
1040    pub hunk: Option<usize>,
1041    pub left_inline: Vec<Range<usize>>,
1042    pub right_inline: Vec<Range<usize>>,
1043}
1044
1045#[derive(Clone, Debug, Eq, PartialEq)]
1046pub enum DiffDisplayRow {
1047    Content(usize),
1048    Fold {
1049        id: usize,
1050        full_range: Range<usize>,
1051        hidden_rows: usize,
1052    },
1053}
1054
1055#[derive(Clone, Debug)]
1056pub struct PreparedDiff {
1057    pub left: PreparedDocument,
1058    pub right: PreparedDocument,
1059    pub rows: Arc<[DiffAlignedRow]>,
1060    pub collapsed: Arc<[DiffDisplayRow]>,
1061    pub hunk_count: usize,
1062}
1063
1064/// Prepare a two-way comparison with default resource limits.
1065///
1066/// # Errors
1067///
1068/// Returns document or diff budget errors.
1069pub fn prepare_diff(
1070    left: &DocumentDescriptor,
1071    right: &DocumentDescriptor,
1072    whitespace: DiffWhitespace,
1073    context_lines: Option<usize>,
1074    syntaxes: &SyntaxRegistry,
1075) -> Result<PreparedDiff, DocumentError> {
1076    prepare_diff_with_limits(
1077        left,
1078        right,
1079        whitespace,
1080        context_lines,
1081        syntaxes,
1082        DocumentLimits::default(),
1083    )
1084}
1085
1086/// Prepare a two-way comparison with Host-provided resource limits.
1087///
1088/// # Errors
1089///
1090/// Returns when document, combined-input, or hunk budgets are exceeded.
1091#[allow(clippy::too_many_lines)]
1092pub fn prepare_diff_with_limits(
1093    left: &DocumentDescriptor,
1094    right: &DocumentDescriptor,
1095    whitespace: DiffWhitespace,
1096    context_lines: Option<usize>,
1097    syntaxes: &SyntaxRegistry,
1098    limits: DocumentLimits,
1099) -> Result<PreparedDiff, DocumentError> {
1100    let total_bytes = left
1101        .source
1102        .text()
1103        .len()
1104        .saturating_add(right.source.text().len());
1105    if total_bytes > limits.max_diff_total_bytes {
1106        return Err(DocumentError::TooManyDiffBytes {
1107            actual: total_bytes,
1108            limit: limits.max_diff_total_bytes,
1109        });
1110    }
1111    let left = prepare_document_with_limits(left, syntaxes, limits)?;
1112    let right = prepare_document_with_limits(right, syntaxes, limits)?;
1113    let left_keys = diff_keys(&left, whitespace);
1114    let right_keys = diff_keys(&right, whitespace);
1115    let deadline = Instant::now().checked_add(limits.diff_timeout);
1116    let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_keys, &right_keys, deadline);
1117    let mut rows = Vec::with_capacity(left.lines().len().max(right.lines().len()));
1118    let mut hunk = 0usize;
1119    let mut in_change = false;
1120    for operation in ops {
1121        match operation {
1122            DiffOp::Equal {
1123                old_index,
1124                new_index,
1125                len,
1126            } => {
1127                in_change = false;
1128                rows.extend((0..len).map(|offset| DiffAlignedRow {
1129                    left: Some(old_index + offset),
1130                    right: Some(new_index + offset),
1131                    kind: DiffRowKind::Equal,
1132                    hunk: None,
1133                    left_inline: Vec::new(),
1134                    right_inline: Vec::new(),
1135                }));
1136            }
1137            DiffOp::Delete {
1138                old_index, old_len, ..
1139            } => {
1140                let id = next_hunk(&mut hunk, &mut in_change, limits.max_diff_hunks)?;
1141                rows.extend((0..old_len).map(|offset| DiffAlignedRow {
1142                    left: Some(old_index + offset),
1143                    right: None,
1144                    kind: DiffRowKind::LeftOnly,
1145                    hunk: Some(id),
1146                    left_inline: Vec::new(),
1147                    right_inline: Vec::new(),
1148                }));
1149            }
1150            DiffOp::Insert {
1151                new_index, new_len, ..
1152            } => {
1153                let id = next_hunk(&mut hunk, &mut in_change, limits.max_diff_hunks)?;
1154                rows.extend((0..new_len).map(|offset| DiffAlignedRow {
1155                    left: None,
1156                    right: Some(new_index + offset),
1157                    kind: DiffRowKind::RightOnly,
1158                    hunk: Some(id),
1159                    left_inline: Vec::new(),
1160                    right_inline: Vec::new(),
1161                }));
1162            }
1163            DiffOp::Replace {
1164                old_index,
1165                old_len,
1166                new_index,
1167                new_len,
1168            } => {
1169                let id = next_hunk(&mut hunk, &mut in_change, limits.max_diff_hunks)?;
1170                let aligned = old_len.max(new_len);
1171                for offset in 0..aligned {
1172                    let left_index = (offset < old_len).then_some(old_index + offset);
1173                    let right_index = (offset < new_len).then_some(new_index + offset);
1174                    let (left_inline, right_inline) = match (left_index, right_index) {
1175                        (Some(left_index), Some(right_index)) => inline_ranges(
1176                            left.line_text(left_index).unwrap_or_default(),
1177                            right.line_text(right_index).unwrap_or_default(),
1178                            deadline,
1179                        ),
1180                        _ => (Vec::new(), Vec::new()),
1181                    };
1182                    rows.push(DiffAlignedRow {
1183                        left: left_index,
1184                        right: right_index,
1185                        kind: match (left_index, right_index) {
1186                            (Some(_), Some(_)) => DiffRowKind::Modified,
1187                            (Some(_), None) => DiffRowKind::LeftOnly,
1188                            (None, Some(_)) => DiffRowKind::RightOnly,
1189                            (None, None) => unreachable!(),
1190                        },
1191                        hunk: Some(id),
1192                        left_inline,
1193                        right_inline,
1194                    });
1195                }
1196            }
1197        }
1198    }
1199    let collapsed = collapse_rows(&rows, context_lines);
1200    Ok(PreparedDiff {
1201        left,
1202        right,
1203        rows: rows.into(),
1204        collapsed: collapsed.into(),
1205        hunk_count: hunk,
1206    })
1207}
1208
1209fn next_hunk(
1210    hunks: &mut usize,
1211    in_change: &mut bool,
1212    limit: usize,
1213) -> Result<usize, DocumentError> {
1214    if !*in_change {
1215        *hunks = hunks.checked_add(1).ok_or(DocumentError::TooManyHunks {
1216            actual: usize::MAX,
1217            limit,
1218        })?;
1219        *in_change = true;
1220    }
1221    if *hunks > limit {
1222        return Err(DocumentError::TooManyHunks {
1223            actual: *hunks,
1224            limit,
1225        });
1226    }
1227    Ok(*hunks - 1)
1228}
1229
1230fn diff_keys(document: &PreparedDocument, whitespace: DiffWhitespace) -> Vec<Cow<'_, str>> {
1231    document
1232        .lines()
1233        .iter()
1234        .enumerate()
1235        .map(|(index, _)| {
1236            let line = document.line_text(index).unwrap_or_default();
1237            match whitespace {
1238                DiffWhitespace::Exact => Cow::Borrowed(line),
1239                DiffWhitespace::IgnoreChanges => {
1240                    Cow::Owned(line.split_whitespace().collect::<Vec<_>>().join(" "))
1241                }
1242                DiffWhitespace::IgnoreAll => Cow::Owned(
1243                    line.chars()
1244                        .filter(|character| !character.is_whitespace())
1245                        .collect(),
1246                ),
1247            }
1248        })
1249        .collect()
1250}
1251
1252fn inline_ranges(
1253    left: &str,
1254    right: &str,
1255    deadline: Option<Instant>,
1256) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
1257    if left.len().saturating_add(right.len()) > MAX_INLINE_DIFF_BYTES {
1258        return (
1259            (!left.is_empty())
1260                .then_some(0..left.len())
1261                .into_iter()
1262                .collect(),
1263            (!right.is_empty())
1264                .then_some(0..right.len())
1265                .into_iter()
1266                .collect(),
1267        );
1268    }
1269    let left_graphemes = left.graphemes(true).collect::<Vec<_>>();
1270    let right_graphemes = right.graphemes(true).collect::<Vec<_>>();
1271    let operations = capture_diff_slices_deadline(
1272        Algorithm::Patience,
1273        &left_graphemes,
1274        &right_graphemes,
1275        deadline,
1276    );
1277    let mut left_offset = 0usize;
1278    let mut right_offset = 0usize;
1279    let mut left_ranges = Vec::new();
1280    let mut right_ranges = Vec::new();
1281    for operation in operations {
1282        for change in operation.iter_changes(&left_graphemes, &right_graphemes) {
1283            let len = change.value().len();
1284            match change.tag() {
1285                similar::ChangeTag::Equal => {
1286                    left_offset += len;
1287                    right_offset += len;
1288                }
1289                similar::ChangeTag::Delete => {
1290                    push_range(&mut left_ranges, left_offset..left_offset + len);
1291                    left_offset += len;
1292                }
1293                similar::ChangeTag::Insert => {
1294                    push_range(&mut right_ranges, right_offset..right_offset + len);
1295                    right_offset += len;
1296                }
1297            }
1298        }
1299    }
1300    (left_ranges, right_ranges)
1301}
1302
1303fn push_range(ranges: &mut Vec<Range<usize>>, range: Range<usize>) {
1304    if range.is_empty() {
1305        return;
1306    }
1307    if let Some(previous) = ranges.last_mut()
1308        && previous.end == range.start
1309    {
1310        previous.end = range.end;
1311    } else {
1312        ranges.push(range);
1313    }
1314}
1315
1316fn collapse_rows(rows: &[DiffAlignedRow], context: Option<usize>) -> Vec<DiffDisplayRow> {
1317    let Some(context) = context else {
1318        return (0..rows.len()).map(DiffDisplayRow::Content).collect();
1319    };
1320    if rows.iter().all(|row| row.kind == DiffRowKind::Equal) {
1321        return if rows.is_empty() {
1322            Vec::new()
1323        } else {
1324            vec![DiffDisplayRow::Fold {
1325                id: 0,
1326                full_range: 0..rows.len(),
1327                hidden_rows: rows.len(),
1328            }]
1329        };
1330    }
1331    let mut keep = vec![false; rows.len()];
1332    for (index, row) in rows.iter().enumerate() {
1333        if row.kind != DiffRowKind::Equal {
1334            let start = index.saturating_sub(context);
1335            let end = index
1336                .saturating_add(context)
1337                .saturating_add(1)
1338                .min(rows.len());
1339            keep[start..end].fill(true);
1340        }
1341    }
1342    let mut display = Vec::new();
1343    let mut index = 0usize;
1344    let mut fold = 0usize;
1345    while index < rows.len() {
1346        if keep[index] {
1347            display.push(DiffDisplayRow::Content(index));
1348            index += 1;
1349            continue;
1350        }
1351        let start = index;
1352        while index < rows.len() && !keep[index] {
1353            index += 1;
1354        }
1355        display.push(DiffDisplayRow::Fold {
1356            id: fold,
1357            full_range: start..index,
1358            hidden_rows: index - start,
1359        });
1360        fold += 1;
1361    }
1362    display
1363}
1364
1365fn stable_text_hash(text: &str) -> u64 {
1366    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1367    const PRIME: u64 = 0x0000_0100_0000_01b3;
1368    text.bytes().fold(OFFSET, |hash, byte| {
1369        (hash ^ u64::from(byte)).wrapping_mul(PRIME)
1370    })
1371}
1372
1373fn validate_identity(value: &str) -> Result<(), DocumentError> {
1374    if value.is_empty()
1375        || value.len() > 256
1376        || !value.chars().all(|character| {
1377            character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | '/' | ':')
1378        })
1379    {
1380        Err(DocumentError::InvalidIdentity(value.to_owned()))
1381    } else {
1382        Ok(())
1383    }
1384}
1385
1386#[derive(Clone, Debug, Error, Eq, PartialEq)]
1387pub enum DocumentError {
1388    #[error("document identity `{0}` must contain 1-256 safe ASCII characters")]
1389    InvalidIdentity(String),
1390    #[error("native text document `{0}` is already registered")]
1391    DuplicateDocument(String),
1392    #[error("native text document `{0}` is not registered")]
1393    UnknownDocument(String),
1394    #[error(
1395        "native text document `{identity}` revision must increase: current {current}, next {next}"
1396    )]
1397    NonMonotonicRevision {
1398        identity: String,
1399        current: u64,
1400        next: u64,
1401    },
1402    #[error("document contains {actual} bytes, exceeding the {limit}-byte budget")]
1403    TooManyBytes { actual: usize, limit: usize },
1404    #[error("diff inputs contain {actual} bytes, exceeding the {limit}-byte budget")]
1405    TooManyDiffBytes { actual: usize, limit: usize },
1406    #[error("document contains {actual} lines, exceeding the {limit}-line budget")]
1407    TooManyLines { actual: usize, limit: usize },
1408    #[error("diff contains {actual} hunks, exceeding the {limit}-hunk budget")]
1409    TooManyHunks { actual: usize, limit: usize },
1410    #[error("syntax definition or parse failed: {0}")]
1411    Syntax(String),
1412    #[error("syntax registry lock was poisoned")]
1413    SyntaxRegistryPoisoned,
1414    #[error("document resource limits must all be greater than zero")]
1415    InvalidLimits,
1416    #[error("document runtime configuration lock was poisoned")]
1417    DocumentConfigPoisoned,
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422    use super::*;
1423
1424    fn descriptor(text: &str, language: &str) -> DocumentDescriptor {
1425        DocumentDescriptor {
1426            source: DocumentSource::from(text.to_owned()),
1427            label: language.to_owned(),
1428            file_name: None,
1429            language: Some(language.to_owned()),
1430        }
1431    }
1432
1433    #[test]
1434    fn native_documents_are_revisioned_and_registry_reads_are_exact() {
1435        let first = NativeTextDocument::new("server/a", 1, Arc::<str>::from("port=80\n")).unwrap();
1436        let second =
1437            NativeTextDocument::new("server/a", 2, Arc::<str>::from("port=443\n")).unwrap();
1438        let reader = ComponentInstancePath::root("View", "main");
1439        let mut registry = NativeTextDocumentRegistry::new();
1440        registry.register("config", first).unwrap();
1441        assert_eq!(
1442            registry.read_tracked(&reader, "config").unwrap().revision(),
1443            1
1444        );
1445        assert_eq!(
1446            registry.replace("config", second).unwrap(),
1447            BTreeSet::from([reader])
1448        );
1449        let stale = NativeTextDocument::new("server/a", 2, Arc::<str>::from("stale")).unwrap();
1450        assert!(matches!(
1451            registry.replace("config", stale),
1452            Err(DocumentError::NonMonotonicRevision { .. })
1453        ));
1454    }
1455
1456    #[test]
1457    fn rhai_and_go_are_built_in_and_unknown_languages_fall_back_to_plain_text() {
1458        let registry = SyntaxRegistry::new();
1459        let rhai = prepare_document(&descriptor("fn view() { 42 }\n", "rhai"), &registry).unwrap();
1460        assert_eq!(rhai.language(), "Rhai");
1461        assert!(
1462            rhai.lines()[0]
1463                .syntax
1464                .iter()
1465                .any(|span| span.kind == SyntaxTokenKind::Keyword),
1466            "{:?}",
1467            rhai.lines()[0].syntax
1468        );
1469
1470        let go = prepare_document(
1471            &descriptor("package main\nfunc main() {}\n", "go"),
1472            &registry,
1473        )
1474        .unwrap();
1475        assert_eq!(go.language(), "Go");
1476        let plain = prepare_document(&descriptor("hello\n", "not-a-language"), &registry).unwrap();
1477        assert_eq!(plain.language(), "Plain Text");
1478    }
1479
1480    #[test]
1481    fn public_launch_language_pack_resolves_every_declared_language() {
1482        let registry = SyntaxRegistry::new();
1483        let mut missing = Vec::new();
1484        for language in [
1485            "rhai",
1486            "rust",
1487            "go",
1488            "javascript",
1489            "typescript",
1490            "tsx",
1491            "json",
1492            "jsonc",
1493            "toml",
1494            "yaml",
1495            "markdown",
1496            "bash",
1497            "python",
1498            "html",
1499            "css",
1500            "sql",
1501            "dockerfile",
1502            "RUST",
1503        ] {
1504            let document = prepare_document(&descriptor("value = 1\n", language), &registry)
1505                .unwrap_or_else(|error| panic!("{language}: {error}"));
1506            if document.language() == "Plain Text" {
1507                missing.push(language);
1508            }
1509        }
1510        assert!(missing.is_empty(), "missing {missing:?}");
1511    }
1512
1513    #[test]
1514    fn line_ranges_normalize_crlf_but_retain_terminal_empty_line() {
1515        let registry = SyntaxRegistry::new();
1516        let document = prepare_document(&descriptor("one\r\ntwo\n", "text"), &registry).unwrap();
1517        assert_eq!(document.lines().len(), 3);
1518        assert_eq!(document.line_text(0), Some("one"));
1519        assert_eq!(document.line_text(1), Some("two"));
1520        assert_eq!(document.line_text(2), Some(""));
1521        assert!(document.ends_with_newline());
1522    }
1523
1524    #[test]
1525    fn diff_is_direction_neutral_refines_unicode_and_collapses_context() {
1526        let registry = SyntaxRegistry::new();
1527        let left = descriptor(
1528            "same\n城市 = 东京\nunchanged 1\nunchanged 2\nunchanged 3\nunchanged 4\n",
1529            "rhai",
1530        );
1531        let right = descriptor(
1532            "same\n城市 = 上海\nunchanged 1\nunchanged 2\nunchanged 3\nunchanged 4\n",
1533            "rhai",
1534        );
1535        let diff = prepare_diff(&left, &right, DiffWhitespace::Exact, Some(1), &registry).unwrap();
1536        assert_eq!(diff.hunk_count, 1);
1537        let modified = diff
1538            .rows
1539            .iter()
1540            .find(|row| row.kind == DiffRowKind::Modified)
1541            .unwrap();
1542        assert!(!modified.left_inline.is_empty());
1543        assert!(!modified.right_inline.is_empty());
1544        assert!(
1545            diff.collapsed
1546                .iter()
1547                .any(|row| matches!(row, DiffDisplayRow::Fold { .. }))
1548        );
1549    }
1550
1551    #[test]
1552    fn whitespace_policy_is_explicit() {
1553        let registry = SyntaxRegistry::new();
1554        let left = descriptor("value = 1\n", "rhai");
1555        let right = descriptor("value   =   1\n", "rhai");
1556        assert_eq!(
1557            prepare_diff(&left, &right, DiffWhitespace::Exact, None, &registry)
1558                .unwrap()
1559                .hunk_count,
1560            1
1561        );
1562        assert_eq!(
1563            prepare_diff(
1564                &left,
1565                &right,
1566                DiffWhitespace::IgnoreChanges,
1567                None,
1568                &registry,
1569            )
1570            .unwrap()
1571            .hunk_count,
1572            0
1573        );
1574    }
1575
1576    #[test]
1577    fn host_limits_fail_explicitly_before_unbounded_work() {
1578        let registry = SyntaxRegistry::new();
1579        let descriptor = descriptor("12345", "text");
1580        let limits = DocumentLimits {
1581            max_document_bytes: 4,
1582            ..DocumentLimits::default()
1583        };
1584        assert!(matches!(
1585            prepare_document_with_limits(&descriptor, &registry, limits),
1586            Err(DocumentError::TooManyBytes {
1587                actual: 5,
1588                limit: 4
1589            })
1590        ));
1591        let runtime = DocumentRuntimeConfig::new();
1592        assert_eq!(runtime.limits(), DocumentLimits::default());
1593        assert_eq!(
1594            runtime.set_limits(DocumentLimits {
1595                max_diff_hunks: 0,
1596                ..DocumentLimits::default()
1597            }),
1598            Err(DocumentError::InvalidLimits)
1599        );
1600    }
1601
1602    #[test]
1603    fn host_registered_syntax_participates_in_background_safe_snapshots() {
1604        let registry = SyntaxRegistry::new();
1605        registry
1606            .register_sublime_syntax(
1607                r"%YAML 1.2
1608---
1609name: Probe
1610file_extensions: [probe]
1611scope: source.probe
1612contexts:
1613  main:
1614    - match: '\bprobe\b'
1615      scope: keyword.control.probe
1616",
1617            )
1618            .unwrap();
1619        let document = prepare_document(&descriptor("probe value\n", "probe"), &registry).unwrap();
1620        assert_eq!(document.language(), "Probe");
1621        assert!(
1622            document.lines()[0]
1623                .syntax
1624                .iter()
1625                .any(|span| span.kind == SyntaxTokenKind::Keyword)
1626        );
1627    }
1628}