Writerly VSCode Extension
Writerly is a markup language extension for VS Code that makes creating structured documents easier. This extension provides syntax highlighting, error reporting, and navigation tools for Writerly (.wly) files.
Features
Open and Preview Files & Images
- Open any file under your cursor with the default application by using
Ctrl+Shift+O (or Cmd+Shift+O on Mac)
- Hover over resolved image filenames to preview supported image files
- Get file path suggestions in supported path-bearing attributes and Markdown links
Navigate Your Documents
- Create links between sections using
>>MyRef handle usages
- Define handles with
handle=MyRef attributes or in text with MyRef##<<
- Jump to any link with
F12
- Rename links everywhere with
F2
- See warnings about unused handle definitions
- Inspect the current file's Writerly document tree from the status bar or the
Writerly: Inspect Document Tree command
Smart Writing Help
- Get diagnostics for indentation, tabs in initial whitespace, tag syntax, empty tags, and code block structure
- See diagnostics for undefined handles, duplicate handle definitions, and invalid handle names
- Auto-complete file paths in supported path contexts
Manage Files From the Editor
- Rename the file referenced under your cursor and update matching path text in Writerly files
- Move the file under your cursor to another directory in the workspace and update matching path text in Writerly files
- Move multiple selected file references at once when they share the same source directory
- Use
F2 / Rename Symbol on the filename part of a file path to rename the referenced file
- Use
F2 / Rename Symbol on the directory part of a file path to move the referenced file
- Create a new file from a template by placing your cursor on a not-yet-existing file path
Handles
Handle definitions are indexed across the active Writerly file set. A handle can
be defined as an attribute line after a tag:
|> section
handle=MyRef
It can also be defined inline in text:
MyRef##<<
At a normal text boundary, decorators follow the handle name:
MyRef#page1#page2##<<
To insert a definition directly after other text without a boundary, use the
explicit ## opener:
blabla##MyRef#page1#page2##<<
Only MyRef is indexed as the handle; the opener and decorators are excluded
from navigation and rename ranges.
An in-text definition may carry an arbitrary value by closing it with >>##
on the same line:
##MyRef#page1##<<arbitrary value | with brackets (like these)>>##
The value is the text between ##<< and the first unescaped >>##. The same
backslash-parity rule used for >> usages applies to value closers. If no
unescaped >>## occurs on the same line, the value is empty and text after
##<< remains ordinary text. Value closers are consumed eagerly: their
trailing ## cannot also open another definition. The position immediately
after an active closer acts as a normal definition boundary, allowing compact
chaining:
##one##<<first value>>##two##<<second value>>##
A space or separate explicit opener remains valid but is not required.
The extension assigns confirmed nonempty value text a dedicated TextMate scope,
string.unquoted.handle-value.writerly, so themes can color it separately from
the handle name, decorators, and delimiters.
The complete in-text definition grammar is also recognized inside fenced code
block content. This allows removable definition annotations to identify code
lines while retaining values, decorators, chaining, escaping, navigation,
rename, and diagnostics:
```python
##calculation-line#page##<<line metadata>>##
result = calculate_total(items)
```
Definitions are not recognized on the opening or closing fence. Handle usages
such as >>calculation-line remain literal code inside fenced blocks.
Handle usages use >>:
See >>MyRef
To write an in-text handle operator literally, place a backslash immediately
before the operator:
MyRef\##<<
See \>>MyRef
An in-text definition requires ##<< to be directly adjacent to the handle name
or final decorator. Therefore, any backslash before ##<< prevents definition
recognition. For usages, an odd-length run of consecutive backslashes escapes
>>, while an even-length run, including zero backslashes, leaves it active.
Only consecutive backslashes immediately before >> are counted. The extension
preserves escape backslashes in the source; removing an escape in rendered output
is the responsibility of the later Writerly processing pipeline.
F12 goes to a single unambiguous definition. F2 renames matching definitions
and usages in the same document tree. Undefined usages, duplicate definitions,
invalid names, and optionally unused definitions are reported as diagnostics.
Writerly Document Trees
For editor features, the extension groups .wly files into document trees using
the same directory shape as Writerly assembly.
Two different Writerly files belong to the same extension document tree when
there is some directory that contains both files as part of its .wly subtree:
- The directory must contain at least one direct
.wly file.
- Both files must be
.wly descendants of that directory.
__parent.wly is not required for document-tree membership. It affects the
assembled structure by making descendant files appear nested under that parent
file.
- A single
.wly file also has its own document scope.
Path segments starting with # are treated as commented-out islands inside the
same document tree:
- Duplicate handles conflict only within the same island.
- Go to definition uses the nearest accessible island, meaning the current
island or an ancestor island.
- Definitions found only in other
# islands are reported as inaccessible.
- Handle rename still applies across the whole document tree.
Available Commands
writerly.openUnderCursorWithDefault - Open file under cursor with system default
writerly.openUnderCursorWithVSCode - Open file under cursor with VS Code
writerly.openUnderCursorAsImageWithVSCode - Open image under cursor with VS Code
writerly.openFileWithDefault - Open current file with system default
writerly.renameFileUnderCursor - Rename the file under the cursor and update references
writerly.moveFileUnderCursor - Move the file or same-directory multi-cursor files under selection to another workspace directory
writerly.createFileUnderCursorFromTemplate - Create a file from a matching template
writerly.restart - Restart the extension
Creating Files From Templates
The writerly.createFileUnderCursorFromTemplate command, shown as Writerly: Create File From Template, lets you scaffold a new file directly from a path written in your document. Place the cursor on a file path that does not exist yet and run the command. The extension will:
- Resolve the directory portion of the path to a unique directory in the workspace (it aborts if the directory is ambiguous or missing, or if a file with that name already exists). Bare directory paths are suffix-matched. Paths beginning with
./ are resolved relative to the workspace folder containing the active Writerly document.
- Look up your configured template files directory (see below).
- Collect every template file (recursively) sharing the same extension as the new file.
- Pick the template whose name shares the longest suffix with the new file name.
- Copy that template to the new location, while renaming it to the new file name.
Configuring the template directory
The command requires the writerly.templateFilesDirectory setting, which points to a directory in your workspace that holds your template files. Bare paths are matched against directory suffixes in the workspace and must resolve to exactly one directory (the command aborts otherwise). A leading ./ makes the path relative to the workspace folder containing the active Writerly document.
This setting is workspace-scoped — You can set it in the workspace's .vscode/settings.json:
{
"writerly.templateFilesDirectory": "./templates"
}
Path Resolution
Under-cursor open, hover, rename, move, and create-from-template commands
resolve path text against files or directories in the workspace.
- File operations use the path text under the cursor to find matching files in
the workspace.
- If exactly one file matches, that file is used.
- If multiple files match, Writerly resolves the target by choosing the unique
closest match relative to the active document's nearest Writerly container.
- If multiple matches tie as closest, the operation aborts and reports the
matching paths.
- Hover requires one unique matching file. If the path is missing or ambiguous,
no hover is shown.
- Directory prompts for create-from-template use the same container-distance
disambiguation rule.
- Move destination directories also ignore same-named directories that are
closer to another topmost Writerly root.
- Bare directory paths are suffix-matched anywhere in the workspace.
- Directory paths beginning with
./ are resolved relative to the workspace
folder containing the active Writerly document.
Reference updates after file rename/move use matching text replacement in
Writerly files:
- all active Writerly files in the workspace are scanned
- candidate matches are literal matches of the old reference string
- if a candidate's closest resolved file is different from the original target,
that candidate is left unchanged
- if a candidate ties between multiple closest matches, the whole rename/move
operation aborts before applying edits
- if a candidate resolves closest to the same original target, it is rewritten
- each changed document is updated with a full-document text edit
- replacements are not parser-aware and are not limited to specific attributes
File Path Completion
Path completion is offered in:
- Writerly attribute values whose attribute name ends with
src
- these exact attribute names:
original, href, srcset, poster, data,
background, icon, favicon, image, logo, thumbnail, preview,
cover, file, path, url, uri, source, use
- Markdown link and image paths in text lines, such as
[label](https://github.com/vistuleB/writerly-vscode-extension/blob/HEAD/path/to/file.txt) and 
All workspace files are indexed, excluding hidden folders, node_modules, and
build output folders. Supported image files show a preview in the completion
documentation popup.
Diagnostics And Language Behavior
The extension reports diagnostics for:
- indentation that is too deep, too low, or not a multiple of four spaces
- tabs in initial whitespace
- empty tags
- invalid tag names
- code block openings inside code blocks
- unclosed code blocks
- spaces in code block info annotations
- more than 100 spaces after
= before a non-empty attribute value
- invalid handle names
- undefined handle usages
- duplicate handle definitions in the same hash island
- unused handle definitions, when enabled
Language configuration:
!! is the line comment marker.
- Pressing Enter after a
|> line auto-indents the next line.
{}, [], (), and "" are configured as auto-closing/surrounding pairs.
- Folding is indentation-based.
File Association Settings
Writerly contributes .wly as its language extension. To make .wly open as
another language in user or workspace settings, use VS Code file associations:
{
"files.associations": {
"*.wly": "plaintext"
}
}
To force .wly back to Writerly in a workspace where another association wins:
{
"files.associations": {
"*.wly": "writerly"
}
}
Other Settings
Unused handle warnings are enabled by default:
{
"writerly.enableUnusedHandleWarnings": true
}
Set it to false to keep handle diagnostics enabled while suppressing unused
definition warnings.