1use 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 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 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 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 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 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 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
798pub fn prepare_document(
805 descriptor: &DocumentDescriptor,
806 syntaxes: &SyntaxRegistry,
807) -> Result<PreparedDocument, DocumentError> {
808 prepare_document_with_limits(descriptor, syntaxes, DocumentLimits::default())
809}
810
811pub 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
1064pub 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#[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"), ®istry).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 ®istry,
1473 )
1474 .unwrap();
1475 assert_eq!(go.language(), "Go");
1476 let plain = prepare_document(&descriptor("hello\n", "not-a-language"), ®istry).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), ®istry)
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"), ®istry).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), ®istry).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, ®istry)
1558 .unwrap()
1559 .hunk_count,
1560 1
1561 );
1562 assert_eq!(
1563 prepare_diff(
1564 &left,
1565 &right,
1566 DiffWhitespace::IgnoreChanges,
1567 None,
1568 ®istry,
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, ®istry, 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"), ®istry).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}