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