calcli/domain/
completion.rs1use std::ops::Range;
10
11use crate::domain::highlight::{self, ANS, CONSTANTS, FUNCTIONS};
12use crate::domain::variables::VariableStore;
13
14pub fn candidates(variables: &VariableStore) -> Vec<String> {
19 let mut names: Vec<String> = Vec::new();
20 for (name, _) in variables.iter() {
21 push_unique(&mut names, name);
22 }
23 for name in FUNCTIONS {
24 push_unique(&mut names, name);
25 }
26 for name in CONSTANTS {
27 push_unique(&mut names, name);
28 }
29 push_unique(&mut names, ANS);
30 names
31}
32
33fn push_unique(names: &mut Vec<String>, name: &str) {
35 if !names.iter().any(|existing| existing == name) {
36 names.push(name.to_string());
37 }
38}
39
40pub fn identifier_before(input: &str, cursor: usize) -> Range<usize> {
46 let chars: Vec<char> = input.chars().collect();
47 let end = cursor.min(chars.len());
48 let start = walk_back(&chars, end);
49 if starts_identifier(&chars, start, end) {
50 start..end
51 } else {
52 end..end
53 }
54}
55
56pub fn identifier_at(input: &str, cursor: usize) -> Range<usize> {
61 let chars: Vec<char> = input.chars().collect();
62 let caret = cursor.min(chars.len());
63 let start = walk_back(&chars, caret);
64 let mut end = caret;
65 while end < chars.len() && highlight::is_identifier_part(chars[end]) {
66 end += 1;
67 }
68 if starts_identifier(&chars, start, end) {
69 start..end
70 } else {
71 caret..caret
72 }
73}
74
75fn walk_back(chars: &[char], end: usize) -> usize {
77 let mut start = end;
78 while start > 0 && highlight::is_identifier_part(chars[start - 1]) {
79 start -= 1;
80 }
81 start
82}
83
84fn starts_identifier(chars: &[char], start: usize, end: usize) -> bool {
87 start < end && highlight::is_identifier_start(chars[start])
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::domain::quantity::Quantity;
94
95 fn vars(names: &[&str]) -> VariableStore {
96 VariableStore::from_pairs(
97 names
98 .iter()
99 .map(|name| (name.to_string(), Quantity::dimensionless(1.0))),
100 )
101 }
102
103 #[test]
104 fn candidates_list_variables_first_then_the_builtins() {
105 let candidates = candidates(&vars(&["radius"]));
106 assert_eq!(candidates.first().map(String::as_str), Some("radius"));
107 assert!(candidates.iter().any(|name| name == "sin"));
108 assert!(candidates.iter().any(|name| name == "pi"));
109 assert_eq!(candidates.last().map(String::as_str), Some("ans"));
110 }
111
112 #[test]
113 fn a_variable_shadowing_a_builtin_is_listed_once() {
114 let candidates = candidates(&vars(&["min"]));
115 assert_eq!(candidates.iter().filter(|name| *name == "min").count(), 1,);
116 }
117
118 #[test]
119 fn identifier_before_spans_the_word_ending_at_the_caret() {
120 assert_eq!(identifier_before("sin", 3), 0..3);
121 assert_eq!(identifier_before("sinh", 2), 0..2);
123 assert_eq!(identifier_before("2*co", 4), 2..4);
125 }
126
127 #[test]
128 fn identifier_before_is_empty_after_a_non_identifier_character() {
129 assert_eq!(identifier_before("2+", 2), 2..2);
130 assert_eq!(identifier_before("sin(", 4), 4..4);
131 assert_eq!(identifier_before("", 0), 0..0);
132 }
133
134 #[test]
135 fn identifier_before_ignores_a_bare_number() {
136 assert_eq!(identifier_before("2", 1), 1..1);
138 assert_eq!(identifier_before("1+2", 3), 3..3);
139 }
140
141 #[test]
142 fn identifier_at_spans_the_whole_word_around_the_caret() {
143 assert_eq!(identifier_at("sinh", 2), 0..4);
146 assert_eq!(identifier_at("2*cos", 4), 2..5);
147 assert_eq!(identifier_at("2+", 2), 2..2);
149 assert_eq!(identifier_at("42", 1), 1..1);
150 }
151}