Knot

A full-featured language server for Twine/SugarCube 2 development in VS Code. Smart completions, type inference, cross-file navigation, build integration, and deep semantic understanding of your interactive fiction.

Install from Marketplace

Smart Completions

50+ built-in macros with snippet bodies, passage name completions inside macro args, variable & property chains, close-tag suggestions, and custom widget/macro detection.

Go to Definition

F12 jumps to passage definitions, widget/Macro.add() declarations, variable assignments, and JS globals — across all files in your workspace.

Type Inference

Automatically infers types from <<set>> assignments. Objects, arrays, numbers, strings — property completions and hover type tables just work.

9 Diagnostic Rules

Unknown passages, unknown macros, duplicate passages, unreachable passages, type mismatches, container structure errors, and more. All configurable.

Rename Everywhere

F2 renames passages, variables, and widgets across all files. Updates links, macro args, [[brackets]], and definitions in one operation.

Tweego Integration

Build, watch, and test your project directly from VS Code. Build errors appear as inline diagnostics. One-click open in browser.

Quick Start

Install Knot from the VS Code Marketplace, open a folder containing .tw or .twee files, and Knot automatically begins indexing your workspace. No configuration is required for basic functionality.

The status bar at the bottom of VS Code shows a spinning indicator while indexing, then displays a checkmark with your passage count and detected story format when ready. Click it anytime to open the command menu.

First Project Setup

If you also use tweego to compile, set knot.tweego.path to the tweego executable path. Then use Ctrl+Alt+B to build and Ctrl+Alt+P to navigate between passages.

Smart Completions

Knot provides context-aware completions that adapt based on where and what you're typing. Completions are triggered by the characters <, [, $, _, ., and ", or you can invoke them manually with Ctrl+Space.

Built-in SugarCube Macro Snippets

Typing << provides completions for all 50+ built-in SugarCube macros, each with a snippet that expands the macro with placeholder arguments:

<<if>>        // expands to:  <<if ${1:condition}>>
<<for>>       // expands to:  <<for ${1:_i} to ${2:0} through ${3:10}>>
<<link>>      // expands to:  <<link "${1:label}" "${2:passage}">>

This dramatically reduces syntax errors and speeds up authoring, especially for macros you use infrequently.

Close-Tag Suggestions

Typing <</ triggers context-aware close-tag suggestions. Knot tracks which block macros are currently open using a stack-based nesting algorithm scanning backward from your cursor. It suggests only the unclosed block macro names, sorted by nesting depth (innermost first).

<<widget>>
  <<for>>
    <<if>>
      <</   // suggests: <</if>>  <</for>>  <</widget>>

If no open stack is found, it falls back to showing all known block macro names.

Passage Name Completions in Macro Arguments

When your cursor is inside a string argument of a passage-referencing macro, Knot provides all workspace passage names as completions:

<<goto "|">>       // cursor between quotes shows all passage names
<<link "Talk" "|">>  // second arg shows passage names

This works for <<goto>>, <<link>>, <<button>>, <<include>>, and all other navigation macros.

Variable & Property Completions

Typing $ outside a macro-open context triggers completions for all known story variables across your workspace, with inferred type information. Once you select a variable and type a dot, Knot resolves the inferred type and provides property chain completions:

$player.         // suggests: name, hp, inventory, stats
$player.stats.    // suggests: hp, mp, strength

Custom Widget & Macro Completions

User-defined macros via <<widget>> or Macro.add() appear in completions alongside built-in macros. The defining passage name is shown in the detail field. JS globals declared in Story JavaScript or inline <<script>> blocks also appear as completions with inferred types.

Header Suppression

Completions are automatically suppressed on passage header lines (:: PassageName) to avoid noise.

Go to Definition (F12)

Press F12 on any of the following to jump to its definition, even across files:

ElementNavigates to
Passage name in [[link]]Passage definition
Passage arg like <<goto "Room">>Passage definition
Macro name like <<myWidget>><<widget "myWidget">> or Macro.add() call
Story variable like $healthFirst <<set $health ...>> assignment
JS global identifierDeclaration in Story JavaScript or <<script>>

Find All References (Shift+F12)

Shift+F12 shows every location where the element under your cursor is referenced across the entire workspace:

Rename (F2)

Press F2 on a passage name, story variable, or widget/macro name to rename it across all files in a single atomic operation:

