Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing

Thanks for helping keep Pine Script highlighting accurate. This document covers the workflow; the README explains how the grammar is put together.
Thanks for helping keep Pine Script support accurate. This document covers the workflow; the README explains what the extension does and how the pieces fit together.

## Setup

Expand All @@ -15,23 +15,30 @@ Node 20 or newer is required.

## Where things live

| Change you want to make | Edit this |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Add or remove a built-in function | `src/data/functions.json` |
| Add or remove a built-in variable | `src/data/variables.json` |
| Add or remove a built-in constant | `src/data/constants.json` |
| Add a compiler annotation (`//@...`) | `src/data/annotations.json` |
| Add a keyword, type or change a scope | `src/grammar.mjs` |
| Change folding, brackets or indentation | `language-configuration.json` |
| Add a snippet | `snippets/pinescript.code-snippets` |
| Completion, hover, commands, inference | `src/extension/core/*` (logic) and `src/extension/providers/*`, `src/extension/commands/*` (VS Code glue) |
| Change a theme color | `themes/*.json` |
| Never | `syntaxes/pinescript.tmLanguage.json`, `src/data/reference.json`, `dist/` (generated) |
| Change you want to make | Edit this |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Add or remove a built-in function | `src/data/functions.json` |
| Add or remove a built-in variable | `src/data/variables.json` |
| Add or remove a built-in constant | `src/data/constants.json` |
| Add a compiler annotation (`//@...`) | `src/data/annotations.json` |
| Add a keyword, type or change a scope | `src/grammar.mjs` |
| Change folding, brackets or indentation | `language-configuration.json` |
| Add a snippet | `snippets/pinescript.code-snippets` |
| Completion, hover, commands, inference | `src/extension/core/*` (logic) and `src/extension/providers/*`, `src/extension/commands/*` (VS Code glue) |
| Change how a document is formatted | `src/extension/core/formatter.ts` |
| Add or change an offline rule | `src/extension/core/lint.ts`, and list the rule in the `pinescript.lint.disabledRules` enum in `package.json` |
| Change a quick fix | `src/extension/core/quick-fix.ts` |
| Definition, references, rename | `src/extension/core/symbols.ts` |
| Colour swatches, semantic tokens | `src/extension/core/colors.ts`, `src/extension/core/semantic.ts` |
| Change a theme color | `themes/*.json` (`tokenColors` for the grammar, `semanticTokenColors` for declared names) |
| Never | `syntaxes/pinescript.tmLanguage.json`, `src/data/reference.json`, `dist/` (generated) |

The data files are grouped by namespace. A function `ta.sma` goes under the `"ta"` key as `"sma"`. Identifiers without a namespace go under the `""` key. Nested namespaces such as `strategy.closedtrades` are their own key.

`src/extension/core` never imports `vscode`; that is what makes it testable with vitest. Providers and commands only translate between VS Code types and core types.

Everything a provider needs from a document goes through `analyze()` in `src/extension/vscode/document-cache.ts`, which parses each document once per version. Anything that walks every identifier of a file should go through `sourceIndex()` in `src/extension/core/symbols.ts` rather than rescanning the token stream: that is what keeps the offline checks linear instead of quadratic on long scripts.

## Working on the extension

