1use crate::dsl::lex::{lex, Token, TokenKind};
30use crate::meta::{function_ops, OpInfo};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Severity {
36 Error,
37 Warning,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Diagnostic {
44 pub line: usize,
45 pub col: usize,
46 pub end_line: usize,
47 pub end_col: usize,
48 pub severity: Severity,
49 pub message: String,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum CompletionKind {
55 Function,
57 Field,
59 Variable,
61 Series,
63 Keyword,
65}
66
67impl CompletionKind {
68 pub fn as_str(self) -> &'static str {
70 match self {
71 CompletionKind::Function => "function",
72 CompletionKind::Field => "field",
73 CompletionKind::Variable => "variable",
74 CompletionKind::Series => "series",
75 CompletionKind::Keyword => "keyword",
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct CompletionItem {
84 pub label: String,
85 pub kind: CompletionKind,
86 pub detail: String,
88 pub documentation: String,
90 pub insert_text: String,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct HoverInfo {
97 pub line: usize,
98 pub col: usize,
99 pub end_line: usize,
100 pub end_col: usize,
101 pub markdown: String,
102}
103
104pub const PRICE_SERIES: &[&str] = &["open", "high", "low", "close", "volume"];
107
108pub const COMMON_FUNDAMENTALS: &[&str] = &[
112 "pe",
113 "pb",
114 "ps",
115 "roe",
116 "roa",
117 "eps",
118 "market_cap",
119 "revenue_growth",
120 "dividend_yield",
121];
122
123const KEYWORDS: &[&str] = &["let", "and", "or", "true", "false"];
125
126pub fn diagnostics(src: &str, known_series: Option<&[String]>) -> Vec<Diagnostic> {
139 let toks = match lex(src) {
141 Ok(t) => t,
142 Err(e) => {
143 return vec![Diagnostic {
144 line: e.line,
145 col: e.col,
146 end_line: e.line,
147 end_col: e.col + 1,
148 severity: Severity::Error,
149 message: e.message,
150 }]
151 }
152 };
153
154 match crate::lint(src, known_series) {
157 Err(e) => {
158 let (end_line, end_col) = token_at(&toks, e.line, e.col)
161 .map(token_end)
162 .unwrap_or((e.line, e.col + 1));
163 vec![Diagnostic {
164 line: e.line,
165 col: e.col,
166 end_line,
167 end_col,
168 severity: Severity::Error,
169 message: e.message,
170 }]
171 }
172 Ok(lints) => lints
173 .into_iter()
174 .map(|l| {
175 let (end_line, end_col) = token_at(&toks, l.line, l.col)
176 .map(token_end)
177 .unwrap_or((l.line, l.col + 1));
178 Diagnostic {
179 line: l.line,
180 col: l.col,
181 end_line,
182 end_col,
183 severity: Severity::Warning,
184 message: l.message,
185 }
186 })
187 .collect(),
188 }
189}
190
191pub fn hover(src: &str, line: usize, col: usize) -> Option<HoverInfo> {
198 let toks = lex(src).ok()?;
199 let t = token_at(&toks, line, col)?;
200 let (end_line, end_col) = token_end(t);
201
202 let markdown = match &t.kind {
203 TokenKind::Ident(name) => {
204 if let Some(op) = lookup_op(name) {
205 op_markdown(&op)
206 } else if let Some(kw) = keyword_markdown(name) {
207 kw
208 } else if PRICE_SERIES.contains(&name.as_str()) {
209 format!("**`{name}`** — input data series (price).")
210 } else {
211 format!("**`{name}`** — data series reference.")
213 }
214 }
215 TokenKind::Op(sym) => binop_markdown(sym)?,
216 TokenKind::Let => keyword_markdown("let")?,
217 _ => return None,
218 };
219
220 Some(HoverInfo {
221 line: t.line,
222 col: t.col,
223 end_line,
224 end_col,
225 markdown,
226 })
227}
228
229fn op_markdown(op: &OpInfo) -> String {
231 let mut s = format!("```lemon\n{}\n```\n\n{}", signature(op), op.description);
232 if !op.aliases.is_empty() {
233 let aliases = op
234 .aliases
235 .iter()
236 .map(|a| format!("`{a}`"))
237 .collect::<Vec<_>>()
238 .join(", ");
239 s.push_str(&format!("\n\n*Aliases: {aliases}*"));
240 }
241 s
242}
243
244fn signature(op: &OpInfo) -> String {
247 let params: Vec<String> = op
248 .fields
249 .iter()
250 .map(|f| {
251 let opt = if f.required { "" } else { "?" };
252 match &f.default {
253 Some(d) => format!("{}{opt}={d}", f.name),
254 None => format!("{}{opt}", f.name),
255 }
256 })
257 .collect();
258 format!("{}({})", op.name, params.join(", "))
259}
260
261fn keyword_markdown(word: &str) -> Option<String> {
262 let body = match word {
263 "let" => "**`let`** — bind a name to a sub-expression: `let ma = sma(close, 20)`. Bindings are inlined at parse time.",
264 "and" => "**`and`** — logical AND. Yields `1.0` where both operands are truthy, else `0.0`.",
265 "or" => "**`or`** — logical OR. Yields `1.0` where either operand is truthy, else `0.0`.",
266 "true" => "**`true`** — boolean literal (keyword-argument values only).",
267 "false" => "**`false`** — boolean literal (keyword-argument values only).",
268 _ => return None,
269 };
270 Some(body.to_string())
271}
272
273fn binop_markdown(sym: &str) -> Option<String> {
274 let desc = match sym {
275 ">" => "greater-than",
276 "<" => "less-than",
277 ">=" => "greater-than-or-equal",
278 "<=" => "less-than-or-equal",
279 "+" => "addition",
280 "-" => "subtraction (or unary negation)",
281 "*" => "multiplication",
282 "/" => "division",
283 _ => return None,
284 };
285 Some(format!(
286 "**`{sym}`** — {desc}. Comparisons output `1.0`/`0.0`."
287 ))
288}
289
290pub fn completions(src: &str, line: usize, col: usize) -> Vec<CompletionItem> {
300 let toks = match lex(src) {
301 Ok(t) => t,
302 Err(_) => return filter_items(static_items(&[]), ""),
305 };
306
307 let prefix = prefix_at(&toks, line, col);
308 let before = tokens_before(&toks, line, col);
309 let enclosing = enclosing_call(&before);
310 let bound = let_bound_names(&before);
311
312 let mut items = Vec::new();
313
314 if let Some(op) = enclosing.as_deref().and_then(lookup_op) {
316 for f in &op.fields {
317 items.push(CompletionItem {
318 label: f.name.to_string(),
319 kind: CompletionKind::Field,
320 detail: format!("{} argument ({})", op.name, f.kind),
321 documentation: String::new(),
322 insert_text: format!("{}=", f.name),
323 });
324 }
325 }
326
327 for name in bound {
329 items.push(CompletionItem {
330 label: name.clone(),
331 kind: CompletionKind::Variable,
332 detail: "let-bound".to_string(),
333 documentation: String::new(),
334 insert_text: name,
335 });
336 }
337
338 items.extend(static_items(&[]));
339 filter_items(items, &prefix)
340}
341
342fn static_items(_extra: &[&str]) -> Vec<CompletionItem> {
346 let mut items = Vec::new();
347
348 for op in function_ops() {
349 items.push(CompletionItem {
350 label: op.name.to_string(),
351 kind: CompletionKind::Function,
352 detail: signature(&op),
353 documentation: op.description.to_string(),
354 insert_text: op.name.to_string(),
355 });
356 }
357
358 for &s in PRICE_SERIES {
359 items.push(series_item(s, "price series"));
360 }
361 for &s in COMMON_FUNDAMENTALS {
362 items.push(series_item(s, "fundamental series"));
363 }
364 for &kw in KEYWORDS {
365 items.push(CompletionItem {
366 label: kw.to_string(),
367 kind: CompletionKind::Keyword,
368 detail: "keyword".to_string(),
369 documentation: String::new(),
370 insert_text: kw.to_string(),
371 });
372 }
373 items
374}
375
376fn series_item(name: &str, detail: &str) -> CompletionItem {
377 CompletionItem {
378 label: name.to_string(),
379 kind: CompletionKind::Series,
380 detail: detail.to_string(),
381 documentation: String::new(),
382 insert_text: name.to_string(),
383 }
384}
385
386fn filter_items(items: Vec<CompletionItem>, prefix: &str) -> Vec<CompletionItem> {
390 let pfx = prefix.to_ascii_lowercase();
391 let mut seen = std::collections::HashSet::new();
392 items
393 .into_iter()
394 .filter(|it| it.label.to_ascii_lowercase().starts_with(&pfx))
395 .filter(|it| seen.insert((it.label.clone(), it.kind)))
396 .collect()
397}
398
399fn lookup_op(name: &str) -> Option<OpInfo> {
405 function_ops()
406 .into_iter()
407 .find(|o| o.name == name || o.aliases.contains(&name))
408}
409
410fn let_bound_names(toks: &[Token]) -> Vec<String> {
414 let mut out = Vec::new();
415 for (i, t) in toks.iter().enumerate() {
416 if t.kind == TokenKind::Let {
417 if let Some(Token {
418 kind: TokenKind::Ident(name),
419 ..
420 }) = toks.get(i + 1)
421 {
422 if !out.contains(name) {
423 out.push(name.clone());
424 }
425 }
426 }
427 }
428 out
429}
430
431fn enclosing_call(toks: &[Token]) -> Option<String> {
436 let mut stack: Vec<Option<String>> = Vec::new();
437 for (i, t) in toks.iter().enumerate() {
438 match &t.kind {
439 TokenKind::LParen => {
440 let name = match i.checked_sub(1).and_then(|p| toks.get(p)) {
441 Some(Token {
442 kind: TokenKind::Ident(n),
443 ..
444 }) => Some(n.clone()),
445 _ => None,
446 };
447 stack.push(name);
448 }
449 TokenKind::LBracket => stack.push(None),
450 TokenKind::RParen | TokenKind::RBracket => {
451 stack.pop();
452 }
453 _ => {}
454 }
455 }
456 stack.into_iter().rev().flatten().next()
457}
458
459fn prefix_at(toks: &[Token], line: usize, col: usize) -> String {
461 for t in toks {
462 if let TokenKind::Ident(name) = &t.kind {
463 let len = name.chars().count();
464 if t.line == line && t.col <= col && col <= t.col + len {
465 let take = col - t.col;
466 return name.chars().take(take).collect();
467 }
468 }
469 }
470 String::new()
471}
472
473fn tokens_before(toks: &[Token], line: usize, col: usize) -> Vec<Token> {
476 toks.iter()
477 .filter(|t| t.kind != TokenKind::Eof)
478 .filter(|t| t.line < line || (t.line == line && t.col < col))
479 .cloned()
480 .collect()
481}
482
483fn token_at(toks: &[Token], line: usize, col: usize) -> Option<&Token> {
486 toks.iter().find(|t| {
487 if t.kind == TokenKind::Eof || t.line != line {
488 return false;
489 }
490 let (_, end_col) = token_end(t);
491 t.col <= col && col < end_col
492 })
493}
494
495fn token_end(t: &Token) -> (usize, usize) {
498 let len = match &t.kind {
499 TokenKind::Ident(s) | TokenKind::Op(s) => s.chars().count(),
500 TokenKind::Str(s) => s.chars().count() + 2, TokenKind::Num(_) => 1, TokenKind::Let => 3,
503 TokenKind::LParen
504 | TokenKind::RParen
505 | TokenKind::LBracket
506 | TokenKind::RBracket
507 | TokenKind::Comma
508 | TokenKind::Eq => 1,
509 TokenKind::Eof => 0,
510 };
511 (t.line, t.col + len)
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523pub enum TokenType {
524 Comment,
525 Number,
526 Str,
527 Keyword,
528 Function,
529 Parameter,
530 Series,
531 Operator,
532 Punctuation,
533}
534
535impl TokenType {
536 pub fn as_str(self) -> &'static str {
538 match self {
539 TokenType::Comment => "comment",
540 TokenType::Number => "number",
541 TokenType::Str => "string",
542 TokenType::Keyword => "keyword",
543 TokenType::Function => "function",
544 TokenType::Parameter => "parameter",
545 TokenType::Series => "series",
546 TokenType::Operator => "operator",
547 TokenType::Punctuation => "punctuation",
548 }
549 }
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
555pub struct SemanticToken {
556 pub line: usize,
557 pub col: usize,
558 pub end_line: usize,
559 pub end_col: usize,
560 pub token_type: TokenType,
561}
562
563pub fn tokens(src: &str) -> Vec<SemanticToken> {
575 let lines: Vec<Vec<char>> = src.split('\n').map(|l| l.chars().collect()).collect();
576 let mut out = comment_spans(&lines);
577
578 if let Ok(toks) = lex(src) {
579 for (i, t) in toks.iter().enumerate() {
580 let token_type = match &t.kind {
581 TokenKind::Eof => continue,
582 TokenKind::Num(_) => TokenType::Number,
583 TokenKind::Str(_) => TokenType::Str,
584 TokenKind::Let => TokenType::Keyword,
585 TokenKind::Op(_) | TokenKind::Eq => TokenType::Operator,
586 TokenKind::LParen
587 | TokenKind::RParen
588 | TokenKind::LBracket
589 | TokenKind::RBracket
590 | TokenKind::Comma => TokenType::Punctuation,
591 TokenKind::Ident(s) => classify_ident(s, toks.get(i + 1)),
592 };
593 let (end_line, end_col) = match &t.kind {
596 TokenKind::Num(_) => (t.line, t.col + number_len(&lines, t.line, t.col)),
597 _ => token_end(t),
598 };
599 out.push(SemanticToken {
600 line: t.line,
601 col: t.col,
602 end_line,
603 end_col,
604 token_type,
605 });
606 }
607 }
608
609 out.sort_by_key(|t| (t.line, t.col));
610 out
611}
612
613fn classify_ident(s: &str, next: Option<&Token>) -> TokenType {
616 if matches!(s, "and" | "or" | "not" | "true" | "false") {
617 return TokenType::Keyword;
618 }
619 match next.map(|t| &t.kind) {
620 Some(TokenKind::LParen) => TokenType::Function,
621 Some(TokenKind::Eq) => TokenType::Parameter,
622 _ => TokenType::Series,
623 }
624}
625
626fn number_len(lines: &[Vec<char>], line: usize, col: usize) -> usize {
629 let Some(row) = lines.get(line - 1) else {
630 return 1;
631 };
632 let start = col - 1;
633 let mut j = start;
634 while j < row.len() {
635 let d = row[j];
636 if d.is_ascii_digit() || d == '_' || d == '.' {
637 j += 1;
638 } else if (d == 'e' || d == 'E')
639 && j + 1 < row.len()
640 && (row[j + 1].is_ascii_digit()
641 || ((row[j + 1] == '+' || row[j + 1] == '-')
642 && j + 2 < row.len()
643 && row[j + 2].is_ascii_digit()))
644 {
645 j += 2; } else {
647 break;
648 }
649 }
650 (j - start).max(1)
651}
652
653fn comment_spans(lines: &[Vec<char>]) -> Vec<SemanticToken> {
658 let mut out = Vec::new();
659 let mut in_string = false;
660 for (li, row) in lines.iter().enumerate() {
661 let mut c = 0;
662 while c < row.len() {
663 match row[c] {
664 '"' => in_string = !in_string,
665 '#' if !in_string => {
666 out.push(SemanticToken {
667 line: li + 1,
668 col: c + 1,
669 end_line: li + 1,
670 end_col: row.len() + 1,
671 token_type: TokenType::Comment,
672 });
673 break; }
675 _ => {}
676 }
677 c += 1;
678 }
679 }
680 out
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 fn series(names: &[&str]) -> Vec<String> {
690 names.iter().map(|s| s.to_string()).collect()
691 }
692
693 #[test]
694 fn clean_source_has_no_diagnostics() {
695 assert!(diagnostics("close > sma(close, 2)", None).is_empty());
696 assert!(diagnostics("close > sma(close, 2)", Some(&series(&["close"]))).is_empty());
698 }
699
700 #[test]
701 fn parse_error_becomes_a_ranged_error_diagnostic() {
702 let diags = diagnostics("sma(close, 2", None);
703 assert_eq!(diags.len(), 1);
704 assert_eq!(diags[0].severity, Severity::Error);
705 assert!(diags[0].end_col > diags[0].col);
706 }
707
708 #[test]
709 fn lex_error_is_reported_without_panicking() {
710 let diags = diagnostics("close $ 1", None);
711 assert_eq!(diags.len(), 1);
712 assert_eq!(diags[0].severity, Severity::Error);
713 assert_eq!((diags[0].line, diags[0].col), (1, 7));
714 }
715
716 #[test]
717 fn parse_error_on_unknown_op_spans_the_word() {
718 let diags = diagnostics("frobnicate(close)", None);
719 assert_eq!(diags.len(), 1);
720 assert_eq!(diags[0].col, 1);
722 assert_eq!(diags[0].end_col, 1 + "frobnicate".len());
723 }
724
725 #[test]
726 fn unknown_series_warning_is_ranged_when_a_series_list_is_given() {
727 let diags = diagnostics("clsoe > 1", Some(&series(&["close", "pe"])));
728 assert_eq!(diags.len(), 1);
729 assert_eq!(diags[0].severity, Severity::Warning);
730 assert!(diags[0].message.contains("close"), "{}", diags[0].message);
731 assert_eq!((diags[0].col, diags[0].end_col), (1, 1 + "clsoe".len()));
733 }
734
735 #[test]
736 fn no_unknown_series_check_without_a_list() {
737 assert!(diagnostics("clsoe > 1", None).is_empty());
739 }
740
741 #[test]
742 fn unused_let_binding_warns_even_without_a_series_list() {
743 let diags = diagnostics("let ma = sma(close, 20)\nclose > 1", None);
744 assert_eq!(diags.len(), 1);
745 assert_eq!(diags[0].severity, Severity::Warning);
746 assert!(diags[0].message.contains("unused let binding `ma`"));
747 assert_eq!(diags[0].end_col, diags[0].col + "ma".len());
749 }
750
751 #[test]
754 fn hover_on_op_shows_signature_and_description() {
755 let h = hover("close > sma(close, 2)", 1, 9).expect("hover on sma");
756 assert!(h.markdown.contains("sma(of, n)"), "{}", h.markdown);
757 assert!(h.markdown.contains("moving average"));
758 assert!(h.markdown.contains("Aliases"), "{}", h.markdown);
759 assert_eq!((h.line, h.col), (1, 9));
761 assert_eq!(h.end_col, 12);
762 }
763
764 #[test]
765 fn hover_on_operator_and_series_and_keyword() {
766 assert!(hover("close > 1", 1, 7)
767 .unwrap()
768 .markdown
769 .contains("greater"));
770 assert!(hover("close > 1", 1, 1).unwrap().markdown.contains("price"));
771 assert!(hover("let a = close\na > 1", 1, 1)
772 .unwrap()
773 .markdown
774 .contains("bind"));
775 }
776
777 #[test]
778 fn hover_on_unknown_series_and_nothing_are_distinguished() {
779 assert!(hover("roic > 1", 1, 1)
780 .unwrap()
781 .markdown
782 .contains("data series"));
783 assert!(hover("close > 1", 1, 6).is_none()); assert!(hover("close > 1", 1, 9).is_none()); assert!(hover("", 1, 1).is_none());
787 }
788
789 #[test]
790 fn hover_ignores_lex_errors() {
791 assert!(hover("close $", 1, 1).is_none());
792 }
793
794 #[test]
795 fn hover_on_op_without_aliases_omits_alias_line() {
796 let h = hover("ema(close, 5)", 1, 1).unwrap();
798 assert!(h.markdown.contains("ema(of, n)"));
799 assert!(!h.markdown.contains("Aliases"), "{}", h.markdown);
800 }
801
802 #[test]
803 fn hover_on_boolean_literal_and_logical_words() {
804 assert!(hover("rank(close, ascending=true)", 1, 23)
805 .unwrap()
806 .markdown
807 .contains("boolean"));
808 assert!(hover("a and b", 1, 3).unwrap().markdown.contains("AND"));
809 assert!(hover("a or b", 1, 3).unwrap().markdown.contains("OR"));
810 }
811
812 #[test]
813 fn every_operator_symbol_has_hover_text() {
814 for sym in [">", "<", ">=", "<=", "+", "-", "*", "/"] {
815 assert!(binop_markdown(sym).is_some(), "no hover for `{sym}`");
816 }
817 assert!(binop_markdown("??").is_none());
818 }
819
820 #[test]
821 fn every_keyword_has_hover_text() {
822 for kw in ["let", "and", "or", "true", "false"] {
823 assert!(keyword_markdown(kw).is_some(), "no hover for `{kw}`");
824 }
825 assert!(keyword_markdown("close").is_none());
826 }
827
828 fn labels(items: &[CompletionItem]) -> Vec<&str> {
831 items.iter().map(|i| i.label.as_str()).collect()
832 }
833
834 #[test]
835 fn completes_op_names_by_prefix() {
836 let items = completions("sm", 1, 3);
837 let ls = labels(&items);
838 assert!(ls.contains(&"sma"));
839 assert!(!ls.contains(&"rank"), "prefix `sm` should exclude rank");
840 }
841
842 #[test]
843 fn empty_prefix_offers_the_whole_vocabulary() {
844 let items = completions("", 1, 1);
845 let ls = labels(&items);
846 assert!(ls.contains(&"sma"));
847 assert!(ls.contains(&"close"));
848 assert!(ls.contains(&"let"));
849 }
850
851 #[test]
852 fn inside_a_call_offers_keyword_arguments_first() {
853 let items = completions("rank(close, )", 1, 13);
855 let field = items
856 .iter()
857 .find(|i| i.label == "ascending")
858 .expect("ascending field offered");
859 assert_eq!(field.kind, CompletionKind::Field);
860 assert_eq!(field.insert_text, "ascending=");
861 }
862
863 #[test]
864 fn completes_let_bound_names() {
865 let src = "let ma = sma(close, 20)\nclose > m";
866 let items = completions(src, 2, 10);
867 let ma = items.iter().find(|i| i.label == "ma").expect("ma offered");
868 assert_eq!(ma.kind, CompletionKind::Variable);
869 }
870
871 #[test]
872 fn enclosing_call_handles_lists_closed_calls_and_leading_paren() {
873 let items = completions("neutralize(close, [pe, ", 1, 23);
875 assert!(items
876 .iter()
877 .any(|i| i.label == "by" && i.kind == CompletionKind::Field));
878
879 let items = completions("sma(close, 2) and cl", 1, 21);
881 assert!(labels(&items).contains(&"close"));
882 assert!(!items.iter().any(|i| i.kind == CompletionKind::Field));
883
884 let items = completions("(cl", 1, 4);
886 assert!(labels(&items).contains(&"close"));
887 }
888
889 #[test]
890 fn completion_survives_a_lex_error() {
891 let items = completions("$sm", 1, 1);
893 assert!(!items.is_empty());
894 assert!(labels(&items).contains(&"sma"));
895 }
896
897 #[test]
898 fn no_duplicate_labels_of_the_same_kind() {
899 let items = completions("", 1, 1);
900 let mut seen = std::collections::HashSet::new();
901 for it in &items {
902 assert!(
903 seen.insert((it.label.clone(), it.kind)),
904 "duplicate: {} / {:?}",
905 it.label,
906 it.kind
907 );
908 }
909 }
910
911 #[test]
914 fn token_end_covers_every_kind_and_let_without_name() {
915 let toks = lex("let x = \"s\" >= 1 + (a) [ , ]").unwrap();
916 for t in &toks {
917 let (_, end) = token_end(t);
918 assert!(end >= t.col);
919 }
920 let str_tok = toks
922 .iter()
923 .find(|t| matches!(t.kind, TokenKind::Str(_)))
924 .unwrap();
925 assert_eq!(token_end(str_tok), (str_tok.line, str_tok.col + 3));
926 let eof = toks.last().unwrap();
928 assert_eq!(token_end(eof), (eof.line, eof.col));
929 assert!(let_bound_names(&lex("let").unwrap()).is_empty());
931 }
932
933 #[test]
934 fn completion_kind_tags_round_trip() {
935 assert_eq!(CompletionKind::Function.as_str(), "function");
936 assert_eq!(CompletionKind::Field.as_str(), "field");
937 assert_eq!(CompletionKind::Variable.as_str(), "variable");
938 assert_eq!(CompletionKind::Series.as_str(), "series");
939 assert_eq!(CompletionKind::Keyword.as_str(), "keyword");
940 }
941
942 fn typed(src: &str) -> Vec<(&'static str, usize, usize, usize)> {
945 tokens(src)
946 .into_iter()
947 .map(|t| (t.token_type.as_str(), t.line, t.col, t.end_col))
948 .collect()
949 }
950
951 #[test]
952 fn classifies_call_series_number_and_punctuation() {
953 assert_eq!(
955 typed("is_largest(sma(close, 2), 3)"),
956 vec![
957 ("function", 1, 1, 11), ("punctuation", 1, 11, 12), ("function", 1, 12, 15), ("punctuation", 1, 15, 16), ("series", 1, 16, 21), ("punctuation", 1, 21, 22), ("number", 1, 23, 24), ("punctuation", 1, 24, 25), ("punctuation", 1, 25, 26), ("number", 1, 27, 28), ("punctuation", 1, 28, 29), ]
969 );
970 }
971
972 #[test]
973 fn keyword_args_logic_and_operators() {
974 let out = typed("rank(close, ascending=true) and close > sma(close, 2)");
976 assert!(out.contains(&("parameter", 1, 13, 22))); assert!(out.contains(&("operator", 1, 22, 23))); assert!(out.contains(&("keyword", 1, 23, 27))); assert!(out.contains(&("keyword", 1, 29, 32))); assert!(out.contains(&("operator", 1, 39, 40))); }
982
983 #[test]
984 fn not_is_keyword_even_before_paren() {
985 let out = typed("not (close)");
987 assert_eq!(out[0], ("keyword", 1, 1, 4));
988 }
989
990 #[test]
991 fn numbers_keep_their_full_width() {
992 assert_eq!(
995 typed("x >= 1_000_000"),
996 vec![
997 ("series", 1, 1, 2), ("operator", 1, 3, 5), ("number", 1, 6, 15), ]
1001 );
1002 assert!(typed("5e8").contains(&("number", 1, 1, 4)));
1003 }
1004
1005 #[test]
1006 fn comments_are_spans_and_ignore_hashes_in_strings() {
1007 let out = typed("close # buy\n");
1009 assert!(out.contains(&("comment", 1, 7, 12)));
1010 let s = typed("in_sector(close, \"A#B\")");
1011 assert!(s.iter().all(|t| t.0 != "comment"));
1012 }
1013
1014 #[test]
1015 fn lex_error_still_yields_comment_spans() {
1016 let out = typed("# note\nsma(close, \"oops");
1018 assert_eq!(out.first(), Some(&("comment", 1, 1, 7)));
1019 }
1020
1021 #[test]
1022 fn token_type_tags_round_trip() {
1023 for (ty, tag) in [
1024 (TokenType::Comment, "comment"),
1025 (TokenType::Number, "number"),
1026 (TokenType::Str, "string"),
1027 (TokenType::Keyword, "keyword"),
1028 (TokenType::Function, "function"),
1029 (TokenType::Parameter, "parameter"),
1030 (TokenType::Series, "series"),
1031 (TokenType::Operator, "operator"),
1032 (TokenType::Punctuation, "punctuation"),
1033 ] {
1034 assert_eq!(ty.as_str(), tag);
1035 }
1036 }
1037}