1use dioxus::prelude::*;
26
27#[derive(Clone, Copy, PartialEq, Eq, Default)]
29pub enum Language {
30 #[default]
31 Logos, Vernacular, Rust, }
35
36#[derive(Clone, Copy, PartialEq, Eq)]
38enum TokenKind {
39 Keyword,
40 Type,
41 String,
42 Number,
43 Comment,
44 Operator,
45 Punctuation,
46 Identifier,
47 Builtin,
48}
49
50struct Token {
52 text: String,
53 kind: TokenKind,
54}
55
56const CODE_EDITOR_STYLE: &str = r#"
57.code-editor {
58 display: flex;
59 flex-direction: column;
60 height: 100%;
61 background: #0f1419;
62 font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
63}
64
65.code-editor-input {
66 position: relative;
67 flex: 1;
68 min-height: 200px;
69}
70
71.code-editor-textarea {
72 position: absolute;
73 top: 0;
74 left: 0;
75 width: 100%;
76 height: 100%;
77 padding: 16px;
78 padding-bottom: 50%; /* Extra space to scroll past end */
79 margin: 0;
80 font-size: 14px;
81 font-family: inherit;
82 line-height: 1.6;
83 background: transparent;
84 color: transparent;
85 caret-color: #667eea;
86 border: none;
87 outline: none;
88 resize: none;
89 white-space: pre-wrap;
90 word-wrap: break-word;
91 overflow-wrap: break-word;
92 overflow: auto;
93 z-index: 2;
94 box-sizing: border-box;
95}
96
97.code-editor-highlight {
98 position: absolute;
99 top: 0;
100 left: 0;
101 width: 100%;
102 height: 100%;
103 padding: 16px;
104 padding-bottom: 50%; /* Extra space to scroll past end */
105 margin: 0;
106 font-size: 14px;
107 font-family: inherit;
108 line-height: 1.6;
109 color: #e8eaed;
110 white-space: pre-wrap;
111 word-wrap: break-word;
112 overflow-wrap: break-word;
113 overflow: auto;
114 pointer-events: none;
115 z-index: 1;
116 box-sizing: border-box;
117 /* Hide scrollbar but keep scroll functionality for sync */
118 scrollbar-width: none;
119 -ms-overflow-style: none;
120}
121
122.code-editor-highlight::-webkit-scrollbar {
123 display: none;
124}
125
126/* Syntax highlighting colors - no font-weight/style changes to keep heights identical */
127.tok-keyword { color: #c678dd; }
128.tok-type { color: #e5c07b; }
129.tok-string { color: #98c379; }
130.tok-number { color: #d19a66; }
131.tok-comment { color: #5c6370; }
132.tok-operator { color: #56b6c2; }
133.tok-punctuation { color: #abb2bf; }
134.tok-identifier { color: #e8eaed; }
135.tok-builtin { color: #61afef; }
136
137/* Mobile optimizations */
138@media (max-width: 768px) {
139 .code-editor-textarea,
140 .code-editor-highlight {
141 font-size: 16px; /* Prevent iOS zoom */
142 padding: 12px;
143 }
144}
145"#;
146
147fn tokenize(source: &str, language: Language) -> Vec<Token> {
149 let mut tokens = Vec::new();
150 let chars: Vec<char> = source.chars().collect();
151 let len = chars.len();
152 let mut i = 0;
153
154 let keywords = match language {
155 Language::Logos => &[
156 "let", "set", "if", "then", "else", "while", "repeat", "for", "in",
157 "return", "match", "with", "end", "function", "struct", "enum",
158 "show", "read", "write", "push", "pop", "add", "remove",
159 "and", "or", "not", "true", "false", "nothing",
160 "zone", "concurrent", "parallel", "launch", "send", "receive", "select",
161 "mount", "sync", "merge", "increase", "decrease",
162 ][..],
163 Language::Vernacular => &[
164 "Definition", "Check", "Eval", "Inductive", "Theorem", "Lemma",
165 "Proof", "Qed", "Axiom", "Hypothesis", "Variable", "Parameter",
166 "fun", "forall", "exists", "match", "with", "end", "let", "in",
167 "Prop", "Type", "Set", "Nat", "Bool", "True", "False",
168 "Not", "And", "Or", "Eq", "Ex", "All",
169 ][..],
170 Language::Rust => &[
171 "fn", "let", "mut", "const", "static", "if", "else", "match", "loop",
172 "while", "for", "in", "return", "break", "continue", "struct", "enum",
173 "impl", "trait", "type", "pub", "mod", "use", "crate", "self", "super",
174 "async", "await", "move", "ref", "where", "true", "false",
175 ][..],
176 };
177
178 let types = match language {
179 Language::Logos => &[
180 "Int", "Text", "Bool", "Real", "Char", "Byte", "Nothing",
181 "Seq", "Map", "Set", "Option", "Result", "Tuple",
182 "Persistent", "Distributed",
183 ][..],
184 Language::Vernacular => &[
185 "Prop", "Type", "Set", "Nat", "Bool", "Int", "List", "Option",
186 "Syntax", "Derivation", "Term",
187 ][..],
188 Language::Rust => &[
189 "i8", "i16", "i32", "i64", "i128", "isize",
190 "u8", "u16", "u32", "u64", "u128", "usize",
191 "f32", "f64", "bool", "char", "str", "String",
192 "Vec", "HashMap", "Option", "Result", "Box", "Rc", "Arc",
193 ][..],
194 };
195
196 let builtins = match language {
197 Language::Logos => &[
198 "show", "length", "first", "last", "rest", "append",
199 "contains", "keys", "values", "range",
200 ][..],
201 Language::Vernacular => &[
202 "Zero", "Succ", "Nil", "Cons", "Some", "None",
203 "refl", "eq_ind", "nat_ind", "syn_diag", "syn_quote", "syn_size",
204 "concludes", "Provable", "Consistent",
205 ][..],
206 Language::Rust => &[
207 "println", "print", "format", "vec", "panic", "assert",
208 "Some", "None", "Ok", "Err",
209 ][..],
210 };
211
212 while i < len {
213 let c = chars[i];
214
215 if c.is_whitespace() {
217 let start = i;
218 while i < len && chars[i].is_whitespace() {
219 i += 1;
220 }
221 tokens.push(Token {
222 text: chars[start..i].iter().collect(),
223 kind: TokenKind::Identifier, });
225 continue;
226 }
227
228 let comment_start = match language {
230 Language::Logos | Language::Vernacular => "--",
231 Language::Rust => "//",
232 };
233 if i + comment_start.len() <= len {
234 let slice: String = chars[i..i + comment_start.len()].iter().collect();
235 if slice == comment_start {
236 let start = i;
237 while i < len && chars[i] != '\n' {
238 i += 1;
239 }
240 tokens.push(Token {
241 text: chars[start..i].iter().collect(),
242 kind: TokenKind::Comment,
243 });
244 continue;
245 }
246 }
247
248 if c == '"' {
250 let start = i;
251 i += 1;
252 while i < len && chars[i] != '"' {
253 if chars[i] == '\\' && i + 1 < len {
254 i += 2;
255 } else {
256 i += 1;
257 }
258 }
259 if i < len {
260 i += 1; }
262 tokens.push(Token {
263 text: chars[start..i].iter().collect(),
264 kind: TokenKind::String,
265 });
266 continue;
267 }
268
269 if c.is_ascii_digit() {
271 let start = i;
272 while i < len && (chars[i].is_ascii_digit() || chars[i] == '.' || chars[i] == '_') {
273 i += 1;
274 }
275 tokens.push(Token {
276 text: chars[start..i].iter().collect(),
277 kind: TokenKind::Number,
278 });
279 continue;
280 }
281
282 if c.is_alphabetic() || c == '_' {
284 let start = i;
285 while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
286 i += 1;
287 }
288 let word: String = chars[start..i].iter().collect();
289 let kind = if keywords.contains(&word.as_str()) {
290 TokenKind::Keyword
291 } else if types.contains(&word.as_str()) {
292 TokenKind::Type
293 } else if builtins.contains(&word.as_str()) {
294 TokenKind::Builtin
295 } else {
296 TokenKind::Identifier
297 };
298 tokens.push(Token { text: word, kind });
299 continue;
300 }
301
302 let operators = [
304 "->", "=>", ":=", "<=", ">=", "==", "!=", "&&", "||",
305 "+", "-", "*", "/", "%", "=", "<", ">", "!", "&", "|", "^",
306 ];
307 let mut matched = false;
308 for op in operators {
309 if i + op.len() <= len {
310 let slice: String = chars[i..i + op.len()].iter().collect();
311 if slice == op {
312 tokens.push(Token {
313 text: slice,
314 kind: TokenKind::Operator,
315 });
316 i += op.len();
317 matched = true;
318 break;
319 }
320 }
321 }
322 if matched {
323 continue;
324 }
325
326 if "(){}[].,;:".contains(c) {
328 tokens.push(Token {
329 text: c.to_string(),
330 kind: TokenKind::Punctuation,
331 });
332 i += 1;
333 continue;
334 }
335
336 tokens.push(Token {
338 text: c.to_string(),
339 kind: TokenKind::Identifier,
340 });
341 i += 1;
342 }
343
344 tokens
345}
346
347fn token_class(kind: TokenKind) -> &'static str {
349 match kind {
350 TokenKind::Keyword => "tok-keyword",
351 TokenKind::Type => "tok-type",
352 TokenKind::String => "tok-string",
353 TokenKind::Number => "tok-number",
354 TokenKind::Comment => "tok-comment",
355 TokenKind::Operator => "tok-operator",
356 TokenKind::Punctuation => "tok-punctuation",
357 TokenKind::Identifier => "tok-identifier",
358 TokenKind::Builtin => "tok-builtin",
359 }
360}
361
362#[component]
364pub fn CodeEditor(
365 value: String,
366 on_change: EventHandler<String>,
367 language: Language,
368 #[props(default = "Enter code...".to_string())]
369 placeholder: String,
370 #[props(default = false)]
371 readonly: bool,
372) -> Element {
373 let tokens = tokenize(&value, language);
374
375 rsx! {
376 style { "{CODE_EDITOR_STYLE}" }
377
378 div { class: "code-editor",
379 div { class: "code-editor-input",
380 div { class: "code-editor-highlight",
382 for token in tokens {
383 if token.text.chars().all(|c| c.is_whitespace()) {
384 "{token.text}"
386 } else {
387 span {
388 class: "{token_class(token.kind)}",
389 "{token.text}"
390 }
391 }
392 }
393 }
394
395 textarea {
397 class: "code-editor-textarea",
398 placeholder: "{placeholder}",
399 value: "{value}",
400 readonly: readonly,
401 spellcheck: "false",
402 autocomplete: "off",
403 autocapitalize: "off",
404 oninput: move |evt| {
405 on_change.call(evt.value());
406 #[cfg(target_arch = "wasm32")]
408 {
409 let _ = js_sys::eval(r#"
410 requestAnimationFrame(function() {
411 document.querySelectorAll('.code-editor-textarea').forEach(function(ta) {
412 var highlight = ta.previousElementSibling;
413 if (highlight && highlight.classList.contains('code-editor-highlight')) {
414 var taMax = ta.scrollHeight - ta.clientHeight;
415 var hlMax = highlight.scrollHeight - highlight.clientHeight;
416 // Cap scroll to the shorter element's max
417 var maxScroll = Math.min(taMax, hlMax);
418 if (ta.scrollTop > maxScroll) {
419 ta.scrollTop = maxScroll;
420 }
421 highlight.scrollTop = ta.scrollTop;
422 highlight.scrollLeft = ta.scrollLeft;
423 }
424 });
425 });
426 "#);
427 }
428 },
429 onscroll: move |_| {
430 #[cfg(target_arch = "wasm32")]
431 {
432 let _ = js_sys::eval(r#"
433 document.querySelectorAll('.code-editor-textarea').forEach(function(ta) {
434 var highlight = ta.previousElementSibling;
435 if (highlight && highlight.classList.contains('code-editor-highlight')) {
436 var taMax = ta.scrollHeight - ta.clientHeight;
437 var hlMax = highlight.scrollHeight - highlight.clientHeight;
438 // Cap scroll to the shorter element's max
439 var maxScroll = Math.min(taMax, hlMax);
440 if (ta.scrollTop > maxScroll) {
441 ta.scrollTop = maxScroll;
442 }
443 highlight.scrollTop = ta.scrollTop;
444 highlight.scrollLeft = ta.scrollLeft;
445 }
446 });
447 "#);
448 }
449 },
450 }
451 }
452 }
453 }
454}
455
456#[component]
458pub fn CodeView(
459 code: String,
460 language: Language,
461) -> Element {
462 let tokens = tokenize(&code, language);
463
464 rsx! {
465 style { "{CODE_EDITOR_STYLE}" }
466
467 div { class: "code-editor",
468 div { class: "code-editor-highlight",
469 style: "position: relative; height: 100%; overflow: auto; pointer-events: auto;",
470 for token in tokens {
471 if token.text.chars().all(|c| c.is_whitespace()) {
472 "{token.text}"
473 } else {
474 span {
475 class: "{token_class(token.kind)}",
476 "{token.text}"
477 }
478 }
479 }
480 }
481 }
482 }
483}