```sh
Expand Down Expand Up @@ -68,18 +75,22 @@ Built-in identifiers come from the [Pine Script v6 reference](https://www.tradin
## Manual checklist before a release

- Completion after `ta.`, inside `plot(`, after `//@`, after `import `
- Hover on `close`, `ta.sma`, a user function, an import line
- Hover on `close`, `ta.sma`, a user function, a function parameter, an import line
- Signature help through all parameters of `plot(`
- Outline shows functions, types, enums, variables
- Outline shows functions, types with fields, enums with members, top-level variables
- Go to Definition, Find All References and Rename on a user function, a variable and an enum member
- Format Document on a file with two space indentation and on one with wrapped calls
- Colour swatch and picker on `#FF9800`, `color.red` and `color.new(color.blue, 25)`
- Offline checks flag an old pragma and a bare `sma`; Convert to v6 fixes them
- Generate Docstring on a function, a type and an enum
- Add Type Annotations on `tests/snapshots/strategy-v6.pine`
- Both themes on `tests/snapshots/strategy-v6.pine`
- With `pinescript.diagnostics.remote` on: an undeclared identifier is underlined
- Add Type Annotations on `tests/snapshots/strategy-v6.pine` and on `var a = 1`
- Both themes on `tests/snapshots/strategy-v6.pine`, with semantic highlighting on
- With `pinescript.diagnostics.remote` on: an undeclared identifier is underlined and the lightbulb offers a fix

## Releasing

Maintainers only.

1. Update `CHANGELOG.md` and bump `version` in `package.json`.
2. Commit, then tag: `git tag v3.x.y && git push --tags`.
2. Commit, then tag: `git tag -a v3.x.y -m 3.x.y && git push origin v3.x.y`.
3. The release workflow builds the `.vsix`, attaches it to a GitHub release and publishes to the Marketplace when the `VSCE_PAT` secret is configured.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and

## [Unreleased]

## [3.4.0] - 2026-09-08

A review pass over the whole extension: three defects that produced wrong output, a large performance
fix, and documentation for every exported declaration.

### Fixed

- **Add Type Annotations wrote broken code.** A declaration's column was found by searching the raw line for its name, so `var a = 1` reported column 1 and the command produced `vint ar a = 1`. Columns now come from the tokens. The same column is used by Go to Definition and Rename, which were off by the same amount.
- **A `for` counter was recorded with `for` as its type**, so a value read from it was annotated `for x = i`. Counters are now typed `int`, and `for x in`, `for [index, element] in` declare their names too. All three are scoped to the loop block rather than the rest of the file.
- **The Outline pointed at the wrong lines** for the fields of a type or the members of an enum whenever a comment or a blank line sat between them; every entry was off by one per interleaved line. Fields and members now carry the line they are written on.
- Range formatting on a document with Windows line endings never recognised that nothing had changed, and returned an edit anyway.

### Changed

- The offline checks are 51 times faster on a long script: 160 ms to 3 ms on a 1,500 line document, and now linear rather than quadratic. Semantic highlighting on the same file went from 13 ms to 2 ms. Both used to rescan the token stream for every symbol they looked at; they share one index per document version now.
- Hovering a function parameter shows its declaration and its `//@param` text instead of nothing.
- The Outline lists the declarations at the top level of a script and no longer mixes in variables declared inside blocks.

### Documentation

- Every exported function, class, interface and type in `src/extension` now carries a comment saying what it is for and what its contract is, rather than only the ones with surprising behaviour.
- `CONTRIBUTING.md` covers the formatter, the offline rules, navigation, colours and semantic tokens, and explains where a change that walks every identifier belongs.

## [3.3.0] - 2026-09-08

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "pine-script-syntax-highlighter",
"displayName": "Pine Script Syntax Highlighter",
"description": "Pine Script v6 for Visual Studio Code: highlighting, completion, hover documentation, signature help, snippets, themes and compiler diagnostics.",
"version": "3.3.0",
"version": "3.4.0",
"publisher": "ex-codes",
"author": {
"name": "Yankı Küçük",
Expand Down
2 changes: 2 additions & 0 deletions src/extension/commands/add-type-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { loadReference } from '../core/reference';
import { planTypeAnnotations } from '../core/type-inference';
import { analyze } from '../vscode/document-cache';

/** The types the compiler last reported for a document, when diagnostics are on. */
export type CompilerTypesLookup = (uri: vscode.Uri) => ReadonlyMap<string, string> | undefined;

/** Registers the command that prefixes untyped declarations with their inferred type. */
export function registerAddTypeAnnotations(context: vscode.ExtensionContext, compilerTypes: CompilerTypesLookup): void {
context.subscriptions.push(
vscode.commands.registerCommand('pinescript.addTypeAnnotations', async () => {
Expand Down
1 change: 1 addition & 0 deletions src/extension/commands/convert-to-v6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { analyze } from '../vscode/document-cache';
/** The rules whose fixes together move a script from an older Pine version to the current one. */
const MIGRATION_RULES = new Set(['missing-version', 'old-version', 'legacy-name', 'unknown-argument']);

/** Registers the command that applies every migration fix in a document at once. */
export function registerConvertToV6(context: vscode.ExtensionContext, ref: ReferenceIndex): void {
context.subscriptions.push(
vscode.commands.registerCommand('pinescript.convertToV6', async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/extension/commands/generate-docstring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { annotationBlockRange, docstringLines } from '../core/docstring';
import { declarationAt, type DeclSymbol } from '../core/document-model';
import { analyze } from '../vscode/document-cache';

/** The edit that writes, or completes, the `//@` block for one declaration. */
export function docstringEdit(document: vscode.TextDocument, symbol: DeclSymbol): vscode.TextEdit {
const { lines } = analyze(document);
const indent = lines[symbol.line]!.match(/^\s*/)![0];
Expand All @@ -14,6 +15,7 @@ export function docstringEdit(document: vscode.TextDocument, symbol: DeclSymbol)
: vscode.TextEdit.insert(new vscode.Position(symbol.line, 0), text);
}

/** Registers the command and its lightbulb action for documenting a declaration. */
export function registerGenerateDocstring(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.commands.registerCommand('pinescript.generateDocstring', async (line?: number) => {
Expand Down
1 change: 1 addition & 0 deletions src/extension/commands/new-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const TITLES: Record<TemplateKind, string> = {
};
const LABELS: Record<TemplateKind, string> = { indicator: 'Indicator', strategy: 'Strategy', library: 'Library' };

/** Registers the three commands that open a new script from a template. */
export function registerNewFileCommands(context: vscode.ExtensionContext): void {
for (const kind of ['indicator', 'strategy', 'library'] as const) {
context.subscriptions.push(
Expand Down
1 change: 1 addition & 0 deletions src/extension/commands/open-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import { REFERENCE_URL, loadReference } from '../core/reference';
import { wordAt } from '../core/tokenizer';

/** Registers the command that opens the reference at the built-in under the cursor. */
export function registerOpenReference(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.commands.registerCommand('pinescript.openReference', async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/extension/core/call-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Token, TokenizedLine } from './tokenizer';

/** The call the cursor sits inside, and which argument it is on. */
export interface CallInfo {
name: string;
argIndex: number;
Expand Down Expand Up @@ -28,6 +29,10 @@ function tokensBefore(tokenLines: TokenizedLine[], line: number, col: number): T
return out;
}

/**
* Finds the innermost call the cursor is inside, looking back over wrapped lines. Grouping
* parentheses and index brackets are stepped over, so their commas do not count as arguments.
*/
export function enclosingCall(tokenLines: TokenizedLine[], line: number, col: number): CallInfo | null {
const tokens = tokensBefore(tokenLines, line, col);
let depth = 0;
Expand Down
1 change: 1 addition & 0 deletions src/extension/core/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Rgba {
alpha: number;
}

/** One colour found in the document, and the range the picker should replace. */
export interface ColorSpot {
line: number;
startCol: number;
Expand Down
2 changes: 2 additions & 0 deletions src/extension/core/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { enclosingCall, type CallInfo } from './call-resolver';
import { isInStringOrComment, type TokenizedLine } from './tokenizer';

/** What the cursor is in the middle of typing, which decides what completion offers. */
export type CompletionContext =
| { kind: 'none' }
| { kind: 'annotation'; prefix: string }
Expand All @@ -14,6 +15,7 @@ const RE_IMPORT = /^\s*import\s+([\w\-/.]*)$/;
const RE_MEMBER = /([A-Za-z_][\w.]*)\.(\w*)$/;
const RE_WORD = /(\w*)$/;

/** Works out what is being typed at a position. */
export function completionContext(
tokenLines: TokenizedLine[],
lineText: string,
Expand Down
3 changes: 3 additions & 0 deletions src/extension/core/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CompileResult, RawIssue } from './pine-facade';

/** One compiler issue placed on the document, with its message resolved. */
export interface CompileDiagnostic {
line: number;
startCol: number;
Expand All @@ -24,6 +25,7 @@ function one(issue: RawIssue, severity: 'error' | 'warning', lineCount: number):
return { line, startCol, endCol, message: render(issue), severity, code: issue.code ?? null, ctx: issue.ctx };
}

/** Converts a compile result to diagnostics, clamping positions to the document. */
export function toDiagnostics(result: CompileResult, lineCount: number): CompileDiagnostic[] {
const out = [
...result.errors.map((e) => one(e, 'error', lineCount)),
Expand All @@ -43,6 +45,7 @@ export function toDiagnostics(result: CompileResult, lineCount: number): Compile
return out;
}

/** The types the compiler inferred for each variable, which beats guessing them. */
export function compilerVariableTypes(result: CompileResult): Map<string, string> {
return new Map(result.variables.map((v) => [v.name, v.type]));
}
3 changes: 3 additions & 0 deletions src/extension/core/docstring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DeclSymbol, FunctionSymbol, LineRange } from './document-model';
const VOID_CALLS =
/^(?:plot|plotshape|plotchar|plotarrow|plotcandle|plotbar|bgcolor|barcolor|fill|hline|alert|alertcondition|log\.\w+|runtime\.error|strategy\.(?:entry|exit|close|close_all|order|cancel|cancel_all|risk\.\w+)|label\.set_\w+|line\.set_\w+|box\.set_\w+|table\.(?:cell|set_\w+|clear|merge_cells)|array\.(?:push|set|unshift|clear|insert|fill|sort|reverse)|matrix\.(?:set|fill|add_row|add_col|remove_row|remove_col)|map\.(?:put|remove|clear))\s*\(/;

/** The `//@` block above a declaration, or null when the comments above it carry no annotation. */
export function annotationBlockRange(lines: string[], declLine: number): LineRange | null {
let start = declLine;
while (start > 0 && /^\s*\/\//.test(lines[start - 1]!)) start--;
Expand All @@ -24,6 +25,7 @@ function bodyReturnsValue(fn: FunctionSymbol): boolean {
return !fn.lastLine || !VOID_CALLS.test(fn.lastLine);
}

/** Builds the documentation block for a declaration, keeping whatever the author already wrote. */
export function docstringLines(symbol: DeclSymbol, existing: string[], indent: string): string[] {
const wanted: { tag: string; name: string | null }[] = [];
if (symbol.kind === 'function') {
Expand Down Expand Up @@ -57,6 +59,7 @@ export function docstringLines(symbol: DeclSymbol, existing: string[], indent: s
return out;
}

/** True when a declaration is missing part of its documentation, which is what offers the fix. */
export function needsDocstring(symbol: DeclSymbol): boolean {
if (symbol.kind === 'function') {
if (!symbol.docs.function && !symbol.docs.description) return true;
Expand Down
Loading
Loading