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}