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#[cfg(test)]
515mod tests {
516 use super::*;
517
518 fn series(names: &[&str]) -> Vec<String> {
521 names.iter().map(|s| s.to_string()).collect()
522 }
523
524 #[test]
525 fn clean_source_has_no_diagnostics() {
526 assert!(diagnostics("close > sma(close, 2)", None).is_empty());
527 assert!(diagnostics("close > sma(close, 2)", Some(&series(&["close"]))).is_empty());
529 }
530
531 #[test]
532 fn parse_error_becomes_a_ranged_error_diagnostic() {
533 let diags = diagnostics("sma(close, 2", None);
534 assert_eq!(diags.len(), 1);
535 assert_eq!(diags[0].severity, Severity::Error);
536 assert!(diags[0].end_col > diags[0].col);
537 }
538
539 #[test]
540 fn lex_error_is_reported_without_panicking() {
541 let diags = diagnostics("close $ 1", None);
542 assert_eq!(diags.len(), 1);
543 assert_eq!(diags[0].severity, Severity::Error);
544 assert_eq!((diags[0].line, diags[0].col), (1, 7));
545 }
546
547 #[test]
548 fn parse_error_on_unknown_op_spans_the_word() {
549 let diags = diagnostics("frobnicate(close)", None);
550 assert_eq!(diags.len(), 1);
551 assert_eq!(diags[0].col, 1);
553 assert_eq!(diags[0].end_col, 1 + "frobnicate".len());
554 }
555
556 #[test]
557 fn unknown_series_warning_is_ranged_when_a_series_list_is_given() {
558 let diags = diagnostics("clsoe > 1", Some(&series(&["close", "pe"])));
559 assert_eq!(diags.len(), 1);
560 assert_eq!(diags[0].severity, Severity::Warning);
561 assert!(diags[0].message.contains("close"), "{}", diags[0].message);
562 assert_eq!((diags[0].col, diags[0].end_col), (1, 1 + "clsoe".len()));
564 }
565
566 #[test]
567 fn no_unknown_series_check_without_a_list() {
568 assert!(diagnostics("clsoe > 1", None).is_empty());
570 }
571
572 #[test]
573 fn unused_let_binding_warns_even_without_a_series_list() {
574 let diags = diagnostics("let ma = sma(close, 20)\nclose > 1", None);
575 assert_eq!(diags.len(), 1);
576 assert_eq!(diags[0].severity, Severity::Warning);
577 assert!(diags[0].message.contains("unused let binding `ma`"));
578 assert_eq!(diags[0].end_col, diags[0].col + "ma".len());
580 }
581
582 #[test]
585 fn hover_on_op_shows_signature_and_description() {
586 let h = hover("close > sma(close, 2)", 1, 9).expect("hover on sma");
587 assert!(h.markdown.contains("sma(of, n)"), "{}", h.markdown);
588 assert!(h.markdown.contains("moving average"));
589 assert!(h.markdown.contains("Aliases"), "{}", h.markdown);
590 assert_eq!((h.line, h.col), (1, 9));
592 assert_eq!(h.end_col, 12);
593 }
594
595 #[test]
596 fn hover_on_operator_and_series_and_keyword() {
597 assert!(hover("close > 1", 1, 7)
598 .unwrap()
599 .markdown
600 .contains("greater"));
601 assert!(hover("close > 1", 1, 1).unwrap().markdown.contains("price"));
602 assert!(hover("let a = close\na > 1", 1, 1)
603 .unwrap()
604 .markdown
605 .contains("bind"));
606 }
607
608 #[test]
609 fn hover_on_unknown_series_and_nothing_are_distinguished() {
610 assert!(hover("roic > 1", 1, 1)
611 .unwrap()
612 .markdown
613 .contains("data series"));
614 assert!(hover("close > 1", 1, 6).is_none()); assert!(hover("close > 1", 1, 9).is_none()); assert!(hover("", 1, 1).is_none());
618 }
619
620 #[test]
621 fn hover_ignores_lex_errors() {
622 assert!(hover("close $", 1, 1).is_none());
623 }
624
625 #[test]
626 fn hover_on_op_without_aliases_omits_alias_line() {
627 let h = hover("ema(close, 5)", 1, 1).unwrap();
629 assert!(h.markdown.contains("ema(of, n)"));
630 assert!(!h.markdown.contains("Aliases"), "{}", h.markdown);
631 }
632
633 #[test]
634 fn hover_on_boolean_literal_and_logical_words() {
635 assert!(hover("rank(close, ascending=true)", 1, 23)
636 .unwrap()
637 .markdown
638 .contains("boolean"));
639 assert!(hover("a and b", 1, 3).unwrap().markdown.contains("AND"));
640 assert!(hover("a or b", 1, 3).unwrap().markdown.contains("OR"));
641 }
642
643 #[test]
644 fn every_operator_symbol_has_hover_text() {
645 for sym in [">", "<", ">=", "<=", "+", "-", "*", "/"] {
646 assert!(binop_markdown(sym).is_some(), "no hover for `{sym}`");
647 }
648 assert!(binop_markdown("??").is_none());
649 }
650
651 #[test]
652 fn every_keyword_has_hover_text() {
653 for kw in ["let", "and", "or", "true", "false"] {
654 assert!(keyword_markdown(kw).is_some(), "no hover for `{kw}`");
655 }
656 assert!(keyword_markdown("close").is_none());
657 }
658
659 fn labels(items: &[CompletionItem]) -> Vec<&str> {
662 items.iter().map(|i| i.label.as_str()).collect()
663 }
664
665 #[test]
666 fn completes_op_names_by_prefix() {
667 let items = completions("sm", 1, 3);
668 let ls = labels(&items);
669 assert!(ls.contains(&"sma"));
670 assert!(!ls.contains(&"rank"), "prefix `sm` should exclude rank");
671 }
672
673 #[test]
674 fn empty_prefix_offers_the_whole_vocabulary() {
675 let items = completions("", 1, 1);
676 let ls = labels(&items);
677 assert!(ls.contains(&"sma"));
678 assert!(ls.contains(&"close"));
679 assert!(ls.contains(&"let"));
680 }
681
682 #[test]
683 fn inside_a_call_offers_keyword_arguments_first() {
684 let items = completions("rank(close, )", 1, 13);
686 let field = items
687 .iter()
688 .find(|i| i.label == "ascending")
689 .expect("ascending field offered");
690 assert_eq!(field.kind, CompletionKind::Field);
691 assert_eq!(field.insert_text, "ascending=");
692 }
693
694 #[test]
695 fn completes_let_bound_names() {
696 let src = "let ma = sma(close, 20)\nclose > m";
697 let items = completions(src, 2, 10);
698 let ma = items.iter().find(|i| i.label == "ma").expect("ma offered");
699 assert_eq!(ma.kind, CompletionKind::Variable);
700 }
701
702 #[test]
703 fn enclosing_call_handles_lists_closed_calls_and_leading_paren() {
704 let items = completions("neutralize(close, [pe, ", 1, 23);
706 assert!(items
707 .iter()
708 .any(|i| i.label == "by" && i.kind == CompletionKind::Field));
709
710 let items = completions("sma(close, 2) and cl", 1, 21);
712 assert!(labels(&items).contains(&"close"));
713 assert!(!items.iter().any(|i| i.kind == CompletionKind::Field));
714
715 let items = completions("(cl", 1, 4);
717 assert!(labels(&items).contains(&"close"));
718 }
719
720 #[test]
721 fn completion_survives_a_lex_error() {
722 let items = completions("$sm", 1, 1);
724 assert!(!items.is_empty());
725 assert!(labels(&items).contains(&"sma"));
726 }
727
728 #[test]
729 fn no_duplicate_labels_of_the_same_kind() {
730 let items = completions("", 1, 1);
731 let mut seen = std::collections::HashSet::new();
732 for it in &items {
733 assert!(
734 seen.insert((it.label.clone(), it.kind)),
735 "duplicate: {} / {:?}",
736 it.label,
737 it.kind
738 );
739 }
740 }
741
742 #[test]
745 fn token_end_covers_every_kind_and_let_without_name() {
746 let toks = lex("let x = \"s\" >= 1 + (a) [ , ]").unwrap();
747 for t in &toks {
748 let (_, end) = token_end(t);
749 assert!(end >= t.col);
750 }
751 let str_tok = toks
753 .iter()
754 .find(|t| matches!(t.kind, TokenKind::Str(_)))
755 .unwrap();
756 assert_eq!(token_end(str_tok), (str_tok.line, str_tok.col + 3));
757 let eof = toks.last().unwrap();
759 assert_eq!(token_end(eof), (eof.line, eof.col));
760 assert!(let_bound_names(&lex("let").unwrap()).is_empty());
762 }
763
764 #[test]
765 fn completion_kind_tags_round_trip() {
766 assert_eq!(CompletionKind::Function.as_str(), "function");
767 assert_eq!(CompletionKind::Field.as_str(), "field");
768 assert_eq!(CompletionKind::Variable.as_str(), "variable");
769 assert_eq!(CompletionKind::Series.as_str(), "series");
770 assert_eq!(CompletionKind::Keyword.as_str(), "keyword");
771 }
772}