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