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}