1use super::code_editor::{Token, TokenKind};
8
9const KEYWORDS: &[&str] = &[
12 "Definition", "Inductive", "Check", "Eval", "Theorem", "Lemma", "Axiom", "Hypothesis",
13 "Variable", "Parameter", "Proof", "Qed", "Statement", "To", "Consider", "When", "Yield", "Let",
14 "let", "Theory", "Main", "Note", "Attribute", "Apply", "Name", "hint", "given", "prove",
15 "cites", "be", "yields", "fix", "forall", "fun", "exists", "match", "return", "with", "end",
16 "in", "if", "then", "else", "as", "by", "binder", "source", "mdata",
17];
18
19const CONNECTIVES: &[&str] = &["and", "or", "implies", "equals", "is", "either", "not"];
21
22const TACTICS: &[&str] = &[
24 "intro", "intros", "assumption", "auto", "decide", "split", "left", "right", "ring", "refl",
25 "lia", "omega", "cc", "simp", "induction", "inversion", "rewrite", "apply", "exact",
26 "destruct", "cases", "reflexivity", "symmetry", "unfold", "trivial", "contradiction",
27 "constructor", "diagonalization",
28];
29
30const SORTS: &[&str] = &["Prop", "Type", "Set"];
32
33const DECLARERS: &[&str] = &["Definition", "Inductive", "Theorem", "Lemma", "Axiom", "Hypothesis", "Variable", "Parameter", "To", "Theory"];
35
36const OPERATORS: &[&str] = &[
38 "<->", "->", "=>", ":=", "<=", ">=", "<>", "/\\", "\\/", "::", "=", "<", ">", "+", "-", "*", "/", "|",
39 ":", "~", "^", "%",
40];
41
42const PUNCTUATION: &[char] = &['(', ')', '[', ']', '{', '}', '.', ',', ';'];
43
44pub(crate) fn tokens(source: &str) -> Vec<Token> {
46 let mut out: Vec<Token> = Vec::new();
47 let mut at = 0;
48 let mut in_proof = false;
50 let mut declares: Option<TokenKind> = None;
53 let mut line_start = true;
54 while at < source.len() {
55 let rest = &source[at..];
56 let first = rest.chars().next().expect("a token starts somewhere");
57 let (len, kind) = if first.is_whitespace() {
58 let len = rest.find(|c: char| !c.is_whitespace()).unwrap_or(rest.len());
59 if rest[..len].matches('\n').count() > 1 {
60 in_proof = false;
61 }
62 (len, TokenKind::Identifier)
63 } else if rest.starts_with("--") {
64 (rest.find('\n').unwrap_or(rest.len()), TokenKind::Comment)
65 } else if rest.starts_with("##") {
66 in_proof = false;
67 (rest.find(|c: char| c != '#').unwrap_or(rest.len()), TokenKind::Punctuation)
68 } else if first == '"' {
69 (string_len(rest), TokenKind::String)
70 } else if first.is_ascii_digit() {
71 (rest.find(|c: char| !(c.is_ascii_digit() || c == '_')).unwrap_or(rest.len()), TokenKind::Number)
72 } else if first.is_alphabetic() || first == '_' {
73 let len = rest.find(|c: char| !(c.is_alphanumeric() || c == '_' || c == '\'')).unwrap_or(rest.len());
74 let word = &rest[..len];
75 if line_start && KEYWORDS.contains(&word) && word != "Proof" {
76 in_proof = false;
77 }
78 let kind = if let Some(declared) = declares.filter(|_| !KEYWORDS.contains(&word)) {
79 declares = None;
80 declared
81 } else if in_proof && TACTICS.contains(&word) {
82 TokenKind::Builtin
83 } else if KEYWORDS.contains(&word) {
84 declares = DECLARERS.contains(&word).then_some(if word == "Inductive" { TokenKind::Type } else { TokenKind::Builtin });
85 in_proof |= word == "Proof";
86 TokenKind::Keyword
87 } else if CONNECTIVES.contains(&word) {
88 TokenKind::Operator
89 } else if SORTS.contains(&word) || word.starts_with(char::is_uppercase) {
90 TokenKind::Type
91 } else {
92 TokenKind::Identifier
93 };
94 (len, kind)
95 } else if let Some(op) = OPERATORS.iter().find(|op| rest.starts_with(**op)) {
96 (op.len(), TokenKind::Operator)
97 } else if is_symbol(first) {
98 (first.len_utf8(), TokenKind::Operator)
99 } else if PUNCTUATION.contains(&first) {
100 (first.len_utf8(), TokenKind::Punctuation)
101 } else {
102 (first.len_utf8(), TokenKind::Identifier)
103 };
104 let text = &rest[..len];
105 line_start = if text.trim().is_empty() { line_start || text.contains('\n') } else { false };
106 out.push(Token { text: rest[..len].to_string(), kind });
107 at += len;
108 }
109 out
110}
111
112fn is_symbol(c: char) -> bool {
114 matches!(c, '\u{2190}'..='\u{21FF}' | '\u{2200}'..='\u{22FF}' | '\u{27F0}'..='\u{27FF}' | '¬' | 'λ' | 'Π' | 'Σ')
115}
116
117fn string_len(rest: &str) -> usize {
119 let mut escaped = false;
120 for (i, c) in rest.char_indices().skip(1) {
121 match c {
122 '"' if !escaped => return i + 1,
123 '\\' if !escaped => escaped = true,
124 _ => escaped = false,
125 }
126 }
127 rest.len()
128}
129
130#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
134pub(crate) const LIGATURE_WORDS: &[&str] = &["forall", "exists", "fun"];
135#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
136pub(crate) const LIGATURE_OPERATORS: &[&str] = &["->", "=>", "<=", ">="];
137#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
138pub(crate) const WORD_CHARS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'";
139#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
140pub(crate) const OPERATOR_CHARS: &str = "-<>=|:!~*+/\\";
141
142#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
145pub(crate) fn ligatures(text: &str) -> Vec<std::ops::Range<usize>> {
146 let bytes = text.as_bytes();
147 let spellings = LIGATURE_WORDS
149 .iter()
150 .map(|s| (*s, WORD_CHARS))
151 .chain(LIGATURE_OPERATORS.iter().map(|s| (*s, OPERATOR_CHARS)));
152 let mut out = Vec::new();
153 let mut at = 0;
154 while at < text.len() {
155 let before = at.checked_sub(1).map(|i| bytes[i]);
156 let found = spellings.clone().find(|(spelling, joiners)| {
157 let joins = |b: u8| joiners.as_bytes().contains(&b);
158 text[at..].starts_with(spelling)
159 && !before.is_some_and(joins)
160 && !bytes.get(at + spelling.len()).is_some_and(|b| joins(*b))
161 });
162 match found {
163 Some((spelling, _)) => {
164 out.push(at..at + spelling.len());
165 at += spelling.len();
166 }
167 None => at += text[at..].chars().next().map_or(1, char::len_utf8),
168 }
169 }
170 out
171}
172
173#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
176pub(crate) enum Snap {
177 Nearest,
178 Start,
179 End,
180}
181
182#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
185pub(crate) fn snap(text: &str, at: usize, snap: Snap) -> usize {
186 match ligatures(text).into_iter().find(|r| r.start < at && at < r.end) {
187 Some(r) => match snap {
188 Snap::Start => r.start,
189 Snap::End => r.end,
190 Snap::Nearest if at - r.start < r.end - at => r.start,
191 Snap::Nearest => r.end,
192 },
193 None => at,
194 }
195}
196
197#[cfg(test)]
198#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/components/vernacular/tests.rs"]
199mod tests;