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
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ A modular PineScript interpreter written in Rust.

Pinecone executes PineScript code (TradingView's scripting language) with support for technical analysis, custom indicators, and strategy backtesting. The interpreter is designed to be extensible - you can add custom builtin functions and output types to integrate with your own systems.

Pinecone comes in two parts:

- **Pinecone SDK** — the set of Rust crates below (interpreter, parser, formatter, linter, language server), used as a library. `pine-lang` is the main entry point.
- **[`pinecone` binary](#pinecone-binary)** — a command-line tool built on the SDK: format, lint and check scripts, and run the language server for editors.

## Features

- PineScript v4 and v5 language support
Expand All @@ -13,14 +18,37 @@ Pinecone executes PineScript code (TradingView's scripting language) with suppor
- Modular output system - extend with [custom types and builtins](examples/custom-builtin-func)
- Type-safe generic architecture

## Install
## Pinecone binary

Install the latest release with `up.sh`:

```sh
curl -fsSL https://raw.githubusercontent.com/ferranbt/pinecone/main/up.sh | bash
```

It provides these commands:

| Command | Description |
| --- | --- |
| `pinecone format <paths>` | Format scripts in place (`--stdout`, `--check`). |
| `pinecone lint <paths>` | Report lint findings (repainting, lookahead, …). |
| `pinecone check <paths>` | Parse, semantically analyze and lint. |
| `pinecone lsp` | Run the language server over stdio, for editor integration. |

Paths may be files or directories (searched for `.pine` files).

`pinecone lsp` starts a language server — diagnostics, formatting, hover, go-to-definition, find references, document symbols, rename and completion, resolved across imported libraries. It powers the [VS Code extension](editors/vscode).

## Pinecone SDK

### Install

```toml
[dependencies]
pine-lang = "0.1"
```

## Example
### Example

A script is replayed over a whole series of bars — series history and indicator
state build up as they execute.
Expand Down
78 changes: 78 additions & 0 deletions crates/pine-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ impl LanguageServer for Backend {
references_provider: Some(OneOf::Left(true)),
document_symbol_provider: Some(OneOf::Left(true)),
document_highlight_provider: Some(OneOf::Left(true)),
rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
})),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
Expand Down Expand Up @@ -280,6 +284,72 @@ impl LanguageServer for Backend {
Ok(highlights.filter(|h| !h.is_empty()))
}

async fn prepare_rename(
&self,
params: TextDocumentPositionParams,
) -> jsonrpc::Result<Option<PrepareRenameResponse>> {
let range = {
let documents = self.documents.lock().unwrap();
documents.get(&params.text_document.uri).and_then(|doc| {
let symbols = doc.symbols.as_ref()?;
let id = symbol_at(symbols, &doc.text, params.position)?;
// The identifier under the cursor in this document.
let line = doc
.text
.lines()
.nth(params.position.line as usize)
.unwrap_or("");
let start = identifier_start(line, params.position.character as usize) as u32;
let width = symbols.symbol(id).name.chars().count() as u32;
let at = Position::new(params.position.line, start);
Some(Range::new(at, Position::new(at.line, start + width)))
})
};
Ok(range.map(PrepareRenameResponse::Range))
}

// `Uri`'s interior mutability is a parse cache that doesn't affect its hash;
// `WorkspaceEdit.changes` is keyed by `Uri` in lsp_types regardless.
#[allow(clippy::mutable_key_type)]
async fn rename(&self, params: RenameParams) -> jsonrpc::Result<Option<WorkspaceEdit>> {
let new_name = params.new_name;
if !is_identifier(&new_name) {
return Err(jsonrpc::Error::invalid_params(format!(
"`{new_name}` is not a valid name"
)));
}
let at = params.text_document_position;
let request_uri = at.text_document.uri;
let changes = {
let documents = self.documents.lock().unwrap();
documents.get(&request_uri).and_then(|doc| {
let symbols = doc.symbols.as_ref()?;
let id = symbol_at(symbols, &doc.text, at.position)?;
let width = symbols.symbol(id).name.chars().count() as u32;
let mut sites: Vec<(FileId, u32, u32)> = Vec::new();
if let Some(decl) = symbols.declaration_location(id) {
sites.push(decl);
}
sites.extend(symbols.references(id));
let mut changes: HashMap<Uri, Vec<TextEdit>> = HashMap::new();
for (file, line, column) in sites {
if let Some(uri) = file_uri(&request_uri, symbols, file) {
changes.entry(uri).or_default().push(TextEdit {
range: name_range(line, column, width),
new_text: new_name.clone(),
});
}
}
(!changes.is_empty()).then_some(changes)
})
};
Ok(changes.map(|changes| WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}))
}

