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.
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.
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:
| Element | Navigates 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 $health | First <<set $health ...>> assignment |
| JS global identifier | Declaration 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:
- Passages:
[[links]], passage-arg macro calls, implicit refs (data-passage,Engine.play()) - Variables: every
<<set>>,<<if>>,<<print>>, and other usage - Macros/widgets: every call site across all files
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:
| Target | What gets updated |
|---|---|
| Passage | [[OldName]] → [[NewName]], <<goto "OldName">> → <<goto "NewName">>, header :: OldName → :: NewName |
| Variable | Every <<set $oldVar>> and usage site |
| Widget/Macro | Definition <<widget "oldName">> and all call sites <<oldName>> |
Clickable Document Links
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.
| Element | What Hover Shows |
|---|---|
| Built-in macro | Name, 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 name | Name, 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 global | API description (State, Engine, Story, etc.) |
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.
| Rule | Default | What It Catches |
|---|---|---|
knot.lint.unknownPassage | warning | Links or macro args referencing a passage that doesn't exist |
knot.lint.unknownMacro | warning | Macros that aren't built-in, user widgets, or Macro.add() calls |
knot.lint.duplicatePassage | error | Two or more passages sharing the same name |
knot.lint.unreachablePassage | warning | Passages not reachable from the start passage via BFS |
knot.lint.typeMismatch | error | Comparison operators with mismatched types (string vs number) |
knot.lint.containerStructure | error | Invalid macro nesting (<<elseif>> outside <<if>>, etc.) |
Additional Diagnostics (always-on)
| Check | Severity | What It Catches |
|---|---|---|
| Deprecated macro | warning | Use of <<click>> (deprecated in favor of <<link>>) |
| Missing required argument | error | <<goto>> without a passage target, etc. |
| Assignment target error | error | <<set>> where the left-hand side isn't a valid target |
| StoryData validation | error/warning | Missing IFID, invalid IFID format, start passage doesn't exist |
| JavaScript syntax | error | Syntax errors in script passages and <<script>> blocks |
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 Type | VS Code Scope | Applied To |
|---|---|---|
| function | entity.name.function.macro.twee | Macros |
| class | entity.name.section.passage.twee | Passage names (including passage-arg strings) |
| variable | variable.other.story.twee | Story/temp variables, property access |
| operator | keyword.operator.sugarcube.twee | SugarCube operators (to, eq, gt, etc.) |
| string | string.quoted.double.twee | String literals |
| number | constant.numeric.twee | Numeric literals |
| comment | comment.block.twee | Block/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
| Feature | Behavior |
|---|---|
| Bracket auto-close | << → >>, [[ → ]], plus standard brackets and quotes |
| Surrounding pairs | Select text + [[ wraps as [[selected text]] |
| Smart indent | Auto-indent after <<if>>, <<for>>, <<widget>>, etc. |
| Folding | Passage bodies, block comments, block macros (individually foldable when nested) |
| On Enter rules | Auto-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
| Command | Shortcut | Description |
|---|---|---|
| knot.goToPassage | Ctrl+Alt+P | Quick-pick to navigate to any passage |
| knot.build | Ctrl+Alt+B | Run tweego build |
| knot.buildTest | — | Build with test mode (-t) |
| knot.startWatch | — | Start tweego watch mode (-w) |
| knot.stopWatch | — | Stop the watch process |
| knot.listFormats | — | List available story formats |
| knot.verifyTweego | — | Verify tweego installation |
| knot.restart | — | Restart the language server |
| knot.refreshDocuments | — | Re-index entire workspace |
| knot.showOutput | — | Open the Knot output channel |
| knot.openSettings | — | Open knot settings |
| knot.mainMenu | — | Unified command quick-pick menu |
Status Bar
| Element | Shows | Click Action |
|---|---|---|
| LSP Status | Spinner / checkmark + passage count + format / IFID warning | Opens main menu |
| Build Button | Play icon / spinner / checkmark / error | Triggers build |
| Watch Toggle | Eye icon / "Watch" label | Toggles watch mode |
| Settings | Gear icon | Opens 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
Tweego
Linting
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:
- IFID generation only appears when your cursor is inside a StoryData passage lacking an IFID
- Passage name completions only appear inside macro string arguments
- Property completions only appear after typing a dot following a variable
- Close-tag suggestions only appear when you type
<</ - Hover over anything is almost always informative — try it!
Efficient Authoring Workflow
- Write links to passages that don't exist yet
- Use "Create missing passage" quick fix to generate stubs
- Use Ctrl+Alt+P to navigate between passages
- Hover over passage names to verify incoming link counts
- Use Shift+F12 to audit references before structural changes
- Use F2 to rename when refactoring
- 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.