TargetWhat gets updated
Passage[[OldName]][[NewName]], <<goto "OldName">><<goto "NewName">>, header :: OldName:: NewName
VariableEvery <<set $oldVar>> and usage site
Widget/MacroDefinition <<widget "oldName">> and all call sites <<oldName>>

Hold Ctrl (Cmd on macOS) and click on any [[Target]] link or passage-arg macro string like <<goto "Target">> to navigate directly to the passage definition.

Quick Passage Navigation

Ctrl+Alt+P (Cmd+Alt+P on macOS) opens a quick-pick listing every passage in your workspace. Each entry shows the passage name, defining file, and incoming link count.

Outline & Workspace Symbols

The Outline panel shows each passage as a top-level symbol with children for variable assignments and widget definitions. Ctrl+T opens workspace-wide symbol search across passages, widgets/macros, and story variables (with $ prefix).

Hover Information

Hover your mouse over any macro, variable, passage name, or JS global for rich contextual information — instant answers without any keyboard interaction.

ElementWhat Hover Shows
Built-in macroName, description, deprecation warning if applicable
Custom macro/widget"Custom Macro" + defining passage name
Story variable ($var)Name, sigil description, definition location, reference count, inferred type table
Temp variable (_var)Name, scope description (passage-local)
Passage nameName, defining file, incoming links grouped by source with count badges
Property path ($obj.prop)Resolved type of the property; if unknown, shows parent's known properties as suggestions
SugarCube globalAPI description (State, Engine, Story, etc.)
Passage Hover Example

Hovering over [[Forest]] might show: Forest — defined in rooms.tw — 3 incoming links: Hallway (2), Garden (1).

Recognized SugarCube globals: State, Story, Engine, Dialog, Fullscreen, LoadScreen, Macro, Passage, Save, Setting, Settings, SimpleAudio, Template, UI, UIBar, Config, SugarCube, setup, prehistory, predisplay, prerender, postdisplay, postrender, passage, tags, visited, turns, time, $args.

Type Inference System

Knot silently tracks variable assignments across your entire workspace and builds a complete type model that powers completions, hover information, property access, and type-mismatch diagnostics. You never need to annotate types explicitly.

How It Works

Every <<set $var to/= expr>> assignment is tracked. Knot infers types including number, string, boolean, null, object (with full property trees), and array (with element type). The first assignment determines the type. Passages are analyzed in priority order: StoryInit first, then special passages, then regular passages.

:: StoryInit
<<set $player to {
  name: "Alex",
  hp: 100,
  stats: { mp: 50, strength: 12 }
}>>

:: Later passage
<<print $player.name>>     // Knot knows this is a string
<<if $player.stats.hp gt 80>>  // Knot knows hp is a number

JS Global Type Inference

Knot parses Story JavaScript and inline <<script>> bodies using acorn. It infers types from initializer expressions: var counter = 0 → number, var labels = ["a", "b"] → string[], var config = {debug: true} → {debug: boolean}.

Dynamic Passage Reference Detection

SugarCube allows passage names to be computed at runtime using variables and expressions. Knot handles this through several mechanisms:

Variable-Based Resolution

When a navigation macro uses a variable target, Knot builds a workspace-wide map of variable-to-string assignments:

<<set $destination to "Forest">>
<<goto $destination>>   // Knot resolves $destination → "Forest"

Resolved passages are included in the link graph and unreachable passage analysis.

Implicit Passage Patterns

Knot detects passage references beyond [[links]] and navigation macros:

<div data-passage="Room">        // HTML attribute
Engine.play("Room")            // JS API call
Story.get("Room")              // JS API call

These implicit references are included in reference counts, Find All References, and reachability analysis.

Macro.add() Detection

Knot specifically detects Macro.add() calls in your JavaScript code using acorn AST walking:

Macro.add("addtime", {
  handler: function() { /* ... */ }
});

After detection, <<addtime>> gets full language support: completions, hover, F12 go-to-definition, and Find All References. The established SugarCube pattern using $(document).one(':storyready', function() { registerMacros(); }) is fully supported.

Diagnostics & Error Detection

Knot continuously analyzes your workspace and reports problems as colored underlines. All diagnostic rules are configurable — set severity to error, warning, info, or off.

