Formatting
Document, range, and on-type formatting via AST parse → NodeUnparser round-trip.
This page
Abstract
Formatting is canonical unparse, not a style linter. The handler parses the buffer, runs NodeUnparser, and if the result differs from the source, replaces the document (or the requested line range, or the just-completed line) with a single TextEdit. There is no prettier-like option matrix; FormattingOptions from the client are currently unused.
Safety rule: the ASDL AST keeps Pine annotations (//@version, //@function, …) but not ordinary prose or block comments. If the buffer contains a comment the unparser would drop, the handler switches to a safe whitespace pass: trailing spaces/tabs are removed and the final newline is normalized, while every comment and string is kept byte-for-byte. Lines inside multi-line strings are never trimmed. Range and on-type formatting apply the same pass to their target line. C002 does not offer a format QuickFix for such buffers because whitespace cleanup cannot shorten a long line. A full comment-preserving CST formatter is future work.
Conceptual model
Rendering…
Interface surface
| Method | Handler |
|---|---|
textDocument/formatting | handle_formatting |
textDocument/rangeFormatting | handle_range_formatting |
textDocument/onTypeFormatting | handle_on_type_formatting |
Capabilities (config.py): both document providers True; on-type advertised as documentOnTypeFormattingProvider with firstTriggerCharacter = "\n".
Full document
- Parse with filename
"<format>". formatted = NodeUnparser().visit(tree).- If identical to source →
[]. - Else one
TextEditspanning(0,0)→ last line/col withnew_text=formatted.
Range
- Unparse the entire document.
- Slice formatted lines and source lines by the client range’s start/end line indices.
- If the in-range slices match →
[]. - Else replace
params.rangewith the formatted line slice.
This is line-aligned, not AST-node-aligned: partial mid-line ranges may produce surprising splices when unparse changes line structure.
On-type (after \n)
After the client inserts a newline, params.position is the cursor on the new line and params.ch is "\n". The handler formats the previous line (or line 0) via the same NodeUnparser and, when that line differs, returns one TextEdit covering only that source line.
- Empty source,
ch != "\n", ortree=None(workspace parse already failed) →[]. - Unparse the whole document.
target = max(0, params.position.line - 1).- If the target is past either line list, or the formatted line equals the source line →
[]. - Else replace
[target, 0) → [target, len(source_line))with the formatted line.
Triggers other than newline are ignored. The advertised trigger is "\n".
Internals
| Path | Role |
|---|---|
features/formatting.py | LSP handlers |
src/pynescript/ast/helper.py | parse |
src/pynescript/ast/unparser.py | NodeUnparser |
VS Code command pynescript.formatDocument delegates to editor.action.formatDocument, which hits this provider when the language client is active. Format-on-type is the client’s onTypeFormatting request after Enter.
Invariants and edge cases
- Parse failure →
[](no partial format; silent no-op from the client’s perspective). On-type also no-ops when the workspace already failed to parse (tree=None). - Idempotence goal: format twice should stabilize if unparse is canonical; any non-idempotent unparse is a core bug, not an LSP bug.
- Comments / trivia: Pine annotations are preserved; prose/block comments switch to the safe whitespace pass (never a destructive edit). This is not a full CST pretty-printer.
- Range and on-type require a full successful parse and matching line counts; only the edit span is clipped. If unparse changes line structure, no range/on-type edit is returned.
- Tab size / insert spaces from
FormattingOptionsare ignored today. - On-type is previous-line only — the new empty line under the cursor is not rewritten.
Worked example
Before:
//@version=6
indicator("x")
plot( close )
After full format (illustrative — exact whitespace follows NodeUnparser):
//@version=6
indicator("x")
plot(close)
If source already matches unparse output, the server returns an empty edit list and the editor leaves the buffer alone.
On-type: typing Enter after a compact assignment length=14 (cursor now at the start of the next line) yields one edit on that assignment line: length = 14.
Failure modes
| Symptom | Cause |
|---|---|
| Format does nothing on broken syntax | Expected: exception → [] |
| Range format rewrites wrong lines | Unparse shifted line count; prefer full-document format |
| On-type after Enter does nothing | Unknown trigger, unparsed buffer, or the previous line already matches unparse |
| Client “format on save” loops | Unparse not idempotent — fix unparser, not client |