Documentation
¶
Index ¶
- Constants
- Variables
- func SemanticTokensLegend() lsp.SemanticTokensLegend
- type Document
- type DocumentVersion
- func (v *DocumentVersion) AST() *rl.SourceFile
- func (v *DocumentVersion) Diagnostics() []lsp.Diagnostic
- func (v *DocumentVersion) Encoding() PositionEncoding
- func (v *DocumentVersion) FileID() FileID
- func (v *DocumentVersion) GetLine(line int) string
- func (v *DocumentVersion) LastGood() *DocumentVersion
- func (v *DocumentVersion) LineIndex() *LineIndex
- func (v *DocumentVersion) Release()
- func (v *DocumentVersion) Resolved() *check.Resolved
- func (v *DocumentVersion) Text() string
- func (v *DocumentVersion) Tree() *rts.RadTree
- func (v *DocumentVersion) Types() *check.TypeInfo
- func (v *DocumentVersion) URI() string
- func (v *DocumentVersion) Version() int64
- type FileID
- type LineIndex
- type PositionEncoding
- type SemanticTokenType
- type State
- func (s *State) AddDoc(uri, text string)
- func (s *State) CodeAction(snap *DocumentVersion, r lsp.Range) (result []lsp.CodeAction, err error)
- func (s *State) Complete(snap *DocumentVersion, pos lsp.Pos) (result []lsp.CompletionItem, err error)
- func (s *State) Definition(snap *DocumentVersion, pos lsp.Pos) (*lsp.Location, error)
- func (s *State) DocumentSymbols(snap *DocumentVersion) ([]lsp.DocumentSymbol, error)
- func (s *State) Encoding() PositionEncoding
- func (s *State) FileIDFor(uri string) FileID
- func (s *State) Hover(snap *DocumentVersion, pos lsp.Pos) (*lsp.Hover, error)
- func (s *State) References(snap *DocumentVersion, pos lsp.Pos, includeDecl bool) ([]lsp.Location, error)
- func (s *State) Rename(snap *DocumentVersion, pos lsp.Pos, newName string) (*lsp.WorkspaceEdit, error)
- func (s *State) SemanticTokens(snap *DocumentVersion) (*lsp.SemanticTokens, error)
- func (s *State) SetEncoding(enc PositionEncoding)
- func (s *State) Snapshot(uri string) *DocumentVersion
- func (s *State) SnapshotByID(id FileID) *DocumentVersion
- func (s *State) UpdateDoc(uri string, changes []lsp.TextDocumentContentChangeEvent)
Constants ¶
const (
RadShebang = "#!/usr/bin/env rad"
)
Variables ¶
var ErrInvalidRenameName = errors.New("not a valid Rad identifier")
ErrInvalidRenameName signals the requested new name isn't a legal Rad identifier. Wrapped by errInvalidRenameName(reason, name) at the call site so the LSP client surfaces *which* rule the input violated (digit start, bad character, reserved word).
var ErrInvalidRenameTarget = errors.New("the symbol under the cursor cannot be renamed")
ErrInvalidRenameTarget signals the cursor isn't on a renameable symbol - e.g. on whitespace, on a builtin (no source decl), or on a literal. The LSP client renders this as "Cannot rename" in the rename popup. The message is generic because the cursor's "what's wrong" depends on context (whitespace vs builtin vs unresolved); callers that need precision should wrap.
var ErrRenameWouldCollide = errors.New("name is already in scope")
ErrRenameWouldCollide signals the new name is already in scope. Wrapped by errRenameCollision(name, ownerHint) so the message tells the user what they'd be colliding with - a same-scope local, a parent-scope binding, or an unloaded builtin.
Functions ¶
func SemanticTokensLegend ¶ added in v0.11.0
func SemanticTokensLegend() lsp.SemanticTokensLegend
SemanticTokensLegend is the shared client/server vocabulary. Exposed via the initialize response so the editor knows how to decode the indices in our emitted token data.
We deliberately keep this list small. The LSP standard names run to 22 token types and 10 modifiers; emitting only the distinctions Rad's analyzer actually understands keeps the theming honest. We can grow this list as the analyzer's view gets richer (e.g. constants, enum members, type names).
Types ¶
type Document ¶ added in v0.11.0
type Document struct {
// contains filtered or unexported fields
}
Document owns the current snapshot of one LSP document. The snapshot pointer is read lock-free; writers serialize through `mu` to ensure a coherent prev->next chain (tree-sitter parsing today is wholesale, but a future incremental-parse path would still want this invariant).
The FileID is fixed at construction; it stays stable across every version of the document so internal code can hold a FileID and always reach the latest snapshot via the State's lookup tables.
func (*Document) Snapshot ¶ added in v0.11.0
func (d *Document) Snapshot() *DocumentVersion
Snapshot returns the current immutable version of this document. Lock-free; safe to call from any goroutine.
func (*Document) Update ¶ added in v0.11.0
func (d *Document) Update(produce func(prev *DocumentVersion) *DocumentVersion) *DocumentVersion
Update runs `produce` under the writer lock to compute the next version from the previous (nil on first open), then atomically swaps it into place. The new version arrives with refs=1 (held by us); after the store we Release the previous version, dropping Document's reference to it. Any reader that had already Acquired the old version keeps it alive via the refcount.
type DocumentVersion ¶ added in v0.11.0
type DocumentVersion struct {
// contains filtered or unexported fields
}
DocumentVersion is an immutable snapshot of a single parse of a document. Multiple readers can hold the same *DocumentVersion concurrently without coordination: the data inside is guaranteed not to mutate. The next didChange produces a NEW DocumentVersion (built off the old) and atomically swaps the owning Document's current pointer.
This is the load-bearing piece of Phase 8: it lets LSP request handlers (hover, goto-def, completion, etc.) grab a snapshot once and reason about a frozen world for the duration of the request, rather than racing against the next keystroke.
Lifetime: the underlying tree-sitter tree owns C-heap memory that the Go GC can't reclaim. We refcount accordingly: the Document holds one reference; each call to State.Snapshot bumps the count and returns the snapshot to the caller, who MUST call Release() when done. When the count reaches zero, the tree is Close()d.
Why refcounting and not a finalizer: tree-sitter Nodes carry references into the C tree that the Go GC doesn't see. A finalizer can fire on a tree while another goroutine is walking nodes that point into it - the documented cgo hazard. Explicit release gives us deterministic free at the moment we know no reader is touching the tree.
func (*DocumentVersion) AST ¶ added in v0.11.0
func (v *DocumentVersion) AST() *rl.SourceFile
func (*DocumentVersion) Diagnostics ¶ added in v0.11.0
func (v *DocumentVersion) Diagnostics() []lsp.Diagnostic
func (*DocumentVersion) Encoding ¶ added in v0.11.0
func (v *DocumentVersion) Encoding() PositionEncoding
func (*DocumentVersion) FileID ¶ added in v0.11.0
func (v *DocumentVersion) FileID() FileID
func (*DocumentVersion) GetLine ¶ added in v0.11.0
func (v *DocumentVersion) GetLine(line int) string
GetLine returns the source of the line at the given index, or "" if out of range. Kept on DocumentVersion (not LineIndex) because callers usually want both the text and the index together.
func (*DocumentVersion) LastGood ¶ added in v0.11.0
func (v *DocumentVersion) LastGood() *DocumentVersion
LastGood returns the most recent version of this document whose resolved/types are non-nil, or nil if no version has ever converted cleanly. Callers that want to fall back to last-good indexes (e.g. UFCS completion ranking mid-edit) should read from this when their primary snapshot's resolved/types are nil. The returned version shares this version's lifetime - do NOT Release it independently.
func (*DocumentVersion) LineIndex ¶ added in v0.11.0
func (v *DocumentVersion) LineIndex() *LineIndex
func (*DocumentVersion) Release ¶ added in v0.11.0
func (v *DocumentVersion) Release()
Release drops one reference. When the count reaches zero the underlying tree-sitter tree is freed. Each call to State.Snapshot pairs with exactly one Release - callers typically `defer snap.Release()` right after the nil-check.
func (*DocumentVersion) Resolved ¶ added in v0.11.0
func (v *DocumentVersion) Resolved() *check.Resolved
func (*DocumentVersion) Text ¶ added in v0.11.0
func (v *DocumentVersion) Text() string
func (*DocumentVersion) Tree ¶ added in v0.11.0
func (v *DocumentVersion) Tree() *rts.RadTree
func (*DocumentVersion) Types ¶ added in v0.11.0
func (v *DocumentVersion) Types() *check.TypeInfo
func (*DocumentVersion) URI ¶ added in v0.11.0
func (v *DocumentVersion) URI() string
func (*DocumentVersion) Version ¶ added in v0.11.0
func (v *DocumentVersion) Version() int64
type FileID ¶ added in v0.11.0
type FileID uint32
FileID is an opaque identifier the analyzer uses to refer to one document. It's separate from the LSP URI on purpose:
- URIs are strings, expensive to hash and pass around in deep traversals (symbol lookups, cross-file references, etc.)
- URIs are externally controlled - any uniqueness invariant lives outside our type system, so the compiler can't help us when we accidentally hand the wrong string to a function expecting a "real" document handle
- URIs survive forever in client memory but a server-side close should be able to recycle/invalidate references; an opaque int ID gives us the seam to do that without changing the wire
FileIDs are minted monotonically per State and never re-used within a session. Comparing FileIDs is O(1) and they fit in a register. The State maintains both URI -> FileID and FileID -> *Document maps so we can translate at the wire boundary and keep everything downstream typed.
const InvalidFileID FileID = 0
InvalidFileID is the sentinel returned when a lookup misses. Real FileIDs are always >= 1 - we start the counter at 1 so zero stays meaningful as "no file" / "not assigned yet."
type LineIndex ¶ added in v0.11.0
type LineIndex struct {
// contains filtered or unexported fields
}
LineIndex translates positions on a fixed text snapshot. Tree-sitter reports positions in utf-8 byte columns; LSP clients want them in the encoding negotiated at initialize. LineIndex is the single conversion point so the rest of the analyzer can stay in byte units (matching tree-sitter) without ever caring about utf-16 surrogate pairs.
Construction is O(n) over the text; per-position queries are O(byteCol) on the affected line because we have to walk the bytes to count code units. That's fine at our scale (Rad files are small and queries are per-diagnostic, not per-keystroke).
func NewLineIndex ¶ added in v0.11.0
NewLineIndex scans text once to record where each line begins.
func (*LineIndex) ByteColumnTo ¶ added in v0.11.0
func (l *LineIndex) ByteColumnTo(line, byteCol int, enc PositionEncoding) int
ByteColumnTo converts a utf-8 byte column on `line` into a column in the target encoding. Out-of-range or past-end-of-line inputs are clamped to the line's length in the target encoding so callers can't produce LSP positions that violate the spec.
func (*LineIndex) ColumnToByte ¶ added in v0.11.0
func (l *LineIndex) ColumnToByte(line, col int, enc PositionEncoding) int
ColumnToByte is the inverse of ByteColumnTo: given a column reported in the client's encoding, return the utf-8 byte column tree-sitter uses internally. Out-of-range inputs clamp to the line's byte length so we never index past the end of a line.
type PositionEncoding ¶ added in v0.11.0
type PositionEncoding string
PositionEncoding identifies how LSP positions are measured. LSP 3.17 lets a client/server negotiate one of these at the initialize handshake; utf-16 is the default when no negotiation occurs.
const ( EncodingUTF8 PositionEncoding = "utf-8" EncodingUTF16 PositionEncoding = "utf-16" EncodingUTF32 PositionEncoding = "utf-32" )
func NegotiatePositionEncoding ¶ added in v0.11.0
func NegotiatePositionEncoding(clientOffered []PositionEncoding) PositionEncoding
NegotiatePositionEncoding picks the server-preferred encoding from the list a client advertised in its initialize params. Per LSP 3.17, if the client offers nothing we must use utf-16. If the client offers something but we share nothing in common we also fall back to utf-16 (the spec's universal mandatory).
type SemanticTokenType ¶ added in v0.11.0
type SemanticTokenType int
SemanticTokenType indexes into the legend below; the LSP wire format encodes token-type as the index, so the legend must stay in sync with these constants. Adding new types only appends to the end of the legend - never insert in the middle.
const ( TokenTypeFunction SemanticTokenType = iota TokenTypeParameter TokenTypeVariable TokenTypeType )
type State ¶
type State struct {
// contains filtered or unexported fields
}
State is the per-server registry of open documents. It owns the shared tree-sitter parser and the negotiated position encoding. The docs map is guarded by a top-level mutex for membership changes (open/close); per-document write coordination lives on Document itself, and reads of any document's content are lock-free via its snapshot pointer.
Documents are identified two ways: by URI (the LSP wire concern) and by FileID (the internal handle). The URI is stable from the client's perspective, but our internal code prefers FileID since it's cheaper to compare and clearly distinguishes "an opened document" from "an arbitrary string."
func (*State) AddDoc ¶
AddDoc opens a document for the first time and produces its initial version. If the URI is already open the existing entry is replaced - LSP shouldn't send didOpen twice for the same URI, but be defensive. A fresh FileID is allocated per AddDoc; reopening a closed URI gets a new id (we don't reuse ids within a session).
func (*State) CodeAction ¶
func (s *State) CodeAction(snap *DocumentVersion, r lsp.Range) (result []lsp.CodeAction, err error)
CodeAction returns the available code actions for the given range against a fixed document snapshot. Same snapshot discipline as Complete: the caller's responsibility to grab a fresh one.
Two sources of actions today:
- The always-available shebang insertion, when the file is missing one - this is whole-document, not range-scoped.
- Diagnostic quick fixes - per-diagnostic actions for issues whose range overlaps the request range. Structured fixes (machine-applicable edits) come out as quickfix-kinded CodeActions; suggestion-only diagnostics produce refactor- kinded info actions with no edit.
func (*State) Complete ¶
func (s *State) Complete(snap *DocumentVersion, pos lsp.Pos) (result []lsp.CompletionItem, err error)
Complete runs the completion logic against a fixed document snapshot. Callers (server handlers) grab the snapshot themselves and pass it explicitly: the snapshot pin-points which version of the document this completion is meant for, even if the user types more characters before the response is sent.
The result is the union of the shebang stub (only meaningful on line 0) and a scope-aware identifier list built from the AST and the resolved/type indexes. The shebang stays first so users who started a new file still get the "Add #!" suggestion at the top of the list.
func (*State) Definition ¶ added in v0.11.0
Definition answers textDocument/definition: given a cursor on an identifier, return the Location of its declaration. Returns nil when there's nothing to jump to: the cursor isn't on an identifier, the name didn't resolve, or the symbol is a builtin (no source decl span to point at).
Single-file today, single Location: Rad has no imports, so we never need to return multiple Locations. The LSP spec allows Location | Location[] | LocationLink[] | null; staying on Location keeps the wire shape simple and trivially forward-compatible.
Builtins return nil rather than e.g. a synthetic stdlib URL. "Go to definition" on `print` doing nothing is much better than dumping the user into a generated file they can't edit; hover already shows them the signature, which is the actually-useful information.
func (*State) DocumentSymbols ¶ added in v0.11.0
func (s *State) DocumentSymbols(snap *DocumentVersion) ([]lsp.DocumentSymbol, error)
DocumentSymbols answers textDocument/documentSymbol with the outline of a file: top-level functions, top-level variable declarations, the args block (with its declared args nested), and each cmd block (with its declared args nested). Returns an empty slice (NOT nil) when the file has no symbols - the LSP wire expects a JSON array.
What counts as "top-level":
- FnDef in SourceFile.Stmts -> Function.
- First-assignment of an identifier in SourceFile.Stmts -> Variable. Only the first assignment per name is emitted; later re-assigns of the same name are not new outline entries.
- SourceFile.Args - the script's args: block - rendered as a Namespace "args" with each ArgDecl as a Variable child.
- Each SourceFile.Cmds entry - rendered as a Module child group named after the command, with its ArgDecls nested.
We deliberately do NOT recurse into function bodies. Editors that want a deeper outline (per-statement, per-loop) can render local vars on demand via Hover/GotoDef; the outline is meant to be glanceable, not exhaustive.
func (*State) Encoding ¶ added in v0.11.0
func (s *State) Encoding() PositionEncoding
Encoding returns the LSP position encoding currently in use.
func (*State) FileIDFor ¶ added in v0.11.0
FileIDFor returns the FileID assigned to the named URI, or InvalidFileID if the document isn't open. Internal callers that already have a *Document should just call doc.FileID() directly.
func (*State) Hover ¶ added in v0.11.0
Hover answers textDocument/hover against a fixed document snapshot. We find the smallest Identifier whose span covers the cursor, look up its Symbol in the resolved view, and format the symbol's type (declared annotation if any, else what the type checker inferred). Returns nil when the cursor isn't on a hoverable thing - the LSP spec lets us return null and the client will simply show nothing.
Why identifier-only for v1: the value gradient is steep. Hovering a name is what users want 90% of the time; expression-result hover (e.g. on a binop or call) is nice-to-have and lights up trivially once we route through TypeInfo.ExprTypes here. We'll grow it as users notice it's missing.
func (*State) References ¶ added in v0.11.0
func (s *State) References(snap *DocumentVersion, pos lsp.Pos, includeDecl bool) ([]lsp.Location, error)
References answers textDocument/references: given a cursor on an identifier, return every Location where that symbol is used in the document. The LSP context flag IncludeDeclaration toggles whether the declaration site itself is included.
We compute on-demand from resolved.Uses rather than maintaining a pre-built reverse index. References is a click-driven feature (not background-streaming like diagnostics), so the cost of walking the Uses map once per request is fine - and it avoids growing every snapshot with a map most snapshots will never use. If the access pattern changes, the snapshot can grow a cache later without breaking callers.
Returns an empty slice (not nil) when there are no references - the LSP wire expects an array.
func (*State) Rename ¶ added in v0.11.0
func (s *State) Rename(snap *DocumentVersion, pos lsp.Pos, newName string) (*lsp.WorkspaceEdit, error)
Rename answers textDocument/rename: produce a WorkspaceEdit covering every site that needs to change so the symbol under the cursor becomes `newName`. Single-file scope - Rad has no imports today, so cross-file renames don't apply.
Returns:
- ErrInvalidRenameTarget when the cursor isn't on a renameable symbol (whitespace, builtin, unresolved name).
- ErrInvalidRenameName when newName isn't a legal identifier.
- ErrRenameWouldCollide when the new name already binds something in the target symbol's scope.
- A WorkspaceEdit with one TextEdit per site otherwise.
func (*State) SemanticTokens ¶ added in v0.11.0
func (s *State) SemanticTokens(snap *DocumentVersion) (*lsp.SemanticTokens, error)
SemanticTokens answers textDocument/semanticTokens/full: walk the resolved view and tag each identifier with its kind (function vs parameter vs variable). The editor uses this for finer-grained syntax highlighting than tree-sitter's CST-level coloring alone provides - e.g. distinguishing a builtin call from a local-variable read.
Returns an empty data slice (not nil) when there's nothing to emit. The LSP spec lets us return null, but always-array makes the client's decode path simpler.
Encoding: per LSP 3.17, tokens are sorted by position then delta-encoded as quintuples of uint. We do the sort and encode in one pass at the end so the AST walk can stay shape-blind.
func (*State) SetEncoding ¶ added in v0.11.0
func (s *State) SetEncoding(enc PositionEncoding)
SetEncoding installs the encoding negotiated at initialize. Must be called before any didOpen so the first document's diagnostics use the right encoding. Initialize happens exactly once per session, so we don't bother guarding against a second call.
func (*State) Snapshot ¶ added in v0.11.0
func (s *State) Snapshot(uri string) *DocumentVersion
Snapshot returns the current version of the named document with its refcount incremented. The caller MUST call Release() when done (typically `defer snap.Release()` right after the nil-check). Returns nil if the document isn't open.
The acquire-after-load loop handles a small race window: between loading the atomic pointer and bumping the refcount, the writer could have Released the version we observed. In that case acquire returns false and we retry; the Document.snapshot pointer has already been updated to a newer version by the time we get here.
func (*State) SnapshotByID ¶ added in v0.11.0
func (s *State) SnapshotByID(id FileID) *DocumentVersion
SnapshotByID returns the current snapshot for a FileID, or nil if the id isn't known. Same Acquire/Release contract as Snapshot: caller MUST Release() what they get back.
func (*State) UpdateDoc ¶
func (s *State) UpdateDoc(uri string, changes []lsp.TextDocumentContentChangeEvent)
UpdateDoc applies a sequence of content changes and produces a fresh version for each. Today the protocol delivers full-document text (TextDocumentSync = 1), so each change spawns a wholesale reparse; the per-change loop is preserved so a future move to incremental edits is a one-spot change.