RuleDefaultWhat It Catches
knot.lint.unknownPassagewarningLinks or macro args referencing a passage that doesn't exist
knot.lint.unknownMacrowarningMacros that aren't built-in, user widgets, or Macro.add() calls
knot.lint.duplicatePassageerrorTwo or more passages sharing the same name
knot.lint.unreachablePassagewarningPassages not reachable from the start passage via BFS
knot.lint.typeMismatcherrorComparison operators with mismatched types (string vs number)
knot.lint.containerStructureerrorInvalid macro nesting (<<elseif>> outside <<if>>, etc.)

Additional Diagnostics (always-on)

CheckSeverityWhat It Catches
Deprecated macrowarningUse of <<click>> (deprecated in favor of <<link>>)
Missing required argumenterror<<goto>> without a passage target, etc.
Assignment target errorerror<<set>> where the left-hand side isn't a valid target
StoryData validationerror/warningMissing IFID, invalid IFID format, start passage doesn't exist
JavaScript syntaxerrorSyntax errors in script passages and <<script>> blocks
Unreachable Passage Analysis

Special passages (StoryInit, StoryCaption, etc.) and system passages (StoryData, Story JavaScript, Story Stylesheet) are always considered reachable. Dynamic references like <<goto $dest>> are resolved by tracking variable assignments to string literals.

Code Actions (Quick Fixes)

Click the lightbulb icon or press Ctrl+. on a diagnostic to get one-click solutions.

Create Missing Passage

When Knot flags an "Unknown passage target" diagnostic, the quick fix creates a new passage stub at the end of the current file with the correct name. Write the link first, then create the passage with one click.

Generate IFID

When your cursor is inside a StoryData passage that lacks an IFID, Knot offers to generate a proper UUID v4 and insert it, preserving existing JSON formatting. This is a context-sensitive action that only appears when actually needed.

Replace Invalid IFID

When StoryData contains an IFID that's not a valid UUID v4, Knot offers to generate a new one and replace the invalid value in-place.

Syntax Highlighting

Knot provides two layers of syntax highlighting for the richest possible visual representation.

TextMate Grammar

The main grammar highlights: passage headers (::), passage names, tags ([script], [stylesheet], [widget], etc.), comments, macro tags and names, story/temp variables, SugarCube operators, links, HTML tags, strings, numbers, and more.

Semantic Tokens

Token TypeVS Code ScopeApplied To
functionentity.name.function.macro.tweeMacros
classentity.name.section.passage.tweePassage names (including passage-arg strings)
variablevariable.other.story.tweeStory/temp variables, property access
operatorkeyword.operator.sugarcube.tweeSugarCube operators (to, eq, gt, etc.)
stringstring.quoted.double.tweeString literals
numberconstant.numeric.tweeNumeric literals
commentcomment.block.tweeBlock/HTML comments

Notably, passage-name strings inside macros like <<goto "Room">> are highlighted as passage tokens rather than generic strings.

Embedded Language Injection

Full JavaScript syntax is injected into: script-tagged passages, Story JavaScript, <<script>> bodies, and expression macros. Full CSS syntax is injected into stylesheet-tagged passages.

Editor Configuration

