Skip to main content

gdscript_base/
lib.rs

1//! `gdscript-base` — foundational POD types shared across the gdscript-analyzer.
2//!
3//! The lowest layer of the crate stack (`plans/01-ARCHITECTURE.md` §1). It holds the
4//! engine-/protocol-neutral, `serde`-serializable result structs every client maps to
5//! its own protocol, plus byte-offset position types and a [`LineIndex`] for the
6//! byte↔(line, column) and byte↔UTF-16 conversions LSP clients need.
7//!
8//! All offsets are **byte** offsets into a file's UTF-8 source. No logic beyond the
9//! conversions lives here. The crate is `wasm32`-safe (no `std::fs`, clocks, threads).
10//!
11//! These POD result structs are the analyzer's **public wire contract** (consumed via
12//! [`gdscript_ide`](https://docs.rs/gdscript-ide)), so every public item is documented and
13//! `#![deny(missing_docs)]` keeps it that way.
14#![cfg_attr(docsrs, feature(doc_cfg))]
15#![deny(missing_docs)]
16
17use serde::{Deserialize, Serialize};
18
19/// An opaque file handle. The host owns the `FileId` → text mapping; the library never
20/// reads paths.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
22pub struct FileId(pub u32);
23
24/// A half-open byte range `[start, end)` into a file's UTF-8 source.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub struct TextRange {
27    /// Inclusive start byte offset.
28    pub start: u32,
29    /// Exclusive end byte offset.
30    pub end: u32,
31}
32
33impl TextRange {
34    /// A new range from `start` to `end` (bytes).
35    #[must_use]
36    pub const fn new(start: u32, end: u32) -> Self {
37        Self { start, end }
38    }
39}
40
41/// A `(file, byte offset)` cursor position.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub struct FilePosition {
44    /// The file.
45    pub file: FileId,
46    /// The byte offset within the file.
47    pub offset: u32,
48}
49
50/// Diagnostic severity.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "lowercase")]
53pub enum Severity {
54    /// A hard error.
55    Error,
56    /// A warning.
57    Warning,
58    /// Informational.
59    Info,
60    /// A hint.
61    Hint,
62}
63
64/// What analysis layer produced a diagnostic — lets clients group/filter parse vs. type
65/// diagnostics without parsing the `code`.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
67#[serde(rename_all = "lowercase")]
68pub enum DiagnosticSource {
69    /// A lexer / parser / indentation diagnostic (Phase 1).
70    #[default]
71    Syntax,
72    /// A type / semantic diagnostic from inference (Phase 2).
73    Type,
74}
75
76/// A diagnostic with a byte range, a stable machine code, a severity, and a message.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct Diagnostic {
79    /// The byte range the diagnostic applies to.
80    pub range: TextRange,
81    /// Severity.
82    pub severity: Severity,
83    /// A stable code, e.g. `GDSCRIPT_SYNTAX` or `INTEGER_DIVISION`.
84    pub code: String,
85    /// Human-readable message.
86    pub message: String,
87    /// Which analysis layer produced it. Defaults to [`DiagnosticSource::Syntax`] so older
88    /// serialized diagnostics and Phase-1 call sites round-trip unchanged.
89    #[serde(default)]
90    pub source: DiagnosticSource,
91    /// Quick-fixes offered for this diagnostic (e.g. "add type annotation"). Empty when none.
92    #[serde(default)]
93    pub fixes: Vec<CodeAction>,
94}
95
96/// The kind of a document symbol (a subset of LSP `SymbolKind`, named for GDScript).
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum SymbolKind {
100    /// A `class_name` / inner `class`.
101    Class,
102    /// A `func`.
103    Function,
104    /// A `func` that is a class member (currently same as `Function`).
105    Method,
106    /// A `var`.
107    Variable,
108    /// A `const`.
109    Constant,
110    /// An `enum`.
111    Enum,
112    /// An enum variant.
113    EnumMember,
114    /// A `signal`.
115    Signal,
116}
117
118/// A (possibly nested) symbol in a document's outline.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct DocumentSymbol {
121    /// The symbol name.
122    pub name: String,
123    /// Optional detail (e.g. a signature).
124    pub detail: Option<String>,
125    /// The symbol kind.
126    pub kind: SymbolKind,
127    /// The full range of the symbol (its whole declaration).
128    pub range: TextRange,
129    /// The range of the name/selection within `range`.
130    pub selection_range: TextRange,
131    /// Nested symbols (members of a class, variants of an enum).
132    pub children: Vec<DocumentSymbol>,
133}
134
135/// What a fold range corresponds to.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "lowercase")]
138pub enum FoldKind {
139    /// An indented block body.
140    Block,
141    /// A `#region`…`#endregion` pair.
142    Region,
143    /// A multi-line bracketed span.
144    Brackets,
145}
146
147/// A foldable range.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149pub struct FoldRange {
150    /// The foldable byte range.
151    pub range: TextRange,
152    /// What kind of fold it is.
153    pub kind: FoldKind,
154}
155
156/// The kind of a completion item.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum CompletionKind {
160    /// A language keyword.
161    Keyword,
162    /// An annotation (`@export`, …).
163    Annotation,
164    /// A function/method name.
165    Function,
166    /// A variable / parameter / local.
167    Variable,
168    /// A constant.
169    Constant,
170    /// A class / type name.
171    Class,
172    /// An enum.
173    Enum,
174    /// A signal.
175    Signal,
176}
177
178/// A by-name completion suggestion.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct CompletionItem {
181    /// The label shown / inserted.
182    pub label: String,
183    /// The kind of suggestion.
184    pub kind: CompletionKind,
185    /// Optional text to insert (defaults to `label`).
186    pub insert_text: Option<String>,
187    /// Optional secondary text shown after the label — a type or signature, e.g. `: int`
188    /// or `(node: Node) -> void`. Phase 2 fills this for typed members; `None` keeps the
189    /// Phase-1 by-name items unchanged.
190    #[serde(default)]
191    pub detail: Option<String>,
192}
193
194// ---------------------------------------------------------------------------
195// Phase 2 PODs — hover, signature help, inlay hints, code actions, navigation.
196// Each is an engine-/protocol-neutral result struct (byte offsets, serde). A feature
197// returning one of these maps it to its own protocol at the client edge. See
198// `plans/PHASE-2-IMPLEMENTATION-PLAYBOOK.md` §1.1.
199// ---------------------------------------------------------------------------
200
201/// Documentation rendered as Markdown (engine `BBCode` already converted at codegen time).
202pub type Markdown = String;
203
204/// The result of a hover query: an inferred type / signature label plus engine docs.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct HoverResult {
207    /// The inferred type / signature rendered for display, e.g. `Node` or
208    /// `add_child(node: Node) -> void`. `None` when the type is `Unknown` (elided — the
209    /// Phase-3 cross-file seam) so we never show a placeholder type.
210    pub ty_label: Option<String>,
211    /// Engine documentation as Markdown. Empty when no doc XML is available.
212    pub doc: Markdown,
213    /// The source range the hover applies to (the hovered token / expression).
214    pub range: TextRange,
215}
216
217/// One parameter within a [`SignatureInfo`].
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct ParamInfo {
220    /// The parameter label, e.g. `node: Node` or `force_readable_name: bool = false`.
221    pub label: String,
222    /// Optional documentation (Markdown).
223    pub doc: Markdown,
224}
225
226/// One signature shown in signature help (GDScript has no overloads, so usually one).
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct SignatureInfo {
229    /// The full signature label, e.g.
230    /// `add_child(node: Node, force_readable_name: bool = false) -> void`.
231    pub label: String,
232    /// Optional documentation (Markdown).
233    pub doc: Markdown,
234    /// The parameters, in order.
235    pub params: Vec<ParamInfo>,
236}
237
238/// The result of a signature-help query at a call site.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct SignatureHelp {
241    /// The candidate signatures.
242    pub signatures: Vec<SignatureInfo>,
243    /// Index into `signatures` of the active one.
244    pub active_signature: u32,
245    /// Index of the active parameter within the active signature. A vararg call keeps the
246    /// last parameter active once the fixed parameters are exhausted.
247    pub active_parameter: u32,
248}
249
250/// What an [`InlayHint`] represents.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "lowercase")]
253pub enum InlayHintKind {
254    /// An inferred type, e.g. `: int` after a `:=` declaration or an unannotated parameter.
255    Type,
256    /// An inferred parameter name shown at a call site.
257    Parameter,
258}
259
260/// An inline hint rendered at a byte offset (e.g. the `: int` the engine LSP omits).
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct InlayHint {
263    /// The byte offset at which to render the hint.
264    pub offset: u32,
265    /// The hint text, e.g. `: int`.
266    pub label: String,
267    /// What kind of hint it is.
268    pub kind: InlayHintKind,
269}
270
271/// The semantic role of a [`SemanticToken`] — a GDScript-named subset of the LSP standard token
272/// types. Richer than a TextMate grammar: it distinguishes a type from a variable, a parameter from
273/// a local, a member from a global, a declaration from a use.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub enum SemanticTokenType {
277    /// A free function / a function call.
278    Function,
279    /// A method (a function that is a class member).
280    Method,
281    /// A local variable / `var`.
282    Variable,
283    /// A function parameter.
284    Parameter,
285    /// A member field accessed via `.`.
286    Property,
287    /// A `class` / `class_name`.
288    Class,
289    /// An `enum`.
290    Enum,
291    /// An enum variant.
292    EnumMember,
293    /// A type name (in a `: T`, `as T`, `is T`, `extends T`, `-> T` position).
294    Type,
295    /// An annotation, e.g. `@export`.
296    Decorator,
297    /// A numeric literal.
298    Number,
299    /// A string literal (incl. `StringName` / `NodePath`).
300    String,
301    /// A comment.
302    Comment,
303    /// A `signal`.
304    Signal,
305    /// A `const`.
306    Constant,
307}
308
309/// Bit flags for [`SemanticToken::modifiers`] (the LSP standard modifier subset we emit).
310pub mod semantic_token_modifier {
311    /// The token is the *declaration* of the symbol (vs. a use).
312    pub const DECLARATION: u32 = 1 << 0;
313    /// A read-only binding (`const`).
314    pub const READONLY: u32 = 1 << 1;
315    /// A `static` member.
316    pub const STATIC: u32 = 1 << 2;
317    /// An engine / built-in symbol (not user code).
318    pub const DEFAULT_LIBRARY: u32 = 1 << 3;
319}
320
321/// A semantic-highlighting token: a source range classified by its contextual/resolved role. Drives
322/// `textDocument/semanticTokens` — intelligence a grammar can't produce. Modifiers are a bitset of
323/// [`semantic_token_modifier`] flags.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325pub struct SemanticToken {
326    /// The token's byte range.
327    pub range: TextRange,
328    /// What the token denotes.
329    pub token_type: SemanticTokenType,
330    /// A bitset of [`semantic_token_modifier`] flags.
331    pub modifiers: u32,
332}
333
334/// A single text edit: replace `range` with `new_text`.
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336pub struct TextEdit {
337    /// The byte range to replace.
338    pub range: TextRange,
339    /// The replacement text.
340    pub new_text: String,
341}
342
343/// The edits to apply to one file (non-overlapping; the client sorts and applies them).
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
345pub struct FileEdit {
346    /// The file the edits apply to.
347    pub file: FileId,
348    /// The edits within that file.
349    pub edits: Vec<TextEdit>,
350}
351
352/// A set of edits across one or more files (a cross-file rename, a quick-fix). Phase 3's rename
353/// spans files, so this is multi-file; a single-file change is just one [`FileEdit`].
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub struct SourceChange {
356    /// The per-file edits.
357    pub edits: Vec<FileEdit>,
358}
359
360impl SourceChange {
361    /// A change that touches a single file.
362    #[must_use]
363    pub fn single(file: FileId, edits: Vec<TextEdit>) -> Self {
364        Self {
365            edits: vec![FileEdit { file, edits }],
366        }
367    }
368}
369
370/// A (file, range) pair — the atom of cross-file navigation results.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
372pub struct FileRange {
373    /// The file the range lives in.
374    pub file: FileId,
375    /// The byte range.
376    pub range: TextRange,
377}
378
379/// Why a token is a reference (rust-analyzer's `ReferenceCategory`, trimmed).
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(rename_all = "lowercase")]
382pub enum ReferenceKind {
383    /// The symbol's declaration site.
384    Declaration,
385    /// A read (the default — any non-write use).
386    Read,
387    /// A write (the symbol on the left of an assignment).
388    Write,
389}
390
391/// One reference to a symbol (find-references result), including its declaration.
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
393pub struct Reference {
394    /// The file the reference is in.
395    pub file: FileId,
396    /// The identifier-token range.
397    pub range: TextRange,
398    /// What kind of reference it is.
399    pub kind: ReferenceKind,
400}
401
402/// Why a [rename](crate) was refused — the "correct or it refuses" contract. Never a partial edit.
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(tag = "kind", rename_all = "snake_case")]
405pub enum RenameError {
406    /// The new name is not a single valid GDScript identifier (or is a keyword).
407    InvalidIdentifier {
408        /// The rejected name.
409        new_name: String,
410    },
411    /// The target is an engine/builtin symbol, or could not be resolved — not ours to rename.
412    NotRenamable {
413        /// Why.
414        reason: String,
415    },
416    /// The new name already exists in an affected scope.
417    WouldCollide {
418        /// Where the colliding symbol is.
419        at: FileRange,
420        /// The colliding name.
421        with: String,
422    },
423    /// The symbol is also reachable via a surface this analyzer cannot safely rewrite (a `.tscn`
424    /// `[connection]`/string call for a method/signal, the `project.godot` `[autoload]` key). We
425    /// refuse rather than leave a stale reference behind.
426    CrossesUnsupportedBoundary {
427        /// What boundary.
428        what: String,
429    },
430}
431
432/// A code action / quick-fix: a titled, optionally-kinded [`SourceChange`].
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434pub struct CodeAction {
435    /// Human-readable title, e.g. `Add type annotation`.
436    pub title: String,
437    /// An LSP-style kind such as `quickfix` or `refactor.rewrite`; `None` if unspecified.
438    pub kind: Option<String>,
439    /// The edit this action performs.
440    pub edit: SourceChange,
441}
442
443/// A navigation target (goto-definition / -declaration). Phase 2 only ever points within
444/// the same file; cross-file targets arrive in Phase 3.
445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
446pub struct NavTarget {
447    /// The file the target lives in.
448    pub file: FileId,
449    /// The full range of the target's declaration.
450    pub full_range: TextRange,
451    /// The name / selection range to focus within `full_range`.
452    pub focus_range: TextRange,
453    /// The target symbol's name.
454    pub name: String,
455    /// The target symbol's kind.
456    pub kind: SymbolKind,
457}
458
459/// A read query was cancelled by a concurrent change. (Phase 1 never actually cancels,
460/// but the type is on the API surface so the Phase 3 salsa swap is source-compatible.)
461#[derive(Debug, Clone, Copy, PartialEq, Eq)]
462pub struct Cancelled;
463
464/// The result of a cancellable read query.
465pub type Cancellable<T> = Result<T, Cancelled>;
466
467/// Maps byte offsets to/from `(line, column)` and UTF-16 columns.
468///
469/// Lines and columns are 0-based. The core emits byte offsets; LSP/JS clients convert
470/// to UTF-16 via this (the documented position-encoding footgun —
471/// `plans/01-ARCHITECTURE.md` §4).
472///
473/// ```
474/// use gdscript_base::LineIndex;
475/// // Line 1 contains a 2-byte `é`, so the byte column and the UTF-16 column diverge
476/// // after it — the encoding footgun this type exists to handle.
477/// let src = "var x := 1\nvar é := 2\n";
478/// let idx = LineIndex::new(src);
479/// let colon = u32::try_from(src.rfind(":=").unwrap()).unwrap(); // the `:` on line 1
480/// let lc = idx.line_col(colon);
481/// assert_eq!((lc.line, lc.col), (1, 7)); // byte column 7 (`é` is 2 bytes)
482/// assert_eq!(idx.utf16_col(src, colon), 6); // UTF-16 column 6 (`é` is 1 code unit)
483/// ```
484#[derive(Debug, Clone)]
485pub struct LineIndex {
486    /// Byte offset of the start of each line (line 0 starts at 0).
487    line_starts: Vec<u32>,
488    /// Total source length in bytes.
489    len: u32,
490}
491
492/// A 0-based `(line, column)` position. `col` is a byte offset within the line.
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
494pub struct LineCol {
495    /// 0-based line.
496    pub line: u32,
497    /// 0-based byte column within the line.
498    pub col: u32,
499}
500
501impl LineIndex {
502    /// Build a line index for `text`.
503    #[must_use]
504    pub fn new(text: &str) -> Self {
505        let mut line_starts = vec![0u32];
506        for (i, b) in text.bytes().enumerate() {
507            if b == b'\n' {
508                // `i` fits in u32 for any file we accept (< 4 GiB).
509                #[allow(clippy::cast_possible_truncation)]
510                line_starts.push(i as u32 + 1);
511            }
512        }
513        #[allow(clippy::cast_possible_truncation)]
514        let len = text.len() as u32;
515        Self { line_starts, len }
516    }
517
518    /// The `(line, byte-column)` of a byte offset (clamped to the end of input).
519    #[must_use]
520    pub fn line_col(&self, offset: u32) -> LineCol {
521        let offset = offset.min(self.len);
522        // The line is the last line-start <= offset.
523        let line = match self.line_starts.binary_search(&offset) {
524            Ok(line) => line,
525            Err(next) => next - 1,
526        };
527        #[allow(clippy::cast_possible_truncation)]
528        let line = line as u32;
529        LineCol {
530            line,
531            col: offset - self.line_starts[line as usize],
532        }
533    }
534
535    /// The UTF-16 column of a byte offset on its line (LSP's default encoding).
536    #[must_use]
537    pub fn utf16_col(&self, text: &str, offset: u32) -> u32 {
538        let lc = self.line_col(offset);
539        let line_start = self.line_starts[lc.line as usize] as usize;
540        let col_end = (line_start + lc.col as usize).min(text.len());
541        let units: usize = text[line_start..col_end].chars().map(char::len_utf16).sum();
542        u32::try_from(units).unwrap_or(u32::MAX)
543    }
544
545    /// The number of lines.
546    #[must_use]
547    pub fn line_count(&self) -> u32 {
548        #[allow(clippy::cast_possible_truncation)]
549        {
550            self.line_starts.len() as u32
551        }
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    #[test]
560    fn line_index_basics() {
561        let src = "ab\ncde\n\nx";
562        let idx = LineIndex::new(src);
563        assert_eq!(idx.line_count(), 4);
564        assert_eq!(idx.line_col(0), LineCol { line: 0, col: 0 });
565        assert_eq!(idx.line_col(1), LineCol { line: 0, col: 1 });
566        assert_eq!(idx.line_col(3), LineCol { line: 1, col: 0 }); // 'c'
567        assert_eq!(idx.line_col(7), LineCol { line: 2, col: 0 }); // blank line
568        assert_eq!(idx.line_col(8), LineCol { line: 3, col: 0 }); // 'x'
569    }
570
571    #[test]
572    fn utf16_columns_account_for_astral_chars() {
573        // "a😀b": 'a' is 1 UTF-8 byte / 1 UTF-16 unit; '😀' is 4 bytes / 2 units.
574        let src = "a😀b";
575        let idx = LineIndex::new(src);
576        assert_eq!(idx.utf16_col(src, 0), 0); // before 'a'
577        assert_eq!(idx.utf16_col(src, 1), 1); // before '😀'
578        assert_eq!(idx.utf16_col(src, 5), 3); // before 'b' (1 + 2 units)
579    }
580}