beck_core/editor.rs
1//! What an editor asks the front end, and the answers, once.
2//!
3//! [`docs/04-compiler-architecture.md`](../../../../../docs/04-compiler-architecture.md) §4.6 fixes
4//! the rule this module exists to keep: *"One binary serves `beck build`, `beck check`, `beck lsp`
5//! and `beck explain`; there is no separate language server implementation to drift."* Until now
6//! that was true of the *compiler* and not of the editor: `beck lsp` held the indexing, the
7//! positions and the word-under-the-caret rule, and anything else wanting them — a playground with
8//! a `<textarea>` in it ([`docs/98`](../../../../../docs/98-playground-report.md) §98.9) — had to
9//! write them a second time.
10//!
11//! So the answers live here, where `beck-cli` and a `wasm32-unknown-unknown` module can both reach
12//! them, and neither renders anything of its own:
13//!
14//! | Answer | What produces it |
15//! |---|---|
16//! | [`tokens`] — highlighting | [`beck_syntax::lexer::lex`] and [`beck_syntax::lexer::KEYWORDS`] |
17//! | [`Editor::marks`] — inline diagnostics | the diagnostics the checker pushed |
18//! | [`Editor::hover`] — a signature | [`crate::iface::render_item`], the one `beck iface` writes |
19//! | [`Editor::completions`] — the names in scope | the checked program's own definition table |
20//! | [`Editor::definition`] — where a name is declared | the span the checker recorded |
21//!
22//! # Why the whole file, every time
23//!
24//! An [`Editor`] is one compile. [`docs/64`](../../../../../docs/64-compile-speed-report.md) §64.6
25//! is why that is defensible today: the worst file in this tree costs 4.7 ms through parse, expand
26//! and check, and the median costs 0.75 ms. [`tokens`] deliberately does *not* need one — a file
27//! being typed into is usually a file that does not compile, and highlighting that waited for a
28//! clean parse would go out whenever it was most wanted.
29
30use std::collections::BTreeMap;
31
32use beck_diag::{Diagnostic, Diagnostics, Severity, SourceMap};
33use beck_syntax::lexer::{lex, Raw, KEYWORDS};
34
35use crate::check::Def;
36use crate::core::{Core, CoreKind};
37use crate::iface::{render_item, render_uses, Kind};
38use crate::ty::Tier;
39use crate::Placed;
40
41// ---------------------------------------------------------------------------------------------
42// Highlighting
43// ---------------------------------------------------------------------------------------------
44
45/// What a run of source *is*, for the purpose of colouring it.
46///
47/// Deliberately small. A theme with thirty categories needs a grammar of its own to feed it; these
48/// eight are what the lexer already distinguishes, so every one of them is a fact rather than a
49/// guess.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum TokenKind {
52 /// A word the parser reads as syntax — [`beck_syntax::lexer::KEYWORDS`], and nothing else.
53 Keyword,
54 /// An identifier. Which ones are *bound* is a question for the checker, and an editor that
55 /// coloured a name differently because it failed to resolve would be recolouring the file on
56 /// every keystroke in the middle of a name.
57 Name,
58 /// `:done` — a keyword literal.
59 Atom,
60 Number,
61 Str,
62 Comment,
63 /// `##` — documentation, which is metadata rather than a comment ([`beck_syntax::doc`]).
64 Doc,
65 Punct,
66}
67
68impl TokenKind {
69 /// The name a stylesheet keys on, and the name a test names.
70 pub fn name(self) -> &'static str {
71 match self {
72 TokenKind::Keyword => "keyword",
73 TokenKind::Name => "name",
74 TokenKind::Atom => "atom",
75 TokenKind::Number => "number",
76 TokenKind::Str => "string",
77 TokenKind::Comment => "comment",
78 TokenKind::Doc => "doc",
79 TokenKind::Punct => "punct",
80 }
81 }
82
83 /// The LSP semantic-token type this maps to, from the protocol's own list.
84 ///
85 /// The mapping is here rather than in `beck lsp` so that an editor's colours and the
86 /// playground's are the same decision written once. `##` is a comment to a client that has no
87 /// finer category for it, which is every client: the protocol has no `documentation` type.
88 pub fn lsp_type(self) -> &'static str {
89 match self {
90 TokenKind::Keyword => "keyword",
91 TokenKind::Name => "variable",
92 TokenKind::Atom => "enumMember",
93 TokenKind::Number => "number",
94 TokenKind::Str => "string",
95 TokenKind::Comment | TokenKind::Doc => "comment",
96 TokenKind::Punct => "operator",
97 }
98 }
99
100 /// The legend a `semanticTokens` capability publishes, in the order [`lsp_index`] counts.
101 ///
102 /// [`lsp_index`]: TokenKind::lsp_index
103 pub fn legend() -> Vec<&'static str> {
104 let mut out: Vec<&'static str> = Vec::new();
105 for kind in TokenKind::all() {
106 let name = kind.lsp_type();
107 if !out.contains(&name) {
108 out.push(name);
109 }
110 }
111 out
112 }
113
114 /// This kind's position in [`legend`](TokenKind::legend).
115 pub fn lsp_index(self) -> u32 {
116 TokenKind::legend()
117 .iter()
118 .position(|t| *t == self.lsp_type())
119 .unwrap_or(0) as u32
120 }
121
122 fn all() -> [TokenKind; 8] {
123 [
124 TokenKind::Keyword,
125 TokenKind::Name,
126 TokenKind::Atom,
127 TokenKind::Number,
128 TokenKind::Str,
129 TokenKind::Comment,
130 TokenKind::Doc,
131 TokenKind::Punct,
132 ]
133 }
134}
135
136/// One coloured run: a byte range and what it is.
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub struct Token {
139 pub start: u32,
140 pub end: u32,
141 pub kind: TokenKind,
142}
143
144/// Every coloured run in `text`, in source order, non-overlapping.
145///
146/// The lexer *skips* comments — a comment is not a token, and making it one would put the layout
147/// algorithm's "a comment-only line has no indentation" rule at risk ([`beck_syntax::doc`] says so
148/// for `##`). So comments are recovered from the **gaps between the lexer's own spans**: whatever
149/// the lexer did not claim is whitespace up to a `#`, and a comment from there to the end of the
150/// line. That keeps one scanner rather than two — a second lexer written for highlighting is how
151/// an editor ends up disagreeing with the compiler about where a string ends.
152///
153/// Diagnostics are discarded: a file with an unlexable character still produces a token stream
154/// (that is what `lex` promises), and highlighting is wanted most exactly when the file is broken.
155pub fn tokens(text: &str) -> Vec<Token> {
156 let mut map = SourceMap::new();
157 let file = map.add("editor.beck", text);
158 let mut diags = Diagnostics::new();
159 let lexed = lex(file, text, &mut diags);
160
161 let mut out: Vec<Token> = Vec::new();
162 let mut cursor = 0usize;
163 for token in &lexed {
164 // The synthetic tokens layout inserted (`INDENT`, `DEDENT`, `NEWLINE`, `EOF`) are not runs
165 // of source, and some of them are zero-width. Only what the lexer actually read is
166 // coloured.
167 let Some(raw) = token.raw() else { continue };
168 let (start, end) = (token.span.start as usize, token.span.end as usize);
169 if start < cursor || end > text.len() {
170 continue;
171 }
172 comments_in(text, cursor, start, &mut out);
173 out.push(Token {
174 start: start as u32,
175 end: end as u32,
176 kind: classify(raw),
177 });
178 cursor = end;
179 }
180 comments_in(text, cursor, text.len(), &mut out);
181 out
182}
183
184/// The comments in a stretch of source the lexer claimed nothing in.
185fn comments_in(text: &str, from: usize, to: usize, out: &mut Vec<Token>) {
186 let mut at = from;
187 while at < to {
188 let Some(hash) = text[at..to].find('#') else {
189 return;
190 };
191 let start = at + hash;
192 let end = text[start..to].find('\n').map(|i| start + i).unwrap_or(to);
193 out.push(Token {
194 start: start as u32,
195 end: end as u32,
196 // `##` is documentation and `#` is a comment — one more `#`, as `///` is one more `/`.
197 kind: if text[start..].starts_with(beck_syntax::doc::PY_MARKER) {
198 TokenKind::Doc
199 } else {
200 TokenKind::Comment
201 },
202 });
203 at = end + 1;
204 }
205}
206
207fn classify(raw: &Raw) -> TokenKind {
208 match raw {
209 Raw::Ident(word) if KEYWORDS.contains(&word.as_str()) => TokenKind::Keyword,
210 Raw::Ident(_) => TokenKind::Name,
211 Raw::Keyword(_) => TokenKind::Atom,
212 Raw::Int(_) | Raw::Float(_) => TokenKind::Number,
213 Raw::Str(_) => TokenKind::Str,
214 _ => TokenKind::Punct,
215 }
216}
217
218// ---------------------------------------------------------------------------------------------
219// The analysed document
220// ---------------------------------------------------------------------------------------------
221
222/// One name an editor can ask about.
223#[derive(Clone, Debug)]
224pub struct Symbol {
225 /// Where the declaration is **in this document**, or `None` for an imported name — whose
226 /// declaration is in a `.becki` this editor is not showing. A jump-to-definition that landed on
227 /// a byte range of the wrong file is worse than one that declines.
228 pub span: Option<(u32, u32)>,
229 /// The signature as `beck iface` would publish it — [`render_item`], not a second renderer.
230 pub signature: String,
231 /// The ` uses …` clause of that signature, alone — [`render_uses`], the half of it an inlay
232 /// hint offers where the source did not write one. Empty for a name that performs nothing.
233 pub uses: String,
234 pub kind: SymbolKind,
235 pub tier: String,
236 /// The `##` comment attached to the declaration, if it has one.
237 pub doc: Option<String>,
238 /// True for a name this module declares, false for one it imported.
239 pub own: bool,
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub enum SymbolKind {
244 Function,
245 Signal,
246}
247
248impl SymbolKind {
249 /// `12` (Function) or `13` (Variable) in LSP's `SymbolKind`.
250 pub fn lsp_symbol(self) -> u32 {
251 match self {
252 SymbolKind::Function => 12,
253 SymbolKind::Signal => 13,
254 }
255 }
256
257 /// `3` (Function) or `6` (Variable) in LSP's `CompletionItemKind`.
258 pub fn lsp_completion(self) -> u32 {
259 match self {
260 SymbolKind::Function => 3,
261 SymbolKind::Signal => 6,
262 }
263 }
264}
265
266/// One thing an editor can offer to finish the word being typed.
267#[derive(Clone, Debug, PartialEq, Eq)]
268pub struct Completion {
269 pub label: String,
270 /// The signature, for a name; empty for a keyword.
271 pub detail: String,
272 pub kind: CompletionKind,
273 pub doc: Option<String>,
274}
275
276#[derive(Clone, Copy, Debug, PartialEq, Eq)]
277pub enum CompletionKind {
278 Keyword,
279 Function,
280 Signal,
281}
282
283impl CompletionKind {
284 /// LSP's `CompletionItemKind`: `14` is Keyword.
285 pub fn lsp(self) -> u32 {
286 match self {
287 CompletionKind::Keyword => 14,
288 CompletionKind::Function => SymbolKind::Function.lsp_completion(),
289 CompletionKind::Signal => SymbolKind::Signal.lsp_completion(),
290 }
291 }
292}
293
294/// One diagnostic, as an editor draws it: a byte range, a code and the text a terminal would print.
295#[derive(Clone, Debug, PartialEq, Eq)]
296pub struct Mark {
297 pub start: u32,
298 pub end: u32,
299 pub error: bool,
300 pub code: String,
301 /// The message, the notes and the fix — everything the terminal renderer would have put under
302 /// the span. An editor that showed only the first line would be dropping the suggestion §3.4
303 /// insists a diagnostic carries.
304 pub message: String,
305}
306
307/// The names an analysis found, kept without the analysis.
308///
309/// Strings and spans, so a server can hold one per open document. See
310/// [`Editor::completing_from`] for what it is for.
311#[derive(Clone, Debug, Default)]
312pub struct Index {
313 names: BTreeMap<String, Symbol>,
314}
315
316impl Index {
317 pub fn is_empty(&self) -> bool {
318 self.names.is_empty()
319 }
320}
321
322/// One document, analysed: what the front end made of the text the editor last sent.
323pub struct Editor {
324 /// What the document is called, so a rename can re-analyse the text it proposes to write.
325 name: String,
326 text: String,
327 /// Which file in [`Editor::map`] this document *is*.
328 ///
329 /// An [`Editor`] holds a whole linked project — the standard library included — so a span it
330 /// can reach is not necessarily a span in this buffer, and two files' byte offsets overlap by
331 /// construction. Every answer that turns a span into a range in *this* document checks it
332 /// against this first. `None` for a document that did not get as far as being read, where
333 /// there is nothing to answer anyway.
334 file: Option<beck_diag::FileId>,
335 placed: Option<Placed>,
336 diagnostics: Diagnostics,
337 map: SourceMap,
338 /// Every name an editor can ask about, own and imported.
339 ///
340 /// A `BTreeMap` so that document symbols come out in a stable order whatever order the checker
341 /// resolved them in — the same reason [`crate::iface::Interface`] keeps its types in
342 /// declaration order.
343 names: BTreeMap<String, Symbol>,
344 /// True when [`Editor::completing_from`] supplied the names, because this text does not check.
345 stale: bool,
346}
347
348impl Editor {
349 /// Parse, expand, check, place and secure one document, and index what an editor can ask about.
350 ///
351 /// A **project** rather than a lone module, with the standard library as the only other place
352 /// modules come from: a file being edited may `import bignum`, and an editor that answered
353 /// "cannot find `add_big`" for every name in it would be answering about a program the compiler
354 /// does not have. There is no directory here — a browser tab has none, and a language server
355 /// resolving a relative path off a URI is a decision [`docs/65`](../../../../../docs/65-the-editor-report.md)
356 /// did not take — so the loader serves this text as the root and nothing else, and
357 /// [`crate::stdlib`] answers the rest.
358 ///
359 /// A library is analysed as a library, not refused: most files being edited are, and
360 /// [`crate::project::slice_or_library`] is the same entry `beck check` uses to say so.
361 pub fn of(name: &str, text: &str) -> Editor {
362 let mut map = SourceMap::new();
363 let mut diagnostics = Diagnostics::new();
364 // The module name a file has, which is what an `import` names it by.
365 let root = name
366 .rsplit(['/', '\\'])
367 .next()
368 .unwrap_or(name)
369 .split('.')
370 .next()
371 .unwrap_or(name)
372 .to_string();
373 let text_of_root = text.to_string();
374 let display = name.to_string();
375 let loader = |want: &str| {
376 (want == root).then(|| crate::project::Sources {
377 module: Some(text_of_root.clone()),
378 interface: None,
379 path: Some(display.clone()),
380 })
381 };
382 let project =
383 crate::project::check_project(&root, &loader, None, &mut map, &mut diagnostics);
384 // The root module's own contract, kept before the project is sliced: it is what separates
385 // the names this file declares from the ones it imported, and after linking the program is
386 // one namespace in which that difference is no longer visible.
387 let published: Vec<String> = project
388 .as_ref()
389 .map(|p| {
390 p.interface
391 .items
392 .iter()
393 .map(|i| i.name.to_string())
394 .collect()
395 })
396 .unwrap_or_default();
397 let placed = project
398 .and_then(|p| crate::project::slice_or_library(p, &mut diagnostics))
399 .filter(|_| !diagnostics.has_errors());
400
401 let mut names = BTreeMap::new();
402 if let Some(placed) = placed.as_ref() {
403 let program = &placed.program;
404 let symbol = |item: &crate::iface::Item, span: Option<(u32, u32)>, own: bool| Symbol {
405 span,
406 signature: render_item(item).trim_end().to_string(),
407 uses: render_uses(&item.effects),
408 kind: match item.kind {
409 Kind::Signal { .. } => SymbolKind::Signal,
410 Kind::Function { .. } => SymbolKind::Function,
411 },
412 tier: item.tier.name().to_string(),
413 doc: program.docs.get(&*item.name).map(|d| d.to_string()),
414 own,
415 };
416
417 // Every name the linked program has, described by the signature `beck iface` would
418 // publish for it — so an editor cannot show a signature the compiler would not. The
419 // ones this file *declares* carry the span the checker recorded; an imported name's
420 // declaration is in another module, and a jump that landed on a byte range of this
421 // document would point at the wrong file.
422 for item in &crate::iface::Interface::of(program).items {
423 let own = published.iter().any(|n| *n == *item.name);
424 let span = own
425 .then(|| {
426 program
427 .defs
428 .get(&item.name)
429 .map(|d| (d.span.start, d.span.end))
430 .or_else(|| {
431 program
432 .signals
433 .iter()
434 .find(|s| s.name == item.name)
435 .map(|s| (s.span.start, s.span.end))
436 })
437 })
438 .flatten();
439 names.insert(item.name.to_string(), symbol(item, span, own));
440 }
441 }
442
443 // The root module is added to the map before anything it imports, so the first file under
444 // this name is this document even when a library module happens to share it.
445 let file = map.find(name);
446 Editor {
447 name: name.to_string(),
448 text: text.to_string(),
449 file,
450 placed,
451 diagnostics,
452 map,
453 names,
454 stale: false,
455 }
456 }
457
458 /// The name table alone, for a caller that wants to keep it and not the analysis.
459 ///
460 /// An [`Editor`] holds a checked program; an [`Index`] holds strings. A server keeping one per
461 /// open document keeps this one.
462 pub fn index(&self) -> Index {
463 Index {
464 names: self.names.clone(),
465 }
466 }
467
468 /// Borrow the previous analysis's names, for text that does not check.
469 ///
470 /// A half-typed name is an unresolved name, so the most common state of a file being written
471 /// in is the state that has no program and therefore no name table. An editor that answered
472 /// nothing there would answer nothing exactly when it was being asked, so a consumer keeps its
473 /// last analysis and hands it here.
474 ///
475 /// What this is **not** is a stale answer presented as a current one: the names are marked
476 /// [`stale`](Editor::stale), the diagnostics are always this text's, and nothing else is
477 /// carried over. [`docs/98`](../../../../../docs/98-playground-report.md) §98.1's rule — a
478 /// stale table beside a red error teaches somebody something false — is about *derived
479 /// answers* like a placement table, and this is a completion list; the difference is that the
480 /// consumer is told.
481 pub fn completing_from(mut self, previous: &Index) -> Editor {
482 if self.names.is_empty() && !previous.names.is_empty() {
483 self.names.clone_from(&previous.names);
484 self.stale = true;
485 }
486 self
487 }
488
489 /// True when the names came from an earlier text than the one being shown.
490 pub fn stale(&self) -> bool {
491 self.stale
492 }
493
494 /// The checked program, for a caller that wants more than an editor's questions.
495 pub fn placed(&self) -> Option<&Placed> {
496 self.placed.as_ref()
497 }
498
499 pub fn diagnostics(&self) -> &Diagnostics {
500 &self.diagnostics
501 }
502
503 pub fn source_map(&self) -> &SourceMap {
504 &self.map
505 }
506
507 /// Every diagnostic, as a range and the text a terminal would print.
508 pub fn marks(&self) -> Vec<Mark> {
509 marks(&self.diagnostics)
510 }
511
512 /// The names this document declares, in name order — `documentSymbol`'s answer.
513 ///
514 /// Imported names are excluded: they are not symbols *of* this file, and an outline listing
515 /// them would list the standard library under every module that used it.
516 pub fn symbols(&self) -> impl Iterator<Item = (&str, &Symbol)> {
517 self.names
518 .iter()
519 .filter(|(_, s)| s.own)
520 .map(|(n, s)| (n.as_str(), s))
521 }
522
523 pub fn symbol(&self, name: &str) -> Option<&Symbol> {
524 self.names.get(name)
525 }
526
527 /// What to say about the name under the caret.
528 pub fn hover(&self, offset: u32) -> Option<&Symbol> {
529 self.names.get(&word_at(&self.text, offset)?)
530 }
531
532 /// What to say about the **class** under the caret, when the caret is in a `class=` value.
533 ///
534 /// [`docs/104`](../../../../../docs/104-styling-and-the-component-library.md) §104.4: "hover
535 /// print the declarations […] that is the Tailwind IntelliSense extension, without an
536 /// extension, because the answers come from the compiler." A class is not a name this document
537 /// declares, so it is not in [`Editor::hover`]'s index and never could be — what it is, is a
538 /// token inside a string, answered from the same table `beck build` emits the sheet from.
539 pub fn class_hover(&self, offset: u32) -> Option<String> {
540 let token = class_token_at(&self.text, offset)?;
541 let rule = crate::style::rule(&token)?;
542 let decls = rule
543 .decls
544 .iter()
545 .map(|(p, v)| format!("{p}: {v};"))
546 .collect::<Vec<_>>()
547 .join(" ");
548 Some(match rule.at.is_empty() {
549 true => format!("{} {{ {decls} }}", rule.selector),
550 false => format!(
551 "{} {{ {} {{ {decls} }} }}",
552 rule.at.join(" "),
553 rule.selector
554 ),
555 })
556 }
557
558 /// The utilities that could finish the class being typed, best-first.
559 ///
560 /// Empty when the caret is not inside a `class=` value, which is what keeps four thousand
561 /// utility names out of every other completion in the file.
562 pub fn class_completions(&self, offset: u32) -> Vec<Completion> {
563 let Some(prefix) = class_prefix_at(&self.text, offset) else {
564 return Vec::new();
565 };
566 let (names, variants) = crate::style::enumerate();
567 // A variant is offered only once one has been typed towards, because `hover:` in front of
568 // four thousand names is four thousand more completions and none of them is what somebody
569 // reaching for `flex` wants.
570 let variants = variants
571 .into_iter()
572 .filter(|v: &&str| !prefix.is_empty() && v.starts_with(prefix.as_str()))
573 .map(|v| Completion {
574 label: format!("{v}:"),
575 detail: String::new(),
576 kind: CompletionKind::Keyword,
577 doc: None,
578 });
579 names
580 .into_iter()
581 .filter(|n| n.starts_with(&prefix))
582 .filter_map(|name| {
583 let rule = crate::style::rule(&name)?;
584 Some(Completion {
585 detail: rule
586 .decls
587 .iter()
588 .map(|(p, v)| format!("{p}: {v}"))
589 .collect::<Vec<_>>()
590 .join("; "),
591 label: name,
592 kind: CompletionKind::Function,
593 doc: None,
594 })
595 })
596 .chain(variants)
597 .collect()
598 }
599
600 /// Where the name under the caret is declared, in this document.
601 pub fn definition(&self, offset: u32) -> Option<(u32, u32)> {
602 self.hover(offset)?.span
603 }
604
605 /// What could finish the word being typed, best-first.
606 ///
607 /// Names before keywords, because a name is specific to this program and a keyword is not, and
608 /// within each group the order is the name order the index already keeps. A caret that is not
609 /// in a word offers everything — which is what an editor's "show me what is here" gesture
610 /// asks for.
611 pub fn completions(&self, offset: u32) -> Vec<Completion> {
612 let prefix = prefix_at(&self.text, offset);
613 let mut out: Vec<Completion> = Vec::new();
614 for (name, symbol) in &self.names {
615 if !name.starts_with(&prefix) {
616 continue;
617 }
618 out.push(Completion {
619 label: name.clone(),
620 detail: symbol.signature.clone(),
621 kind: match symbol.kind {
622 SymbolKind::Function => CompletionKind::Function,
623 SymbolKind::Signal => CompletionKind::Signal,
624 },
625 doc: symbol.doc.clone(),
626 });
627 }
628 for keyword in KEYWORDS {
629 if keyword.starts_with(&prefix) {
630 out.push(Completion {
631 label: (*keyword).to_string(),
632 detail: String::new(),
633 kind: CompletionKind::Keyword,
634 doc: None,
635 });
636 }
637 }
638 out
639 }
640
641 /// The word a completion would replace, so a client can send an edit rather than a guess.
642 pub fn prefix(&self, offset: u32) -> String {
643 prefix_at(&self.text, offset)
644 }
645
646 // -----------------------------------------------------------------------------------------
647 // Occurrences, and the rename built on them
648 // -----------------------------------------------------------------------------------------
649
650 /// Every place the name under the caret appears in this document.
651 ///
652 /// Empty rather than partial when the two accounts of the document disagree — see
653 /// [`occurrences`](Editor::occurrences), which is where that rule is.
654 pub fn references(&self, offset: u32) -> Vec<Occurrence> {
655 let Some(name) = word_at(&self.text, offset) else {
656 return Vec::new();
657 };
658 self.occurrences(&name).unwrap_or_default()
659 }
660
661 /// Every place `name` appears, or `None` when this document's two accounts of it disagree.
662 ///
663 /// # The two accounts, and why both
664 ///
665 /// The **lexical** account is the token stream: every run of the text that reads
666 /// `name`, keywords included. It is complete by construction — the lexer saw the whole file —
667 /// and it knows nothing, so a local variable that happens to share the name is in it, and so is
668 /// the `page` in `expect page contains "1"`, which is the grammar's word rather than a
669 /// reference to the signal of that name.
670 ///
671 /// The **semantic** account is the checked program: a [`CoreKind::Global`] node per reference,
672 /// resolved, so a local of the same name is *not* in it and a name reached through an import
673 /// is. It knows everything and is not complete: a reference the checker rewrote — a trait
674 /// method resolved to an impl, a macro's expansion — has a span that is a call site rather
675 /// than an identifier.
676 ///
677 /// What makes an *edit* safe is the two agreeing: every semantic reference begins on a lexical
678 /// identifier that reads `name`, and the only lexical identifier left over is the
679 /// declaration's own. A file where that holds has no shadow, no unspanned mention and no
680 /// rewritten reference, and the lexical ranges are then the whole truth about where the name
681 /// is — which is also why the *edits* are the lexical ranges and never the spans. Where
682 /// it does not hold this answers `None`, and both callers decline rather than edit — because
683 /// the alternative to declining is a rename that silently changes what a program means, which
684 /// is worse than a rename that does not happen.
685 pub fn occurrences(&self, name: &str) -> Option<Vec<Occurrence>> {
686 let placed = self.placed.as_ref()?;
687 if self.stale {
688 return None;
689 }
690 let symbol = self.names.get(name)?;
691
692 let lexical = self.written(name);
693 if lexical.is_empty() {
694 return None;
695 }
696
697 let mut semantic: Vec<u32> = Vec::new();
698 for core in self.own_expressions(placed) {
699 self.globals_in(core, name, &mut semantic);
700 }
701 semantic.extend(self.static_mentions(placed, name));
702 semantic.sort_unstable();
703 semantic.dedup();
704 // Both sides are in source order, so every membership question below is a binary search:
705 // a name used a thousand times in a file is not a thousand scans of a thousand tokens.
706 let is_reference = |at: &u32| semantic.binary_search(at).is_ok();
707 if !semantic
708 .iter()
709 .all(|at| lexical.binary_search_by_key(at, |(s, _)| *s).is_ok())
710 {
711 return None;
712 }
713
714 // A `test` block's clauses are a grammar of their own — `expect page contains "1"`,
715 // `when session("ana") sends …`, `expect state == fold_of […]` — and its words are
716 // identifiers to the lexer. `page` there does not name the `page` signal: the runner finds
717 // the page by its *type*, so renaming the signal leaves the clause saying what it said.
718 // Nothing inside a clause is edited on the strength of a lexical match, and nothing inside
719 // one refuses the rename either; the clause's actual expressions are in the semantic
720 // account like any others, and get edited from there.
721 let grammar: Vec<(u32, u32)> = placed
722 .program
723 .tests
724 .iter()
725 .filter(|t| self.owns(t.span))
726 .flat_map(|t| t.clause_spans())
727 .filter(|s| self.owns(*s))
728 .map(|s| (s.start, s.end))
729 .collect();
730 let left: Vec<(u32, u32)> = lexical
731 .iter()
732 .copied()
733 .filter(|(s, _)| !is_reference(s))
734 .filter(|(s, e)| !grammar.iter().any(|(from, to)| s >= from && e <= to))
735 .collect();
736 // A name this document declares is written once more than it is referred to, and that once
737 // is inside its own declaration. An imported name is not written here at all.
738 let declaration = match symbol.span {
739 Some((start, end)) => match left[..] {
740 [only] if only.0 >= start && only.1 <= end => Some(only),
741 _ => return None,
742 },
743 None => {
744 if !left.is_empty() {
745 return None;
746 }
747 None
748 }
749 };
750
751 let mut out: Vec<Occurrence> = lexical
752 .into_iter()
753 .filter(|(start, end)| is_reference(start) || Some((*start, *end)) == declaration)
754 .map(|(start, end)| Occurrence {
755 start,
756 end,
757 declaration: Some((start, end)) == declaration,
758 })
759 .collect();
760 out.sort_unstable_by_key(|o| o.start);
761 Some(out)
762 }
763
764 /// Where a rename would edit, or why it will not.
765 ///
766 /// The edits are [`occurrences`](Editor::occurrences)', and everything else here is a refusal.
767 /// [`docs/03`](../../../../../docs/03-type-and-effect-system.md) §3.4's rule for placement — a
768 /// compile error with a suggested annotation, never a silent guess — is the same rule this
769 /// keeps for an edit: a refusal an author can read beats a rewrite they have to check.
770 ///
771 /// The last check is the expensive and the decisive one: the proposed text is **analysed**,
772 /// and a rename that would not compile is not offered. That costs one more compile of one file
773 /// — [`docs/64`](../../../../../docs/64-compile-speed-report.md) §64.6's 4.7 ms at the worst
774 /// file in this tree — on a keystroke nobody types twice a minute, and it is what turns the
775 /// reasoning above into a fact about the text rather than an argument about the IR.
776 pub fn rename(&self, offset: u32, to: &str) -> Result<Vec<Occurrence>, Refusal> {
777 if self.placed.is_none() || self.stale {
778 return Err(Refusal::Broken);
779 }
780 let name = word_at(&self.text, offset).ok_or(Refusal::NotAName)?;
781 let symbol = self.names.get(&name).ok_or(Refusal::NotAName)?;
782 if !symbol.own || symbol.span.is_none() {
783 return Err(Refusal::Imported(name));
784 }
785 if !is_name(to) {
786 return Err(Refusal::NotAnIdentifier(to.to_string()));
787 }
788 // Anything already written under that name, whether or not the checker resolved it: a
789 // top-level name, a parameter, a binding, a type. `occurrences` would notice the collision
790 // for a global and could not for a local — a body's `let` keeps no name past the checker —
791 // so the question asked here is the lexical one, which needs no resolution to answer.
792 if self.names.contains_key(to) || !self.written(to).is_empty() {
793 return Err(Refusal::Taken(to.to_string()));
794 }
795 let edits = self.occurrences(&name).ok_or(Refusal::Unaccounted(name))?;
796
797 let mut proposed = self.text.clone();
798 for edit in edits.iter().rev() {
799 proposed.replace_range(edit.start as usize..edit.end as usize, to);
800 }
801 let after = Editor::of(&self.name, &proposed);
802 if let Some(broken) = after
803 .diagnostics()
804 .iter()
805 .find(|d| d.severity == Severity::Error)
806 {
807 return Err(Refusal::WouldNotCompile {
808 code: broken.code.to_string(),
809 message: broken.message.clone(),
810 });
811 }
812 // Compiling is not enough on its own. A module with no merge point is a *library* rather
813 // than an error ([`crate::project::slice_or_library`]), so a rename that cost a program its
814 // page or its fold would pass the check above while quietly demoting an application to a
815 // module that no longer runs.
816 let kind = |e: &Editor| e.placed().map(|p| p.is_application());
817 if kind(&after) != kind(self) {
818 return Err(Refusal::WouldStopBeingAnApplication);
819 }
820 Ok(edits)
821 }
822
823 // -----------------------------------------------------------------------------------------
824 // Inlay hints
825 // -----------------------------------------------------------------------------------------
826
827 /// What the compiler worked out that the source does not say, where it could be written down.
828 ///
829 /// The two inferred halves of a Beck signature, and only those: **where a definition runs**,
830 /// which §3.4 makes a solved constraint rather than an annotation, and **what it performs**,
831 /// which §3.6 makes an inferred row a boundary later has to declare. A name whose source
832 /// already carries the annotation gets no hint for it — an inlay hint repeating what is on the
833 /// line beside it is noise, and the point of these is that they are the part nobody wrote.
834 ///
835 /// Every label is what an author could paste in at the offset it carries, which is why the
836 /// effect hint is rendered by [`render_uses`] and the tier hint reads `@on(...)`: a hint you
837 /// can accept is worth more than a hint you have to translate.
838 pub fn hints(&self) -> Vec<Hint> {
839 let Some(placed) = self.placed.as_ref() else {
840 return Vec::new();
841 };
842 let program = &placed.program;
843 let lexed = tokens(&self.text);
844 let mut out: Vec<Hint> = Vec::new();
845 for name in &program.def_order {
846 let Some(def) = program.defs.get(name) else {
847 continue;
848 };
849 // A `.becki` line has no body to place and no row to infer: it is the declaration.
850 if def.is_declaration || !self.owns(def.span) {
851 continue;
852 }
853 if let Some(hint) = self.tier_hint(def.tier, def.tier_is_written, def.span) {
854 out.push(hint);
855 }
856 if !def.row_is_declared {
857 let uses = self
858 .names
859 .get(&**name)
860 .map(|s| s.uses.clone())
861 .unwrap_or_default();
862 if let (false, Some(offset)) = (uses.is_empty(), self.signature_end(def, &lexed)) {
863 out.push(Hint {
864 offset,
865 label: uses,
866 kind: HintKind::Effects,
867 });
868 }
869 }
870 }
871 for signal in &program.signals {
872 if let Some(hint) = self.tier_hint(signal.tier, signal.tier_is_written, signal.span) {
873 out.push(hint);
874 }
875 }
876 out.sort_by_key(|h| h.offset);
877 out
878 }
879
880 /// The `@on(...)` a declaration did not write, where there is one to show.
881 ///
882 /// [`Tier::Any`] is not one. It is what §3.3 calls *unplaced* — pure code, compiled to every
883 /// tier that needs it — so it is the absence of a placement rather than a placement, and a
884 /// library whose every helper carried `@on(any)` would be a file of hints saying nothing. What
885 /// this shows is the answer to "where does this end up", asked where that has an answer.
886 fn tier_hint(&self, tier: Tier, written: bool, span: beck_diag::Span) -> Option<Hint> {
887 (!written && tier != Tier::Any && self.owns(span)).then(|| Hint {
888 offset: span.start,
889 label: format!("@on({})", tier.name()),
890 kind: HintKind::Tier,
891 })
892 }
893
894 /// The colon that ends a definition's signature — where a `uses` clause would be written.
895 ///
896 /// A signature contains colons of its own, one per parameter, so it is the first colon at
897 /// **bracket depth zero** rather than the first colon: `def f(x: Int) -> Int:` has two, and an
898 /// offset at the earlier one would put the clause in the middle of the parameter list. Over
899 /// the token stream rather than over the text, so a `:` inside a string or a comment is not
900 /// one — the same reason [`tokens`] exists at all.
901 ///
902 /// Not found from the body's span, which is where this was first written and wrong: a body's
903 /// first expression starts *after* the `return` that introduces it, so the text between the
904 /// colon and the span is a keyword rather than whitespace and there is nothing there to
905 /// recognise the colon by.
906 ///
907 /// The tokens are in source order, so the declaration's own are found by binary search and the
908 /// scan stops at the colon. Filtering the whole stream per definition would have made hinting a
909 /// file cost `definitions × tokens`, which is the quadratic this codebase keeps finding in
910 /// exactly this shape ([`docs/64`](../../../../../docs/64-compile-speed-report.md) §64.2).
911 fn signature_end(&self, def: &Def, tokens: &[Token]) -> Option<u32> {
912 let from = tokens.partition_point(|t| t.start < def.span.start);
913 let mut depth = 0i32;
914 for token in tokens[from..].iter().take_while(|t| t.end <= def.span.end) {
915 if token.kind != TokenKind::Punct {
916 continue;
917 }
918 match self.text.get(token.start as usize..token.end as usize)? {
919 "(" | "[" | "{" => depth += 1,
920 ")" | "]" | "}" => depth -= 1,
921 ":" if depth == 0 => return Some(token.start),
922 _ => {}
923 }
924 }
925 None
926 }
927
928 /// Every run of this document that reads as the word `name`, in source order.
929 ///
930 /// A **keyword** counts, and that is not an oversight: `page`, `state`, `events` and `session`
931 /// are words the parser reads as syntax inside a `test` block and perfectly ordinary names for
932 /// a signal outside one — `page: Signal[Html] = per_session(count, view)` is in nearly every
933 /// program in [`corpus/`](../../../../corpus). Reading only [`TokenKind::Name`] meant the most
934 /// common name in the language had *no* occurrences at all and every question about it was
935 /// declined. What separates the two uses is not the token, it is where it sits: the grammar's
936 /// own words are inside a clause, and [`occurrences`](Editor::occurrences) drops those.
937 fn written(&self, name: &str) -> Vec<(u32, u32)> {
938 tokens(&self.text)
939 .into_iter()
940 .filter(|t| matches!(t.kind, TokenKind::Name | TokenKind::Keyword))
941 .map(|t| (t.start, t.end))
942 .filter(|(s, e)| self.text.get(*s as usize..*e as usize) == Some(name))
943 .collect()
944 }
945
946 /// True for a span that is a range of *this* document.
947 fn owns(&self, span: beck_diag::Span) -> bool {
948 !span.is_none() && Some(span.file) == self.file
949 }
950
951 /// Every expression this document wrote: the bodies it declares, its signals, and its tests.
952 ///
953 /// Tests are in the list because a name used only by a `test` block is used — this is the
954 /// walk [`docs/70`](../../../../../docs/70-the-evaluator-gets-fast-report.md) found three
955 /// passes had been missing — and a rename blind to them would edit a program into one that no
956 /// longer compiles.
957 fn own_expressions<'a>(&'a self, placed: &'a Placed) -> Vec<&'a Core> {
958 let program = &placed.program;
959 let mut out: Vec<&Core> = Vec::new();
960 for def in program.defs.values() {
961 if self.owns(def.span) {
962 out.push(&def.body);
963 }
964 }
965 for signal in &program.signals {
966 if self.owns(signal.span) {
967 out.push(&signal.expr);
968 }
969 }
970 for test in &program.tests {
971 if self.owns(test.span) {
972 out.extend(test.cores());
973 }
974 }
975 out
976 }
977
978 /// Where a static expectation names `name` — `expect place(page) == client`.
979 ///
980 /// Not in [`own_expressions`](Editor::own_expressions), because it is not an expression:
981 /// [`docs/21`](../../../../../docs/21-tests-in-beck-and-proof.md) §21.2's static assertions are
982 /// answered from the placement table without running anything, so the name in one is a
983 /// reference the checker resolves and keeps no [`Core`] node for. It is still a use of the
984 /// name, and it was the one thing in the corpus that a rename could not account for: `page` is
985 /// the name most Beck programs assert about, and 48 of them declined until this was here
986 /// ([`docs/65`](../../../../../docs/65-the-editor-report.md) §65.6).
987 fn static_mentions(&self, placed: &Placed, name: &str) -> Vec<u32> {
988 let mut out = Vec::new();
989 for test in &placed.program.tests {
990 if !self.owns(test.span) {
991 continue;
992 }
993 for clause in &test.clauses {
994 if let crate::testing::Clause::Expect {
995 what:
996 crate::testing::Expectation::Place {
997 what, what_span, ..
998 },
999 ..
1000 } = clause
1001 {
1002 if &**what == name && self.owns(*what_span) {
1003 out.push(what_span.start);
1004 }
1005 }
1006 }
1007 }
1008 out
1009 }
1010
1011 /// Where each reference to `name` in an expression tree **begins**.
1012 ///
1013 /// The start rather than the range, because a reference that is called carries the span of the
1014 /// *call*: `double(x)` is one node spanning the parentheses and their contents, and the name
1015 /// is its first token. Which token that is, is the lexical account's question — this one only
1016 /// says that the checker resolved a reference to `name` starting there.
1017 fn globals_in(&self, core: &Core, name: &str, out: &mut Vec<u32>) {
1018 if let CoreKind::Global(global) = &core.kind {
1019 if &**global == name && self.owns(core.span) {
1020 out.push(core.span.start);
1021 }
1022 }
1023 for child in crate::core::children(core) {
1024 self.globals_in(child, name, out);
1025 }
1026 }
1027}
1028
1029/// One place a name appears in the document.
1030#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1031pub struct Occurrence {
1032 pub start: u32,
1033 pub end: u32,
1034 /// True for the one that declares it, so a client can mark it as the write among the reads.
1035 pub declaration: bool,
1036}
1037
1038/// Why a rename will not happen.
1039///
1040/// A variant per reason rather than one string, because the caller renders them: a language server
1041/// puts them in an error response and a browser tab puts them beside the box somebody typed in.
1042#[derive(Clone, Debug, PartialEq, Eq)]
1043pub enum Refusal {
1044 /// The document does not currently compile, so there is no program to rename in.
1045 Broken,
1046 /// There is no name under the caret.
1047 NotAName,
1048 /// The name is declared in another module, and this editor is not showing that file.
1049 Imported(String),
1050 /// The new name is not one the lexer would read as an identifier.
1051 NotAnIdentifier(String),
1052 /// Something in this document is already written under the new name.
1053 Taken(String),
1054 /// The name is used in a way this document's analysis cannot account for — see
1055 /// [`Editor::occurrences`].
1056 Unaccounted(String),
1057 /// The edit was made and the result does not compile.
1058 WouldNotCompile { code: String, message: String },
1059 /// The edit was made, the result compiles, and it is no longer an application.
1060 WouldStopBeingAnApplication,
1061}
1062
1063impl Refusal {
1064 /// The sentence a person reads, in the terms they typed in.
1065 pub fn message(&self) -> String {
1066 match self {
1067 Refusal::Broken => {
1068 "this file does not compile, so there is nothing to rename in it yet".to_string()
1069 }
1070 Refusal::NotAName => "there is no name under the cursor".to_string(),
1071 Refusal::Imported(name) => {
1072 format!("`{name}` is declared in another module, which this file cannot edit")
1073 }
1074 Refusal::NotAnIdentifier(to) => format!("`{to}` is not a name Beck can read"),
1075 Refusal::Taken(to) => format!("`{to}` is already used in this file"),
1076 Refusal::Unaccounted(name) => format!(
1077 "`{name}` is used somewhere this rename cannot account for — a local of the same \
1078 name, or a mention the checker keeps no position for, such as `expect place({name})`"
1079 ),
1080 Refusal::WouldNotCompile { code, message } => {
1081 format!("the renamed file would not compile: {code}: {message}")
1082 }
1083 Refusal::WouldStopBeingAnApplication => {
1084 "the renamed file would still compile, as a library rather than as the \
1085 application it is now"
1086 .to_string()
1087 }
1088 }
1089 }
1090}
1091
1092/// One thing the compiler worked out and the source does not say.
1093#[derive(Clone, Debug, PartialEq, Eq)]
1094pub struct Hint {
1095 /// Where it belongs, as a byte offset into the document.
1096 pub offset: u32,
1097 /// The label, which is also what could be written at `offset` to say the same thing.
1098 pub label: String,
1099 pub kind: HintKind,
1100}
1101
1102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1103pub enum HintKind {
1104 /// `@on(server)` — where the solver put this definition (§3.4).
1105 Tier,
1106 /// ` uses net.out(…)` — the row the checker inferred for it (§3.6).
1107 Effects,
1108}
1109
1110/// True for a word the lexer would read as one identifier.
1111///
1112/// The lexer rather than a rule written here, because "what is a name" already has an answer with
1113/// a Unicode profile behind it ([`beck_syntax::security`], `docs/44` §44.5), and a rename that
1114/// accepted a name the compiler would refuse — a confusable, a bidirectional control, a keyword —
1115/// would be a second definition of an identifier in a project that has spent a report on having
1116/// one.
1117fn is_name(text: &str) -> bool {
1118 if text.is_empty() || KEYWORDS.contains(&text) {
1119 return false;
1120 }
1121 let mut map = SourceMap::new();
1122 let file = map.add("rename.beck", text);
1123 let mut diags = Diagnostics::new();
1124 let lexed = lex(file, text, &mut diags);
1125 if diags.has_errors() {
1126 return false;
1127 }
1128 let mut words = lexed.iter().filter_map(|t| t.raw());
1129 matches!(
1130 (words.next(), words.next()),
1131 (Some(Raw::Ident(word)), None) if word.as_str() == text
1132 )
1133}
1134
1135/// Diagnostics as an editor draws them, for a caller that has the diagnostics and not an
1136/// [`Editor`] — the playground's analysis has already compiled the text and is not going to do it
1137/// twice.
1138///
1139/// A zero-width span is widened to one character: a caret with nothing under it is a squiggle
1140/// nobody can see, and "the compiler pointed at the end of the file" is a thing that happens.
1141pub fn marks(diagnostics: &Diagnostics) -> Vec<Mark> {
1142 diagnostics
1143 .iter()
1144 .map(|d| Mark {
1145 start: d.primary.start,
1146 end: d.primary.end.max(d.primary.start + 1),
1147 error: d.severity == Severity::Error,
1148 code: d.code.to_string(),
1149 message: message_of(d),
1150 })
1151 .collect()
1152}
1153
1154/// The message an editor shows, with the notes the terminal renderer would have printed.
1155///
1156/// A `B0350` that says only "cannot find `foo`" is a worse diagnostic in an editor than in a
1157/// terminal, because the editor drops everything the terminal put underneath it. The notes carry
1158/// the fix suggestion §3.4 insists on, so they travel.
1159pub fn message_of(d: &Diagnostic) -> String {
1160 let mut out = d.message.clone();
1161 for note in &d.notes {
1162 out.push_str("\n\nnote: ");
1163 out.push_str(note);
1164 }
1165 if let Some(fix) = &d.fix {
1166 out.push_str("\n\nhelp: ");
1167 out.push_str(fix);
1168 }
1169 out
1170}
1171
1172// ---------------------------------------------------------------------------------------------
1173// Positions
1174// ---------------------------------------------------------------------------------------------
1175
1176/// A byte offset as a zero-based line and **UTF-16** character offset.
1177///
1178/// UTF-16 because that is what LSP specifies by default, and getting it wrong is invisible until
1179/// somebody writes an emoji in a string literal — which `beck-syntax`'s own security tests say they
1180/// will. `SourceMap::line_col` counts *characters* and is one-based, so it is the wrong unit twice
1181/// over and is deliberately not used here.
1182pub fn utf16_position(text: &str, offset: u32) -> (u32, u32) {
1183 let offset = (offset as usize).min(text.len());
1184 let mut line = 0u32;
1185 let mut character = 0u32;
1186 for (i, c) in text.char_indices() {
1187 if i >= offset {
1188 break;
1189 }
1190 if c == '\n' {
1191 line += 1;
1192 character = 0;
1193 } else {
1194 character += c.len_utf16() as u32;
1195 }
1196 }
1197 (line, character)
1198}
1199
1200/// A byte offset as a **UTF-16** offset from the start of the text.
1201///
1202/// The flat version of [`utf16_position`], for an editor that works in offsets rather than in
1203/// lines: a browser's `<textarea>` counts its value in UTF-16 code units, so a span the compiler
1204/// gave in bytes lands in the wrong place the first time somebody writes an emoji in a string.
1205/// Neither side guesses — the module that has the text does the conversion.
1206pub fn utf16_offset(text: &str, byte: u32) -> u32 {
1207 let byte = (byte as usize).min(text.len());
1208 text[..byte].chars().map(|c| c.len_utf16() as u32).sum()
1209}
1210
1211/// The inverse: a UTF-16 offset back to a byte offset.
1212pub fn byte_of_utf16(text: &str, utf16: u32) -> u32 {
1213 let mut counted = 0u32;
1214 for (i, c) in text.char_indices() {
1215 if counted >= utf16 {
1216 return i as u32;
1217 }
1218 counted += c.len_utf16() as u32;
1219 }
1220 text.len() as u32
1221}
1222
1223/// The inverse of [`utf16_position`]: a line and UTF-16 character back to a byte offset.
1224pub fn byte_offset(text: &str, line: u32, character: u32) -> Option<u32> {
1225 let mut at_line = 0u32;
1226 let mut utf16 = 0u32;
1227 for (i, c) in text.char_indices() {
1228 if at_line == line && utf16 == character {
1229 return Some(i as u32);
1230 }
1231 if c == '\n' {
1232 if at_line == line {
1233 // The position is past the end of its line, which a client may legitimately send
1234 // when the cursor sits after the last character.
1235 return Some(i as u32);
1236 }
1237 at_line += 1;
1238 utf16 = 0;
1239 } else if at_line == line {
1240 utf16 += c.len_utf16() as u32;
1241 }
1242 }
1243 (at_line == line).then_some(text.len() as u32)
1244}
1245
1246/// The identifier the cursor is inside or immediately after.
1247///
1248/// "Immediately after" matters: an editor sends the position of the caret, and a caret at the end
1249/// of `total` is one byte past the `l`. A server that only looked at the byte under the caret would
1250/// answer nothing for the most common way of asking.
1251pub fn word_at(text: &str, offset: u32) -> Option<String> {
1252 let is_word = |c: char| c.is_alphanumeric() || c == '_';
1253 let bytes = text.as_bytes();
1254 let mut at = (offset as usize).min(bytes.len());
1255 if at > 0 && (at == bytes.len() || !is_word(text[at..].chars().next()?)) {
1256 at -= 1;
1257 }
1258 if !text.is_char_boundary(at) || !is_word(text[at..].chars().next()?) {
1259 return None;
1260 }
1261 let mut start = at;
1262 while start > 0 {
1263 let prev = text[..start].char_indices().next_back()?;
1264 if !is_word(prev.1) {
1265 break;
1266 }
1267 start = prev.0;
1268 }
1269 let end = text[at..]
1270 .char_indices()
1271 .find(|(_, c)| !is_word(*c))
1272 .map(|(i, _)| at + i)
1273 .unwrap_or(text.len());
1274 Some(text[start..end].to_string())
1275}
1276
1277/// The word characters immediately *before* the caret — what a completion is filtering on.
1278///
1279/// Not [`word_at`]: a caret in the middle of `to|tal` completes on `to`, because the rest of the
1280/// word is what the person is about to replace, and offering only names that start with `total`
1281/// would answer a question they have not finished asking.
1282/// Whether the caret is inside the value of a `class=` attribute, and what has been typed of the
1283/// token it is in.
1284///
1285/// Text rather than tree, and deliberately: a class being *typed* has not parsed yet, so there is
1286/// no node to ask. The rule is the one a reader would apply — find the string the caret is in, then
1287/// look left past the rest of the list for `class=`.
1288///
1289/// `None` when the caret is not in such a string, which is what keeps the utility table out of
1290/// every other completion in the file.
1291fn class_prefix_at(text: &str, offset: u32) -> Option<String> {
1292 let mut at = (offset as usize).min(text.len());
1293 while !text.is_char_boundary(at) {
1294 at -= 1;
1295 }
1296 let line_start = text[..at].rfind('\n').map_or(0, |i| i + 1);
1297 let before = &text[line_start..at];
1298 // Inside a string exactly when an odd number of quotes precede the caret on its line. A quote
1299 // inside a string is not a case this has to handle: `class=` values are class names.
1300 if before.matches('"').count().is_multiple_of(2) {
1301 return None;
1302 }
1303 let quote = before.rfind('"')?;
1304 // Everything between `class=` and that quote has to be the value so far — nothing, or the
1305 // opening of a list and the strings already in it. Anything else means the value closed and
1306 // something took its place: `class="flex", placeholder="gap in the diary"` has `class=` to its
1307 // left and a caret inside a string, and is not a class at all.
1308 let head = &before[..quote];
1309 let opened = head.rfind("class=")? + "class=".len();
1310 let between = &head[opened..];
1311 if !between.is_empty() {
1312 let outside: String = between.split('"').step_by(2).collect::<Vec<_>>().join("");
1313 if !between.starts_with('[')
1314 || !outside
1315 .chars()
1316 .all(|c| c == '[' || c == ',' || c.is_whitespace())
1317 {
1318 return None;
1319 }
1320 }
1321 // The token being typed is what follows the last space inside the string: `class="flex it‸"`
1322 // is completing `it` rather than `flex it`.
1323 let typed = &before[quote + 1..];
1324 Some(typed.rsplit(' ').next().unwrap_or(typed).to_string())
1325}
1326
1327/// The whole class token the caret is inside, for hover.
1328fn class_token_at(text: &str, offset: u32) -> Option<String> {
1329 let prefix = class_prefix_at(text, offset)?;
1330 let mut at = (offset as usize).min(text.len());
1331 while !text.is_char_boundary(at) {
1332 at -= 1;
1333 }
1334 let rest = &text[at..];
1335 let end = rest.find(['"', ' ', '\n']).unwrap_or(rest.len());
1336 Some(format!("{prefix}{}", &rest[..end]))
1337}
1338
1339fn prefix_at(text: &str, offset: u32) -> String {
1340 let is_word = |c: char| c.is_alphanumeric() || c == '_';
1341 let mut end = (offset as usize).min(text.len());
1342 while !text.is_char_boundary(end) {
1343 end -= 1;
1344 }
1345 let mut start = end;
1346 while start > 0 {
1347 let Some(prev) = text[..start].char_indices().next_back() else {
1348 break;
1349 };
1350 if !is_word(prev.1) {
1351 break;
1352 }
1353 start = prev.0;
1354 }
1355 text[start..end].to_string()
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360 use super::*;
1361
1362 #[test]
1363 fn positions_are_utf16_and_round_trip() {
1364 // The byte, character and UTF-16 counts all differ on this line, which is the only way to
1365 // tell a correct implementation from one that happens to agree on ASCII.
1366 let text = "def f() -> Str:\n return \"🎈 x\"\n";
1367 let balloon = text.find('🎈').expect("the emoji is there") as u32;
1368 assert_eq!(utf16_position(text, balloon), (1, 12));
1369 assert_eq!(byte_offset(text, 1, 12), Some(balloon));
1370 // One position past the emoji is *two* UTF-16 units later, not one.
1371 assert_eq!(byte_offset(text, 1, 14), Some(balloon + 4));
1372
1373 // And the flat form a `<textarea>` counts in. The balloon is four bytes and two units, so
1374 // the two numbers differ from here on — which is the whole reason this conversion exists.
1375 let flat = utf16_offset(text, balloon);
1376 assert_eq!(flat, text[..balloon as usize].chars().count() as u32);
1377 assert_eq!(byte_of_utf16(text, flat), balloon);
1378 assert_eq!(byte_of_utf16(text, flat + 2), balloon + 4);
1379 assert_eq!(utf16_offset(text, balloon + 4), flat + 2);
1380 }
1381
1382 #[test]
1383 fn a_caret_at_either_end_of_a_name_finds_it() {
1384 let text = "def total(x: Int) -> Int:\n return x\n";
1385 let at = text.find("total").expect("it is there") as u32;
1386 assert_eq!(word_at(text, at).as_deref(), Some("total"));
1387 assert_eq!(word_at(text, at + 2).as_deref(), Some("total"));
1388 // The caret sits *after* the last character, which is where an editor puts it when you
1389 // finish typing a name.
1390 assert_eq!(word_at(text, at + 5).as_deref(), Some("total"));
1391 // The same rule read from the other side: a caret in the space before `total` is a caret
1392 // just past `def`, and answering `def` is what "immediately after" means.
1393 assert_eq!(word_at(text, at - 1).as_deref(), Some("def"));
1394 // Somewhere no identifier touches on either side finds nothing rather than guessing.
1395 let arrow = text.find("-> Int").expect("it is there") as u32;
1396 assert_eq!(word_at(text, arrow + 1), None);
1397 }
1398
1399 #[test]
1400 fn a_prefix_is_what_is_behind_the_caret_and_not_the_whole_word() {
1401 let text = "def total(x: Int) -> Int:\n return tot\n";
1402 let at = text.rfind("tot").expect("it is there") as u32;
1403 assert_eq!(prefix_at(text, at + 3), "tot");
1404 assert_eq!(prefix_at(text, at + 1), "t");
1405 // A caret against whitespace completes on nothing, which is how "show me everything" is
1406 // asked for. `at` itself is that position: the space before `tot`.
1407 assert_eq!(prefix_at(text, at), "");
1408 // And a caret at the end of the word before it completes on *that* word, which is the
1409 // same rule read from the other side.
1410 assert_eq!(prefix_at(text, at - 1), "return");
1411 }
1412
1413 #[test]
1414 fn a_comment_is_coloured_even_though_the_lexer_skips_it() {
1415 let text = "## The answer.\ndef f() -> Int:\n return 1 # a comment\n";
1416 let out = tokens(text);
1417 let kinds: Vec<&str> = out.iter().map(|t| t.kind.name()).collect();
1418 assert_eq!(kinds.first(), Some(&"doc"), "{kinds:?}");
1419 assert!(kinds.contains(&"comment"), "{kinds:?}");
1420 // The `##` line is one token covering exactly that line, and the trailing comment starts
1421 // at the `#` rather than at the space before it.
1422 let doc = out[0];
1423 assert_eq!(
1424 &text[doc.start as usize..doc.end as usize],
1425 "## The answer."
1426 );
1427 let comment = out
1428 .iter()
1429 .find(|t| t.kind == TokenKind::Comment)
1430 .expect("the comment");
1431 assert_eq!(
1432 &text[comment.start as usize..comment.end as usize],
1433 "# a comment"
1434 );
1435 // And `def` is a keyword while `f` is not, which is the whole of what a highlighter is.
1436 let def = out
1437 .iter()
1438 .find(|t| &text[t.start as usize..t.end as usize] == "def");
1439 assert_eq!(def.map(|t| t.kind), Some(TokenKind::Keyword));
1440 let f = out
1441 .iter()
1442 .find(|t| &text[t.start as usize..t.end as usize] == "f");
1443 assert_eq!(f.map(|t| t.kind), Some(TokenKind::Name));
1444 }
1445
1446 #[test]
1447 fn a_hash_inside_a_string_is_not_a_comment() {
1448 // The gap scanner never sees it, because the string is a token and the scanner only reads
1449 // what the lexer left. This is the assertion that says so.
1450 let text = "def f() -> Str:\n return \"# not a comment\"\n";
1451 let out = tokens(text);
1452 assert!(
1453 !out.iter().any(|t| t.kind == TokenKind::Comment),
1454 "{:?}",
1455 out.iter()
1456 .map(|t| (&text[t.start as usize..t.end as usize], t.kind))
1457 .collect::<Vec<_>>()
1458 );
1459 }
1460
1461 #[test]
1462 fn every_token_is_ordered_and_covers_its_own_bytes() {
1463 let text = "## doc\ndef add(a: Int, b: Int) -> Int:\n return a + b # sum\n";
1464 let out = tokens(text);
1465 let mut at = 0u32;
1466 for token in &out {
1467 assert!(token.start >= at, "{out:?}");
1468 assert!(token.end > token.start, "{out:?}");
1469 assert!(token.end as usize <= text.len(), "{out:?}");
1470 at = token.end;
1471 }
1472 }
1473
1474 #[test]
1475 fn hover_and_completion_are_the_signature_beck_iface_publishes() {
1476 let editor = Editor::of(
1477 "t.beck",
1478 "## Adds two numbers.\ndef add(a: Int, b: Int) -> Int:\n return a + b\n",
1479 );
1480 assert!(!editor.diagnostics().has_errors());
1481 let at = "## Adds two numbers.\ndef ad".len() as u32;
1482 let symbol = editor.hover(at).expect("the name under the caret");
1483 assert_eq!(symbol.signature, "def add(a: Int, b: Int) -> Int");
1484 assert_eq!(symbol.doc.as_deref(), Some("Adds two numbers."));
1485 assert!(symbol.own);
1486 assert!(symbol.span.is_some());
1487
1488 // Completion filters on what is behind the caret, and offers the same signature.
1489 let source = "def add(a: Int, b: Int) -> Int:\n return add(a, b)\n";
1490 let editor = Editor::of("t.beck", source);
1491 let caret = source.rfind("add").expect("it is there") as u32 + 2;
1492 let offered = editor.completions(caret);
1493 assert_eq!(
1494 offered
1495 .first()
1496 .map(|c| (c.label.as_str(), c.detail.as_str())),
1497 Some(("add", "def add(a: Int, b: Int) -> Int"))
1498 );
1499 }
1500
1501 /// The state a file is in while somebody types into it: half a name, and no program.
1502 ///
1503 /// Without [`Editor::completing_from`] the answer here is nothing at all, which is the answer
1504 /// an editor gives exactly when it is being asked. The diagnostics stay this text's — only the
1505 /// names are borrowed, and they say so.
1506 #[test]
1507 fn a_half_typed_name_still_completes_from_the_last_analysis() {
1508 let good = Editor::of(
1509 "t.beck",
1510 "def add(a: Int, b: Int) -> Int:\n return a + b\n",
1511 );
1512 let source =
1513 "def add(a: Int, b: Int) -> Int:\n return a + b\ndef g() -> Int:\n return ad\n";
1514 let mid_edit = Editor::of("t.beck", source);
1515 assert!(mid_edit.diagnostics().has_errors());
1516 // Keywords are a property of the language and are offered whatever the text says; a *name*
1517 // is what there is nothing to offer from.
1518 assert!(mid_edit
1519 .completions(source.len() as u32)
1520 .iter()
1521 .all(|c| c.kind == CompletionKind::Keyword));
1522
1523 let mid_edit = mid_edit.completing_from(&good.index());
1524 assert!(mid_edit.stale());
1525 assert!(
1526 mid_edit.diagnostics().has_errors(),
1527 "the errors are this text's"
1528 );
1529 let caret = source.rfind("ad").expect("it is there") as u32 + 2;
1530 assert_eq!(
1531 mid_edit.completions(caret).first().map(|c| c.label.clone()),
1532 Some("add".to_string())
1533 );
1534 }
1535
1536 #[test]
1537 fn an_imported_name_is_offered_and_described_but_not_jumped_to() {
1538 // `word_count` is not declared here, so it has no span in this document — and a jump that
1539 // landed on a byte range of the interface it came from would point at the wrong file.
1540 let source = "import text\n\ndef size(s: Str) -> Int:\n return word_count(s)\n";
1541 let editor = Editor::of("t.beck", source);
1542 assert!(
1543 !editor.diagnostics().has_errors(),
1544 "{}",
1545 editor.diagnostics().render(editor.source_map())
1546 );
1547 let symbol = editor
1548 .symbol("word_count")
1549 .expect("an imported name is indexed");
1550 assert!(!symbol.own);
1551 assert_eq!(symbol.span, None);
1552 assert_eq!(symbol.signature, "def word_count(text: Str) -> Int");
1553 // It is offered as a completion — which is the point of indexing it at all.
1554 let caret = source.rfind("word_c").expect("it is there") as u32 + 6;
1555 assert!(editor
1556 .completions(caret)
1557 .iter()
1558 .any(|c| c.label == "word_count"));
1559 // And it is not in this file's outline.
1560 assert!(editor.symbols().all(|(name, _)| name != "word_count"));
1561 }
1562
1563 #[test]
1564 fn a_mark_carries_the_notes_the_terminal_would_have_printed() {
1565 let editor = Editor::of("x.beck", "def f(x: Int) -> Str:\n return x\n");
1566 let marks = editor.marks();
1567 let mark = marks.first().expect("it does not compile");
1568 assert!(mark.error);
1569 assert!(mark.message.contains("expected"), "{}", mark.message);
1570 assert!(mark.end > mark.start);
1571 }
1572
1573 // ------------------------------------------------------------------------------------------
1574 // Occurrences, rename and hints
1575 // ------------------------------------------------------------------------------------------
1576
1577 const USED_TWICE: &str = "\
1578def double(x: Int) -> Int:
1579 return x * 2
1580
1581def quadruple(x: Int) -> Int:
1582 return double(double(x))
1583";
1584
1585 fn caret(text: &str, at: &str) -> u32 {
1586 text.find(at).expect("it is there") as u32
1587 }
1588
1589 #[test]
1590 fn every_use_of_a_name_is_found_and_the_declaration_is_marked() {
1591 let editor = Editor::of("t.beck", USED_TWICE);
1592 let found = editor.references(caret(USED_TWICE, "double"));
1593 assert_eq!(found.len(), 3, "{found:?}");
1594 // The one inside `def double(…)` is the declaration; the two inside `quadruple` are not.
1595 assert!(found[0].declaration);
1596 assert!(found[1..].iter().all(|o| !o.declaration));
1597 for occurrence in &found {
1598 assert_eq!(
1599 &USED_TWICE[occurrence.start as usize..occurrence.end as usize],
1600 "double"
1601 );
1602 }
1603 // Asked from a use rather than from the declaration, the answer is the same set.
1604 let from_use = editor.references(caret(USED_TWICE, "double(double"));
1605 assert_eq!(from_use, found);
1606 }
1607
1608 #[test]
1609 fn a_rename_edits_every_use_and_the_declaration() {
1610 let editor = Editor::of("t.beck", USED_TWICE);
1611 let edits = editor
1612 .rename(caret(USED_TWICE, "double"), "twice")
1613 .expect("a plain rename");
1614 let mut renamed = USED_TWICE.to_string();
1615 for edit in edits.iter().rev() {
1616 renamed.replace_range(edit.start as usize..edit.end as usize, "twice");
1617 }
1618 assert_eq!(
1619 renamed,
1620 "def twice(x: Int) -> Int:\n return x * 2\n\ndef quadruple(x: Int) -> Int:\n \
1621 return twice(twice(x))\n"
1622 );
1623 // And the thing the edits are *for*: the file still compiles.
1624 let after = Editor::of("t.beck", &renamed);
1625 assert!(
1626 !after.diagnostics().has_errors(),
1627 "{}",
1628 after.diagnostics().render(after.source_map())
1629 );
1630 }
1631
1632 #[test]
1633 fn a_name_used_only_by_a_test_is_still_renamed() {
1634 // The walk `docs/70` found three passes had been missing. A rename that missed it would
1635 // edit every definition and leave the `test` block calling a name that no longer exists —
1636 // and the verification step is what turns that into a refusal rather than a broken file,
1637 // so this asserts the *edit*, which is the outcome the refusal would have hidden.
1638 let source = "\
1639def limit() -> Int:
1640 return 3
1641
1642def under(n: Int) -> Bool:
1643 return n < limit()
1644
1645test \"the limit holds\":
1646 expect under(limit() - 1)
1647";
1648 let editor = Editor::of("t.beck", source);
1649 assert!(
1650 !editor.diagnostics().has_errors(),
1651 "{}",
1652 editor.diagnostics().render(editor.source_map())
1653 );
1654 let edits = editor
1655 .rename(caret(source, "limit"), "ceiling")
1656 .expect("a name a test uses is renameable");
1657 assert_eq!(edits.len(), 3, "{edits:?}");
1658 assert!(
1659 edits.iter().any(|e| e.start > caret(source, "test ")),
1660 "the use inside the test block is edited too: {edits:?}"
1661 );
1662 }
1663
1664 #[test]
1665 fn a_local_of_the_same_name_stops_the_rename_rather_than_capturing_it() {
1666 // The failure this rules out is silent: `total` the parameter and `total` the definition
1667 // are different bindings, the lexer cannot tell them apart, and an edit that renamed both
1668 // would change what the body means while still compiling.
1669 let source = "\
1670def total(x: Int) -> Int:
1671 return x + 1
1672
1673def report(total: Int) -> Int:
1674 return total + 1
1675";
1676 let editor = Editor::of("t.beck", source);
1677 assert!(
1678 !editor.diagnostics().has_errors(),
1679 "{}",
1680 editor.diagnostics().render(editor.source_map())
1681 );
1682 assert_eq!(
1683 editor.rename(caret(source, "total"), "amount"),
1684 Err(Refusal::Unaccounted("total".to_string()))
1685 );
1686 // And references declines for the same reason rather than reporting the shadow as a use.
1687 assert!(editor.references(caret(source, "total")).is_empty());
1688 }
1689
1690 #[test]
1691 fn a_rename_onto_a_name_that_is_taken_is_refused() {
1692 let editor = Editor::of("t.beck", USED_TWICE);
1693 assert_eq!(
1694 editor.rename(caret(USED_TWICE, "double"), "quadruple"),
1695 Err(Refusal::Taken("quadruple".to_string()))
1696 );
1697 // Including a name that is only a *parameter* — which is not in the name table, so the
1698 // check that catches it is the lexical one.
1699 assert_eq!(
1700 editor.rename(caret(USED_TWICE, "double"), "x"),
1701 Err(Refusal::Taken("x".to_string()))
1702 );
1703 }
1704
1705 #[test]
1706 fn a_new_name_is_one_the_lexer_would_read() {
1707 let editor = Editor::of("t.beck", USED_TWICE);
1708 for bad in ["", "2fast", "with space", "def", "a-b", "🎈"] {
1709 assert!(
1710 matches!(
1711 editor.rename(caret(USED_TWICE, "double"), bad),
1712 Err(Refusal::NotAnIdentifier(_))
1713 ),
1714 "`{bad}` is not a name Beck can read"
1715 );
1716 }
1717 assert!(editor
1718 .rename(caret(USED_TWICE, "double"), "twice_over")
1719 .is_ok());
1720 }
1721
1722 #[test]
1723 fn an_imported_name_and_a_broken_file_are_both_refused() {
1724 let source = "import text\n\ndef size(s: Str) -> Int:\n return word_count(s)\n";
1725 let editor = Editor::of("t.beck", source);
1726 assert_eq!(
1727 editor.rename(caret(source, "word_count"), "words"),
1728 Err(Refusal::Imported("word_count".to_string()))
1729 );
1730
1731 let broken = Editor::of("t.beck", "def f(x: Int) -> Str:\n return x\n");
1732 assert_eq!(
1733 broken.rename(caret("def f(", "f"), "g"),
1734 Err(Refusal::Broken)
1735 );
1736 }
1737
1738 #[test]
1739 fn a_hint_is_the_annotation_nobody_wrote() {
1740 let source = "\
1741def key() -> secret[Str]:
1742 return secret_env(\"API_KEY\")
1743
1744@on(server)
1745def other() -> secret[Str]:
1746 return secret_env(\"OTHER_KEY\")
1747
1748def pure(x: Int) -> Int:
1749 return x + 1
1750";
1751 let editor = Editor::of("t.beck", source);
1752 assert!(
1753 !editor.diagnostics().has_errors(),
1754 "{}",
1755 editor.diagnostics().render(editor.source_map())
1756 );
1757 let tiers: Vec<Hint> = editor
1758 .hints()
1759 .into_iter()
1760 .filter(|h| h.kind == HintKind::Tier)
1761 .collect();
1762 // `key` is placed and does not say so. `other` says so. `pure` is unplaced, and "anywhere"
1763 // is not a placement worth writing on the line.
1764 assert_eq!(tiers.len(), 1, "{tiers:?}");
1765 assert_eq!(tiers[0].offset, caret(source, "def key"));
1766 assert_eq!(tiers[0].label, "@on(server)");
1767
1768 // And it is the annotation the source would have carried: written in, it still compiles,
1769 // and it no longer hints.
1770 let mut written = source.to_string();
1771 written.insert_str(tiers[0].offset as usize, "@on(server)\n");
1772 let after = Editor::of("t.beck", &written);
1773 assert!(
1774 !after.diagnostics().has_errors(),
1775 "{written}\n{}",
1776 after.diagnostics().render(after.source_map())
1777 );
1778 assert!(after.hints().iter().all(|h| h.kind != HintKind::Tier));
1779 }
1780
1781 #[test]
1782 fn an_inferred_row_is_hinted_where_the_signature_would_carry_it() {
1783 // Two properties in one, and the second is the interesting one: the offset is the colon
1784 // that ends the signature, so pasting the label in writes a signature that parses.
1785 let source = "\
1786def stamp() -> Int uses nondet:
1787 return now()
1788
1789def later() -> Int:
1790 return stamp() + 1000
1791";
1792 let editor = Editor::of("t.beck", source);
1793 assert!(
1794 !editor.diagnostics().has_errors(),
1795 "{}",
1796 editor.diagnostics().render(editor.source_map())
1797 );
1798 let hint = editor
1799 .hints()
1800 .into_iter()
1801 .find(|h| h.kind == HintKind::Effects)
1802 .expect("`later` performs what `stamp` performs and does not say so");
1803 assert_eq!(&source[hint.offset as usize..hint.offset as usize + 1], ":");
1804 assert!(hint.label.starts_with(" uses "), "{}", hint.label);
1805
1806 let mut written = source.to_string();
1807 written.insert_str(hint.offset as usize, &hint.label);
1808 let after = Editor::of("t.beck", &written);
1809 assert!(
1810 !after.diagnostics().has_errors(),
1811 "a hint you can paste in:\n{written}\n{}",
1812 after.diagnostics().render(after.source_map())
1813 );
1814 // And `stamp`, which declares its row, is not hinted about it a second time.
1815 assert!(editor
1816 .hints()
1817 .iter()
1818 .all(|h| h.kind != HintKind::Effects || h.offset > caret(source, "def later")));
1819 }
1820}