1use std::collections::{HashMap, HashSet};
2use std::sync::OnceLock;
3
4use tower_lsp::lsp_types::{
5 SemanticToken, SemanticTokenType, SemanticTokensLegend, SemanticTokenModifier,
6};
7
8use logicaffeine_language::token::{BlockType, Span, Token, TokenType};
9
10use crate::document::DocumentState;
11use crate::index::DefinitionKind;
12use crate::line_index::LineIndex;
13
14pub const TOKEN_TYPES: &[SemanticTokenType] = &[
18 SemanticTokenType::KEYWORD, SemanticTokenType::TYPE, SemanticTokenType::FUNCTION, SemanticTokenType::VARIABLE, SemanticTokenType::STRING, SemanticTokenType::NUMBER, SemanticTokenType::OPERATOR, SemanticTokenType::NAMESPACE, SemanticTokenType::MODIFIER, SemanticTokenType::PROPERTY, SemanticTokenType::COMMENT, SemanticTokenType::PARAMETER, SemanticTokenType::ENUM_MEMBER, ];
32
33pub const TOKEN_MODIFIERS: &[SemanticTokenModifier] = &[
34 SemanticTokenModifier::DECLARATION, SemanticTokenModifier::READONLY, SemanticTokenModifier::MODIFICATION, SemanticTokenModifier::DEFAULT_LIBRARY, ];
39
40pub const TYPE_KEYWORD: u32 = 0;
41pub const TYPE_TYPE: u32 = 1;
42pub const TYPE_FUNCTION: u32 = 2;
43pub const TYPE_VARIABLE: u32 = 3;
44pub const TYPE_STRING: u32 = 4;
45pub const TYPE_NUMBER: u32 = 5;
46pub const TYPE_OPERATOR: u32 = 6;
47pub const TYPE_NAMESPACE: u32 = 7;
48pub const TYPE_MODIFIER: u32 = 8;
49pub const TYPE_PROPERTY: u32 = 9;
50pub const TYPE_COMMENT: u32 = 10;
51pub const TYPE_PARAMETER: u32 = 11;
52pub const TYPE_ENUM_MEMBER: u32 = 12;
53
54pub const MOD_DECLARATION: u32 = 1 << 0;
55pub const MOD_READONLY: u32 = 1 << 1;
56pub const MOD_MODIFICATION: u32 = 1 << 2;
57pub const MOD_DEFAULT_LIBRARY: u32 = 1 << 3;
58
59pub fn legend() -> SemanticTokensLegend {
60 SemanticTokensLegend {
61 token_types: TOKEN_TYPES.to_vec(),
62 token_modifiers: TOKEN_MODIFIERS.to_vec(),
63 }
64}
65
66pub fn encode_document_tokens(doc: &DocumentState) -> Vec<SemanticToken> {
76 let overlay = ResolutionOverlay::build(doc);
77 let spans = paint_spans(doc, &overlay, None);
78 encode_spans(&spans, &doc.line_index)
79}
80
81fn paint_spans(
86 doc: &DocumentState,
87 overlay: &ResolutionOverlay,
88 range: Option<(usize, usize)>,
89) -> Vec<(Span, u32, u32)> {
90 let mut spans = Vec::new();
91 for token in &doc.tokens {
92 if let Some((start, end)) = range {
93 if token.span.end <= start || token.span.start >= end {
94 continue;
95 }
96 }
97 let (token_type, modifiers) = overlay.classify(token, doc);
98 let Some(token_type) = token_type else { continue };
99 if matches!(token.kind, TokenType::InterpolatedString(_)) && token_type == TYPE_STRING {
100 expand_interpolation(doc, token, &mut spans);
101 } else {
102 spans.push((token.span, token_type, modifiers));
103 }
104 }
105 spans
106}
107
108fn expand_interpolation(doc: &DocumentState, token: &Token, out: &mut Vec<(Span, u32, u32)>) {
111 let Some(src) = doc.source.get(token.span.start..token.span.end) else {
112 out.push((token.span, TYPE_STRING, 0));
113 return;
114 };
115 let base = token.span.start;
116 let bytes = src.as_bytes();
117 let mut segment_start = 0usize;
118 let mut i = 0usize;
119 while i < bytes.len() {
120 match bytes[i] {
121 b'{' if bytes.get(i + 1) == Some(&b'{') => i += 2,
122 b'{' => {
123 let Some(close_rel) = src[i + 1..].find('}') else {
124 i += 1;
125 continue;
126 };
127 let close = i + 1 + close_rel;
128 if i > segment_start {
129 out.push((Span::new(base + segment_start, base + i), TYPE_STRING, 0));
130 }
131 out.push((Span::new(base + i, base + i + 1), TYPE_OPERATOR, 0));
132 paint_fragment(doc, &src[i + 1..close], base + i + 1, out);
133 out.push((Span::new(base + close, base + close + 1), TYPE_OPERATOR, 0));
134 segment_start = close + 1;
135 i = close + 1;
136 }
137 _ => i += 1,
138 }
139 }
140 if segment_start < src.len() {
141 out.push((Span::new(base + segment_start, base + src.len()), TYPE_STRING, 0));
142 }
143}
144
145fn paint_fragment(
150 doc: &DocumentState,
151 fragment: &str,
152 offset: usize,
153 out: &mut Vec<(Span, u32, u32)>,
154) {
155 let mut interner = logicaffeine_base::Interner::new();
156 let mut lexer = logicaffeine_language::lexer::Lexer::new(fragment, &mut interner);
157 let tokens = lexer.tokenize();
158 for token in tokens {
159 if token.span.end > fragment.len() || token.span.start >= token.span.end {
160 continue;
161 }
162 let (token_type, mut modifiers) = classify_token(&token.kind);
163 let Some(mut token_type) = token_type else { continue };
164 if let Some(name) = crate::index::resolve_token_name(&token, &interner) {
167 if let Some(def) = doc.symbol_index.definitions_of(name).first() {
168 token_type = semantic_type_for(&def.kind);
169 modifiers = readonly_bit(def);
170 }
171 }
172 out.push((
173 Span::new(offset + token.span.start, offset + token.span.end),
174 token_type,
175 modifiers,
176 ));
177 }
178}
179
180fn encode_spans(spans: &[(Span, u32, u32)], line_index: &LineIndex) -> Vec<SemanticToken> {
182 let mut result = Vec::with_capacity(spans.len());
183 let mut prev_line = 0u32;
184 let mut prev_start = 0u32;
185 for (span, token_type, modifiers) in spans {
186 let pos = line_index.position(span.start);
187 let length = line_index.utf16_length(span.start, span.end);
188 if length == 0 {
189 continue;
190 }
191 let delta_line = pos.line - prev_line;
192 let delta_start =
193 if delta_line == 0 { pos.character - prev_start } else { pos.character };
194 result.push(SemanticToken {
195 delta_line,
196 delta_start,
197 length,
198 token_type: *token_type,
199 token_modifiers_bitset: *modifiers,
200 });
201 prev_line = pos.line;
202 prev_start = pos.character;
203 }
204 result
205}
206
207struct ResolutionOverlay {
208 decl_at: HashMap<usize, usize>,
210 ref_at: HashMap<usize, usize>,
212 mutation_at: HashSet<usize>,
214 prose: Vec<Span>,
216 type_names: HashSet<String>,
219}
220
221impl ResolutionOverlay {
222 fn build(doc: &DocumentState) -> Self {
223 let index = &doc.symbol_index;
224
225 let mut decl_at = HashMap::new();
226 for (i, def) in index.definitions.iter().enumerate() {
227 if def.span != Span::default() {
228 decl_at.insert(def.span.start, i);
229 }
230 }
231
232 let mut ref_at = HashMap::new();
233 for reference in &index.references {
234 if let Some(def_idx) = reference.definition_idx {
235 ref_at.insert(reference.span.start, def_idx);
236 }
237 }
238
239 let prose = index
240 .block_spans
241 .iter()
242 .filter(|(_, block_type, _)| {
243 matches!(block_type, BlockType::Note | BlockType::Example)
244 })
245 .map(|(_, _, span)| *span)
246 .collect();
247
248 let type_names = doc
249 .type_registry
250 .iter_types()
251 .map(|(sym, _)| doc.interner.resolve(*sym).to_string())
252 .collect();
253
254 ResolutionOverlay {
255 decl_at,
256 ref_at,
257 mutation_at: find_mutation_targets(&doc.tokens),
258 prose,
259 type_names,
260 }
261 }
262
263 fn classify(&self, token: &Token, doc: &DocumentState) -> (Option<u32>, u32) {
264 if matches!(token.kind, TokenType::BlockHeader { .. }) {
266 return classify_token(&token.kind);
267 }
268
269 let start = token.span.start;
271 if self.prose.iter().any(|p| start > p.start && start < p.end) {
272 return (Some(TYPE_COMMENT), 0);
273 }
274
275 let (mut token_type, mut modifiers) = classify_token(&token.kind);
276
277 if let Some(&def_idx) = self.decl_at.get(&start) {
278 let def = &doc.symbol_index.definitions[def_idx];
279 token_type = Some(semantic_type_for(&def.kind));
280 modifiers = MOD_DECLARATION | readonly_bit(def);
281 } else if let Some(&def_idx) = self.ref_at.get(&start) {
282 let def = &doc.symbol_index.definitions[def_idx];
283 token_type = Some(semantic_type_for(&def.kind));
284 modifiers = readonly_bit(def);
285 } else if is_identifier_like(&token.kind)
286 && self.type_names.contains(doc.interner.resolve(token.lexeme))
287 {
288 token_type = Some(TYPE_TYPE);
291 modifiers &= !MOD_DECLARATION;
292 } else if matches!(
293 doc.interner.resolve(token.lexeme),
294 "mutable" | "mut"
295 ) {
296 token_type = Some(TYPE_MODIFIER);
299 modifiers = 0;
300 } else if matches!(token.kind, TokenType::ProperName(_)) {
301 modifiers &= !MOD_DECLARATION;
304 }
305
306 if self.mutation_at.contains(&start) {
307 modifiers |= MOD_MODIFICATION;
308 }
309 if is_identifier_like(&token.kind)
310 && prelude_names().contains(doc.interner.resolve(token.lexeme))
311 {
312 modifiers |= MOD_DEFAULT_LIBRARY;
313 }
314
315 (token_type, modifiers)
316 }
317}
318
319fn readonly_bit(def: &crate::index::Definition) -> u32 {
320 match def.mutable {
321 Some(false) => MOD_READONLY,
322 _ => 0,
323 }
324}
325
326pub fn semantic_type_for(kind: &DefinitionKind) -> u32 {
329 match kind {
330 DefinitionKind::Variable => TYPE_VARIABLE,
331 DefinitionKind::Function => TYPE_FUNCTION,
332 DefinitionKind::Struct => TYPE_TYPE,
333 DefinitionKind::Enum => TYPE_TYPE,
334 DefinitionKind::Field => TYPE_PROPERTY,
335 DefinitionKind::Parameter => TYPE_PARAMETER,
336 DefinitionKind::Block => TYPE_NAMESPACE,
337 DefinitionKind::Variant => TYPE_ENUM_MEMBER,
338 DefinitionKind::Theorem => TYPE_NAMESPACE,
339 }
340}
341
342fn is_identifier_like(kind: &TokenType) -> bool {
343 matches!(
344 kind,
345 TokenType::Identifier
346 | TokenType::ProperName(_)
347 | TokenType::Noun(_)
348 | TokenType::Verb { .. }
349 | TokenType::Adjective(_)
350 )
351}
352
353fn prelude_names() -> &'static HashSet<String> {
355 static NAMES: OnceLock<HashSet<String>> = OnceLock::new();
356 NAMES.get_or_init(|| {
357 logicaffeine_compile::loader::prelude_vocabulary()
358 .into_iter()
359 .collect()
360 })
361}
362
363pub fn find_mutation_targets(tokens: &[Token]) -> HashSet<usize> {
369 let mut targets = HashSet::new();
370 let mut i = 0;
371 while i < tokens.len() {
372 match &tokens[i].kind {
373 TokenType::Set => {
374 let mut last_ident: Option<usize> = None;
375 let mut before_at: Option<usize> = None;
376 let mut j = i + 1;
377 while j < tokens.len() {
378 match &tokens[j].kind {
379 TokenType::To => break,
380 TokenType::At => before_at = last_ident,
381 TokenType::Period => break,
382 kind if is_identifier_like(kind) => {
383 last_ident = Some(tokens[j].span.start)
384 }
385 _ => {}
386 }
387 j += 1;
388 }
389 if let Some(start) = before_at.or(last_ident) {
390 targets.insert(start);
391 }
392 i = j;
393 }
394 TokenType::Increase | TokenType::Decrease => {
395 if let Some(next) = tokens[i + 1..]
396 .iter()
397 .find(|t| is_identifier_like(&t.kind))
398 {
399 targets.insert(next.span.start);
400 }
401 i += 1;
402 }
403 TokenType::Push | TokenType::Add | TokenType::Append => {
404 if let Some(start) = ident_after_keyword(tokens, i, &TokenType::To) {
406 targets.insert(start);
407 }
408 i += 1;
409 }
410 TokenType::Pop | TokenType::Remove => {
411 if let Some(start) = ident_after_keyword(tokens, i, &TokenType::From) {
412 targets.insert(start);
413 }
414 i += 1;
415 }
416 _ => i += 1,
417 }
418 }
419 targets
420}
421
422fn ident_after_keyword(tokens: &[Token], from: usize, keyword: &TokenType) -> Option<usize> {
424 let mut seen_keyword = false;
425 for token in &tokens[from + 1..] {
426 match &token.kind {
427 TokenType::Period => return None,
428 kind if kind == keyword => seen_keyword = true,
429 kind if seen_keyword && is_identifier_like(kind) => {
430 return Some(token.span.start)
431 }
432 _ => {}
433 }
434 }
435 None
436}
437
438pub fn encode_tokens(tokens: &[Token], line_index: &LineIndex) -> Vec<SemanticToken> {
442 encode_with(tokens, line_index, |token| classify_token(&token.kind))
443}
444
445pub fn encode_document_tokens_in_range(
449 doc: &DocumentState,
450 start_offset: usize,
451 end_offset: usize,
452) -> Vec<SemanticToken> {
453 let overlay = ResolutionOverlay::build(doc);
454 let spans = paint_spans(doc, &overlay, Some((start_offset, end_offset)));
455 encode_spans(&spans, &doc.line_index)
456}
457
458pub fn semantic_token_edits(
465 prev: &[SemanticToken],
466 next: &[SemanticToken],
467) -> Vec<tower_lsp::lsp_types::SemanticTokensEdit> {
468 let prefix = prev
469 .iter()
470 .zip(next.iter())
471 .take_while(|(a, b)| a == b)
472 .count();
473 if prefix == prev.len() && prefix == next.len() {
474 return Vec::new();
475 }
476 let max_suffix = prev.len().min(next.len()) - prefix;
477 let suffix = prev
478 .iter()
479 .rev()
480 .zip(next.iter().rev())
481 .take_while(|(a, b)| a == b)
482 .count()
483 .min(max_suffix);
484
485 vec![tower_lsp::lsp_types::SemanticTokensEdit {
486 start: (prefix * 5) as u32,
487 delete_count: ((prev.len() - prefix - suffix) * 5) as u32,
488 data: Some(next[prefix..next.len() - suffix].to_vec()),
489 }]
490}
491
492fn encode_with(
493 tokens: &[Token],
494 line_index: &LineIndex,
495 classify: impl Fn(&Token) -> (Option<u32>, u32),
496) -> Vec<SemanticToken> {
497 let mut result = Vec::with_capacity(tokens.len());
498 let mut prev_line = 0u32;
499 let mut prev_start = 0u32;
500
501 for token in tokens {
502 let (token_type, modifiers) = classify(token);
503 let token_type = match token_type {
504 Some(t) => t,
505 None => continue, };
507
508 let pos = line_index.position(token.span.start);
509 let length = line_index.utf16_length(token.span.start, token.span.end);
510
511 if length == 0 {
512 continue;
513 }
514
515 let delta_line = pos.line - prev_line;
516 let delta_start = if delta_line == 0 {
517 pos.character - prev_start
518 } else {
519 pos.character
520 };
521
522 result.push(SemanticToken {
523 delta_line,
524 delta_start,
525 length,
526 token_type,
527 token_modifiers_bitset: modifiers,
528 });
529
530 prev_line = pos.line;
531 prev_start = pos.character;
532 }
533
534 result
535}
536
537pub fn classify_token(kind: &TokenType) -> (Option<u32>, u32) {
545 use logicaffeine_language::token_class::{classify, TokenClass};
546 let class = classify(kind);
547 let index = class.map(|c| match c {
548 TokenClass::Keyword => TYPE_KEYWORD,
549 TokenClass::Type => TYPE_TYPE,
550 TokenClass::Function => TYPE_FUNCTION,
551 TokenClass::Variable => TYPE_VARIABLE,
552 TokenClass::String => TYPE_STRING,
553 TokenClass::Number => TYPE_NUMBER,
554 TokenClass::Operator => TYPE_OPERATOR,
555 TokenClass::Namespace => TYPE_NAMESPACE,
556 TokenClass::Modifier => TYPE_MODIFIER,
557 TokenClass::Property => TYPE_PROPERTY,
558 TokenClass::Comment => TYPE_COMMENT,
559 TokenClass::Parameter => TYPE_PARAMETER,
560 TokenClass::EnumMember => TYPE_ENUM_MEMBER,
561 });
562 let modifiers = if matches!(kind, TokenType::ProperName(_)) {
563 MOD_DECLARATION
564 } else {
565 0
566 };
567 (index, modifiers)
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use logicaffeine_base::Interner;
574 use logicaffeine_language::token::Span;
575
576 #[test]
577 fn all_token_types_classified() {
578 let mut interner = Interner::new();
581 let sym = interner.intern("test");
582
583 let test_tokens = vec![
584 TokenType::Let,
585 TokenType::Noun(sym),
586 TokenType::Verb {
587 lemma: sym,
588 time: logicaffeine_language::lexicon::Time::Present,
589 aspect: logicaffeine_language::lexicon::Aspect::Simple,
590 class: logicaffeine_language::lexicon::VerbClass::Activity,
591 },
592 TokenType::StringLiteral(sym),
593 TokenType::Number(sym),
594 TokenType::BlockHeader { block_type: logicaffeine_language::token::BlockType::Main },
595 ];
596
597 for tt in &test_tokens {
598 let (ty, _) = classify_token(tt);
599 assert!(ty.is_some(), "Token {:?} should be classified", tt);
600 }
601 }
602
603 #[test]
604 fn keywords_classified_as_keyword() {
605 let keywords = [
606 TokenType::Let, TokenType::Set, TokenType::Return,
607 TokenType::While, TokenType::Repeat, TokenType::Push,
608 ];
609 for kw in &keywords {
610 let (ty, _) = classify_token(kw);
611 assert_eq!(ty, Some(0), "Keyword {:?} should map to type 0", kw);
612 }
613 }
614
615 #[test]
616 fn operators_classified_correctly() {
617 let ops = [
618 TokenType::Plus, TokenType::Minus, TokenType::Star,
619 TokenType::Lt, TokenType::Gt, TokenType::EqEq,
620 ];
621 for op in &ops {
622 let (ty, _) = classify_token(op);
623 assert_eq!(ty, Some(6), "Operator {:?} should map to type 6", op);
624 }
625 }
626
627 #[test]
628 fn string_literal_classified() {
629 let mut interner = Interner::new();
630 let sym = interner.intern("hello");
631 let (ty, _) = classify_token(&TokenType::StringLiteral(sym));
632 assert_eq!(ty, Some(4), "String literal should map to type 4");
633 }
634
635 #[test]
636 fn number_literal_classified() {
637 let mut interner = Interner::new();
638 let sym = interner.intern("42");
639 let (ty, _) = classify_token(&TokenType::Number(sym));
640 assert_eq!(ty, Some(5), "Number should map to type 5");
641 }
642
643 #[test]
644 fn structural_tokens_skipped() {
645 let skipped = [
646 TokenType::Indent, TokenType::Dedent,
647 TokenType::Newline, TokenType::EOF,
648 ];
649 for tt in &skipped {
650 let (ty, _) = classify_token(tt);
651 assert_eq!(ty, None, "Structural token {:?} should be skipped", tt);
652 }
653 }
654
655 #[test]
656 fn zero_length_tokens_skipped_in_encoding() {
657 let line_index = LineIndex::new("Let x be 5.");
658 let mut interner = Interner::new();
659 let sym = interner.intern("");
660 let tokens = vec![
661 Token::new(TokenType::Indent, sym, Span::new(0, 0)),
662 ];
663 let encoded = encode_tokens(&tokens, &line_index);
664 assert!(encoded.is_empty(), "Zero-length tokens should be skipped");
665 }
666
667 #[test]
668 fn multi_line_delta_encoding() {
669 let line_index = LineIndex::new("Let x\nbe 5.");
670 let mut interner = Interner::new();
671 let let_sym = interner.intern("Let");
672 let be_sym = interner.intern("be");
673
674 let tokens = vec![
675 Token::new(TokenType::Let, let_sym, Span::new(0, 3)),
676 Token::new(TokenType::Be, be_sym, Span::new(6, 8)),
677 ];
678
679 let encoded = encode_tokens(&tokens, &line_index);
680 assert_eq!(encoded.len(), 2);
681 assert_eq!(encoded[0].delta_line, 0);
682 assert_eq!(encoded[1].delta_line, 1, "Second token should be on next line");
683 assert_eq!(encoded[1].delta_start, 0, "After line change, delta_start resets");
684 }
685
686 #[test]
687 fn block_header_classified_as_namespace() {
688 let (ty, _) = classify_token(&TokenType::BlockHeader {
689 block_type: logicaffeine_language::token::BlockType::Main,
690 });
691 assert_eq!(ty, Some(7), "Block header should map to type 7 (namespace)");
692 }
693
694 #[test]
695 fn noun_classified_as_type() {
696 let mut interner = Interner::new();
697 let sym = interner.intern("person");
698 let (ty, _) = classify_token(&TokenType::Noun(sym));
699 assert_eq!(ty, Some(1), "Noun should map to type 1 (TYPE)");
700 }
701
702 #[test]
703 fn proper_name_has_declaration_modifier() {
704 let mut interner = Interner::new();
705 let sym = interner.intern("Alice");
706 let (ty, mods) = classify_token(&TokenType::ProperName(sym));
707 assert_eq!(ty, Some(3), "ProperName should map to type 3 (VARIABLE)");
708 assert_eq!(mods, 1, "ProperName should have DECLARATION modifier (bit 0)");
709 }
710
711 #[test]
712 fn ambiguous_uses_primary() {
713 let mut interner = Interner::new();
714 let sym = interner.intern("test");
715 let primary = Box::new(TokenType::Noun(sym));
716 let alternatives = vec![TokenType::Identifier];
717 let (ty, _) = classify_token(&TokenType::Ambiguous { primary, alternatives });
718 assert_eq!(ty, Some(1), "Ambiguous wrapping Noun should classify as type 1");
719 }
720
721 #[test]
722 fn encode_tokens_utf16_length() {
723 let source = "café";
725 let line_index = LineIndex::new(source);
726 let mut interner = Interner::new();
727 let sym = interner.intern("café");
728 let tokens = vec![
729 Token::new(TokenType::Identifier, sym, Span::new(0, 5)), ];
731 let encoded = encode_tokens(&tokens, &line_index);
732 assert_eq!(encoded.len(), 1);
733 assert_eq!(encoded[0].length, 4, "UTF-16 length of 'café' should be 4, not 5 bytes");
734 }
735
736 #[test]
737 fn classify_ambiguous_nested_has_depth_guard() {
738 let mut interner = Interner::new();
739 let sym = interner.intern("test");
740 let inner = TokenType::Noun(sym);
742 let mid = TokenType::Ambiguous {
743 primary: Box::new(inner),
744 alternatives: vec![TokenType::Identifier],
745 };
746 let outer = TokenType::Ambiguous {
747 primary: Box::new(mid),
748 alternatives: vec![TokenType::Identifier],
749 };
750 let (ty, _) = classify_token(&outer);
751 assert_eq!(ty, Some(1), "Nested Ambiguous wrapping Noun should still classify as type 1 (TYPE)");
752 }
753
754 #[test]
755 fn classify_ambiguous_too_deep_returns_none() {
756 let mut interner = Interner::new();
757 let sym = interner.intern("test");
758 let mut current = TokenType::Noun(sym);
760 for _ in 0..10 {
761 current = TokenType::Ambiguous {
762 primary: Box::new(current),
763 alternatives: vec![TokenType::Identifier],
764 };
765 }
766 let (ty, _) = classify_token(¤t);
768 assert_eq!(ty, None, "Excessively nested Ambiguous should return None");
769 }
770
771 #[test]
772 fn delta_encoding() {
773 let line_index = LineIndex::new("Let x be 5.");
774 let mut interner = Interner::new();
775 let let_sym = interner.intern("Let");
776 let x_sym = interner.intern("x");
777
778 let tokens = vec![
779 Token::new(TokenType::Let, let_sym, Span::new(0, 3)),
780 Token::new(TokenType::Identifier, x_sym, Span::new(4, 5)),
781 ];
782
783 let encoded = encode_tokens(&tokens, &line_index);
784 assert_eq!(encoded.len(), 2);
785 assert_eq!(encoded[0].delta_line, 0);
786 assert_eq!(encoded[0].delta_start, 0);
787 assert_eq!(encoded[0].length, 3);
788 assert_eq!(encoded[1].delta_line, 0);
789 assert_eq!(encoded[1].delta_start, 4);
790 assert_eq!(encoded[1].length, 1);
791 }
792}