async fn completion(
&self,
params: CompletionParams,
Expand Down Expand Up @@ -329,6 +399,14 @@ fn file_uri(request: &Uri, symbols: &SymbolTable, file: FileId) -> Option<Uri> {
Uri::from_file_path(path)
}

/// Whether `name` is a valid Pine identifier — the constraint a rename target
/// must meet before edits are produced.
fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
chars.next().is_some_and(|c| c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || c == '_')
}

/// The `width`-character range of a name at a 1-based `(line, column)`.
fn name_range(line: u32, column: u32, width: u32) -> Range {
let start = Position::new(line - 1, column - 1);
Expand Down
29 changes: 19 additions & 10 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,33 @@ powered by the [`pinecone`](https://github.com/ferranbt/pinecone) toolchain.
`lookahead` bias, and intrabar strategy recalculation.
- **Formatting** — format a document with the standard *Format Document*
command (Shift+Alt+F).
- **Hover** — signatures and kinds for your variables, functions and types.
- **Go to definition** (F12) for symbols declared in the file.
- **Hover** — signatures and kinds for your variables, functions and types; for
a function, its declaration and every call site.
- **Completion** — fields, enum cases and library exports after `.`, plus
builtin namespace members (`ta.`, `math.`, …) with their signatures.
- **Go to definition** (F12) and **find all references** (Shift+F12), resolved
into imported libraries.
- **Document outline** and breadcrumbs (Ctrl+Shift+O).
- **Highlight occurrences** of the symbol under the cursor.
- **Rename** (F2) across every occurrence, including imported libraries.

## Requirements

The extension talks to the `pinecone` language server, so that binary needs to
be available:
The extension talks to the `pinecone` language server. If it isn't found, the
extension offers to **download** it for you. Otherwise, make it available by
either:

- Download a prebuilt `pinecone` from the
[releases](https://github.com/ferranbt/pinecone/releases) (or build it from
source with `cargo build --release -p pinecone`), and
- put it on your `PATH`, **or** point the extension at it with the
`pinecone.server.path` setting.
- putting a `pinecone` binary on your `PATH` (install it with
[`up.sh`](https://github.com/ferranbt/pinecone#pinecone-binary), download a
prebuilt one from the
[releases](https://github.com/ferranbt/pinecone/releases), or build it with
`cargo build --release -p pinecone`), **or**
- pointing the extension at it with the `pinecone.server.path` setting.

## Extension settings

| Setting | Default | Description |
| ---------------------- | ---------- | ------------------------------------------------------- |
| `pinecone.server.path` | `pinecone` | Path to the `pinecone` executable used as the server. |

Server logs are available under **View → Output → Pinecone**.
Server logs are available under **View → Output → Pinecone**.
41 changes: 41 additions & 0 deletions editors/vscode/src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,47 @@ suite("pinecone language server", () => {
assert.deepStrictEqual(lines, [2, 3], JSON.stringify(lines));
});

test("renames a symbol across its occurrences", async () => {
const uri = fixture("symbols.pine");
await open(uri);

// On the `double` declaration on line 3.
const edit = await vscode.commands.executeCommand<vscode.WorkspaceEdit>(
"vscode.executeDocumentRenameProvider",
uri,
new vscode.Position(2, 0),
"triple"
);
const edits = edit.get(uri);
const lines = edits.map((e) => e.range.start.line).sort();
// Declaration (line 3) and call (line 4), 0-based, both renamed.
assert.deepStrictEqual(lines, [2, 3], JSON.stringify(lines));
assert.ok(edits.every((e) => e.newText === "triple"));
});

test("renames across an imported library", async () => {
const uri = fixture("imports.pine");
await open(uri);

// On `add` in the call `lib.add(1, 2)`.
const edit = await vscode.commands.executeCommand<vscode.WorkspaceEdit>(
"vscode.executeDocumentRenameProvider",
uri,
new vscode.Position(5, 8),
"sum"
);
const paths = edit.entries().map(([u]) => u.path);
// The edit spans both the caller and the library that declares `add`.
assert.ok(
paths.some((p) => p.endsWith("imports.pine")),
paths.join(", ")
);
assert.ok(
paths.some((p) => p.endsWith("mylib.pine")),
paths.join(", ")
);
});

test("completes an object's fields", async () => {
const uri = fixture("completion.pine");
await open(uri);
Expand Down
Loading