FeatureBehavior
Bracket auto-close<<>>, [[]], plus standard brackets and quotes
Surrounding pairsSelect text + [[ wraps as [[selected text]]
Smart indentAuto-indent after <<if>>, <<for>>, <<widget>>, etc.
FoldingPassage bodies, block comments, block macros (individually foldable when nested)
On Enter rulesAuto-indent new line after block macro openers

Tweego Build Integration

Knot integrates with the tweego compiler for build, test, and watch functionality directly from VS Code.

Building

Ctrl+Alt+B runs tweego with your configured settings. On success, a notification shows build duration and offers an "Open in browser" button. On failure, structured errors are published as inline diagnostics.

Watch Mode

Start watch mode to auto-rebuild whenever you save a file. The status bar shows a prominent "Watch" indicator. Diagnostics auto-clear on each rebuild.

Verification & Format Listing

knot.verifyTweego confirms your tweego installation. knot.listFormats shows available story formats.

Commands & Shortcuts

CommandShortcutDescription
knot.goToPassageCtrl+Alt+PQuick-pick to navigate to any passage
knot.buildCtrl+Alt+BRun tweego build
knot.buildTestBuild with test mode (-t)
knot.startWatchStart tweego watch mode (-w)
knot.stopWatchStop the watch process
knot.listFormatsList available story formats
knot.verifyTweegoVerify tweego installation
knot.restartRestart the language server
knot.refreshDocumentsRe-index entire workspace
knot.showOutputOpen the Knot output channel
knot.openSettingsOpen knot settings
knot.mainMenuUnified command quick-pick menu

Status Bar

ElementShowsClick Action
LSP StatusSpinner / checkmark + passage count + format / IFID warningOpens main menu
Build ButtonPlay icon / spinner / checkmark / errorTriggers build
Watch ToggleEye icon / "Watch" labelToggles watch mode
SettingsGear iconOpens knot settings

The LSP status indicator shows a warning background when StoryData is missing an IFID, making it impossible to overlook.

Configuration Settings

All settings are prefixed knot. and accessible via VS Code Settings (search "knot") or settings.json. Changes to linting or project settings require a language server restart; Knot prompts you when it detects relevant changes.

Project

knot.project.storyFilesDirectory"src"
Source directory for tweego compilation (relative to workspace)
knot.project.storyFormatsDirectory".storyformats"
Story format packages directory (added to TWEEGO_PATH)
knot.project.include[]
Additional directories to include in workspace indexing
knot.project.exclude[]
Glob patterns to exclude from indexing (adds to built-in excludes)

Tweego

knot.tweego.path"tweego"
Path to the tweego executable
knot.tweego.outputFile"dist/index.html"
Output file path (relative to workspace)
knot.tweego.formatOverride""
Override the story format declared in StoryData
knot.tweego.modulePaths[]
Module directories (-m flag)
knot.tweego.headFile""
File appended to <head> (--head flag)
knot.tweego.noTrimfalse
Disable whitespace trimming (--no-trim flag)
knot.tweego.logFilesfalse
Log processed files (--log-files flag)
knot.tweego.extraArgs""
Extra CLI arguments passed to tweego

Linting

knot.lint.unknownPassage"warning"
Severity for links to undefined passages (error/warning/info/off)
knot.lint.unknownMacro"warning"
Severity for unrecognized macro names
knot.lint.duplicatePassage"error"
Severity for duplicate passage definitions
knot.lint.typeMismatch"error"
Severity for type mismatches in comparisons
knot.lint.unreachablePassage"warning"
Severity for passages unreachable from start
knot.lint.containerStructure"error"
Severity for invalid macro nesting

Under the Hood

Incremental Indexing

Knot uses a 120ms debounced index coordinator. Multiple rapid edits are coalesced into a single reanalysis pass. Concurrent reanalysis is prevented.

File System Watching

A VS Code file watcher monitors .tw/.twee files for external changes. Open file LSP content always wins over disk. Files > 2 MB are skipped.

Incremental Parsing

When you edit a file, only the affected passage is re-parsed; other passage ASTs are reused. The parse cache uses LRU eviction with a 500-file limit.

Format Adapter System

SugarCube 2 provides the full feature set. A fallback adapter provides safe no-op behavior with basic workspace features. Format resolution uses a 4-step strategy: exact match, alias match, prefix match, then fallback. Defaults to SugarCube 2.

Virtual Document Generation

For JavaScript syntax checking, Knot converts Twee markup into a virtual JS document: extracts script bodies, injects runtime prelude, normalizes SugarCube operators (to=, eq===, gt>), and converts $var to State.variables.var. A source map maps virtual offsets back to original positions.

Tips & Tricks

Discovering Context-Sensitive Features

Many of Knot's features are context-sensitive and only appear in specific situations:

Efficient Authoring Workflow

  1. Write links to passages that don't exist yet
  2. Use "Create missing passage" quick fix to generate stubs
  3. Use Ctrl+Alt+P to navigate between passages
  4. Hover over passage names to verify incoming link counts
  5. Use Shift+F12 to audit references before structural changes
  6. Use F2 to rename when refactoring
  7. Enable watch mode for continuous compilation feedback

Macro.add() Best Practices

Define custom macros in your Story JavaScript passage (tagged [script]) for the best Knot experience. The established SugarCube pattern is fully supported:

$(document).one(':storyready', function () {
  registerMacros();
});

function registerMacros() {
  Macro.add('addtime', {
    handler: function () { /* ... */ }
  });
}

Each Macro.add() call is detected and provides completions, hover, and go-to-definition.