Skip to content

Use StatementRangeSyntaxError#919

Open
DavisVaughan wants to merge 6 commits intoquarto-dev:mainfrom
DavisVaughan:feature/statement-range-rejection
Open

Use StatementRangeSyntaxError#919
DavisVaughan wants to merge 6 commits intoquarto-dev:mainfrom
DavisVaughan:feature/statement-range-rejection

Conversation

@DavisVaughan
Copy link
Collaborator

@DavisVaughan DavisVaughan commented Feb 18, 2026

Follow up to posit-dev/positron#11907

It feels to me like we should still use the "old" hooks.d.ts system for this, because StatementRange is already tied up in that hooks.d.ts file. Mixing hooks.d.ts and types from @posit-dev/positron doesn't feel like it would be a very good idea for this extension of an existing feature, IIUC.

We version guard on Positron 2026.03.0 rather than bumping the required version of Positron

Here's a test in the Positron 2026.04.0 build 85 daily build:

Screen.Recording.2026-03-11.at.2.14.01.PM.mov

To resolve

```
quarto:dev: ✘ [ERROR] Could not resolve "positron"
quarto:dev:
quarto:dev:     src/host/hooks.ts:19:23:
quarto:dev:       19 │ import * as hooks from 'positron';
```
DavisVaughan added a commit to posit-dev/positron that referenced this pull request Feb 24, 2026
Addresses #8350

- Requires posit-dev/ark#1040
- Blocks quarto-dev/quarto#919

Builds on:
- quarto-dev/quarto#914
- posit-dev/ark#1030

# Summary

This PR adds a new throwable `StatementRangeSyntaxError` to
`positron.d.ts`.

```ts
/**
 * An error thrown by a {@link StatementRangeProvider} to indicate that a statement range
 * cannot be provided due to a syntax error in the document.
 */
export class StatementRangeSyntaxError extends Error {
	/**
	 * Zero indexed line number where the syntax error occurred.
	 */
	readonly line?: number;

	/**
	 * Creates a new statement range syntax error.
	 *
	 * @param line Zero indexed line number where the syntax error occurred.
	 */
	constructor(line?: number);
}
```

This is the only public facing API change from this PR.

The idea is that from extension land (`positron-r`, `positron-python`,
and `quarto`) you should be able to throw a `StatementRangeSyntaxError`
from your `provideStatementRange()` provider, with an optional `line`
number of where the syntax error _roughly_ starts. This gets propagated
up through the main thread and into our `Cmd+Enter` handling in
`positronConsoleActions.ts`, where we now detect this special case and:
- Emit an info level notification telling the user that we can't execute
code due to a syntax error. If the `line` number is present, we give
them a button to jump to that line.
- Bail from `Cmd+Enter` handling altogether. We don't even do
one-line-at-a-time execution. #8350 has proved that this is just too
confusing when you are trying to execute code after a syntax error.

Here is what the end result looks like in an R script:


https://github.com/user-attachments/assets/23dca240-06af-40d5-8b1b-a8b24af99259

Note how you can execute R code _above_ the first syntax error, thanks
to posit-dev/ark#1030. It is only _below_ the
first syntax error that we emit this notification and refuse to execute.

It even works in roxygen2 comments where we have a little "subdocument":


https://github.com/user-attachments/assets/ec920753-400d-4493-b534-188dcf4e2a65

And lastly, it works in Quarto, where we have a "virtual document" for
R/Python and map the `line` number from that virtual document back into
the original Qmd, thanks to
quarto-dev/quarto#919 (note that if you try to
run that Quarto PR, you need to remove the version guard on Positron
2026.03.0 manually).

Additionally, quarto-dev/quarto#914 ensures that
you can use StatementRange in any syntax-error-free cell, even if some
other cell in the document has a syntax error.


https://github.com/user-attachments/assets/a01b9ed1-22fe-4169-b088-a8393800d9db

# Design

I considered many different designs along the way, but landed on a model
of:

- Extension host side _models errors as throwable errors_, i.e. with
`StatementRange` and `throw StatementRangeSyntaxError`

  ```ts
  export interface StatementRange {
    readonly range: vscode.Range;
    readonly code?: string;
  }

  export class StatementRangeSyntaxError extends Error {
    readonly line?: number;
    constructor(line?: number);
  }
  ```

- Main thread side _models errors as data_, i.e. a single
`IStatementRange` type made up as:

  ```ts
type IStatementRange = IStatementRangeSuccess | IStatementRangeRejection

  type IStatementRangeRejection = IStatementRangeSyntaxRejection

  export interface IStatementRangeSuccess {
    readonly kind: StatementRangeKind.Success;
    readonly range: IRange;
    readonly code?: string;
  }

  export interface IStatementRangeSyntaxRejection {
    readonly kind: StatementRangeKind.Rejection;
    readonly rejectionKind: StatementRangeRejectionKind.Syntax;
    readonly line?: number;
  }
  ```

Some rationale:

- Backwards compatible and public facing `StatementRange` doesn't change

- Can add new error/rejection variants as needed

- Can't easily model main thread side errors as, well, `Error`s, because
the `StatementRangeSyntaxError` doesn't serialize across the extension
-> main thread boundary well. You basically have to turn it into some
"data" type anyways to do this, and then it doesn't feel worth it to
convert back on the main thread side.

- Ark's custom `StatementRange` LSP Request also now uses the "success"
vs "rejection" model. From Ark's LSP-ish perspective, a "rejection" is
an allowed value that can be returned as an LSP Response. We reserve the
JSONRPC error path for true "holy crap something horrible happened and I
can't complete this request" cases.

The end result looks like this:

<img width="5127" height="4470" alt="test"
src="https://github.com/user-attachments/assets/b341c1dd-edac-45a2-9aed-03a4b05b3c01"
/>

# Prior art

There isn't much prior art for exactly what I'm trying to do here, but
there is a little bit:

- `FileSystemError` with its various flavors. Not quite the same as us
because none of the flavors have any metadata to pass through, like
`line`. That metadata is what steered me away from trying to have one
overarching `StatementRangeError` class that could contain something
like `static SyntaxError(line?: number): StatementRangeError`. I did try
this, but the boilerplate didn't feel worth it.


https://github.com/posit-dev/positron/blob/31c0111d42eaff76f06e1800836663e5765e1df1/src/vscode-dts/vscode.d.ts#L9474-L9486

- `Rejection`, which is used by the `RenameProvider`. Catches an error
from the LSP side of things and converts it into a `Rejection` "data"
type, kind of like we do. Strangely does `RenameProvider & Rejection` as
the return value type.
 
- Defined here
https://github.com/posit-dev/positron/blob/9754ca9e56524315fa6b2d1981fb6cb82ef5cd7b/src/vs/editor/common/languages.ts#L2127-L2138
- Catching the error here
https://github.com/posit-dev/positron/blob/9754ca9e56524315fa6b2d1981fb6cb82ef5cd7b/src/vs/workbench/api/common/extHostLanguageFeatures.ts#L882-L903

# QA

### Release Notes

#### New Features

- Stepping through code with `Cmd + Enter` now works more reliably when
there are syntax errors in the document:
    - For R code:
- [Code _above_ the first syntax
error](posit-dev/ark#1030) can now be executed
reliably with `Cmd + Enter`
- [Code _below_ the first syntax
error](#11907) causes a
notification to pop up that informs you that reliable execution is
impossible and provides you with a button to jump to the syntax error
- For Quarto documents, a syntax error in one chunk no longer affects
the ability to [run code in other
chunks](quarto-dev/quarto#914)

### QA Notes

QA: It would be nice to have two integration tests

```r
1 
  + 1 # <put cursor here and execute this, it should work now and send the whole statement>

2 + \ 2
```

```r
1 + \ 1

2 
  + 2 # <put cursor here and execute this, it should not send anything, and a notification should pop up>
```

`@:ark` `@:quarto` `@:console`

---------

Signed-off-by: Davis Vaughan <davis@posit.co>
Co-authored-by: Lionel Henry <lionel.hry@proton.me>
I think this is causing test failures because in CI we run the tests as vscode, but in these externals we are trying to assert that positron will be available, and it's not

It would be nice to figure this out though, because this was required for me to run dev quarto with dev positron.
@DavisVaughan DavisVaughan marked this pull request as ready for review March 11, 2026 17:07
This is why I was needing `'positron'` as an "external" before. We've never had to expose a class that we need to construct before, and it requires using it as a "value" rather than just as a "type" so it needs to show up in `PositronApi` and be called via `hooksApi()`
runtime: PositronRuntime;
languages: PositronLanguages;
window: PositronWindow;
StatementRangeSyntaxError: typeof StatementRangeSyntaxError;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we access StatementRangeSyntaxError as a value and not just as a type, it must live here as well. i.e. we do err instanceof hooks.StatementRangeSyntaxError and throw new hooks.StatementRangeSyntaxError() with it.

This explains why I was seeing weird errors during yarn run build dev of

quarto:dev: ✘ [ERROR] Could not resolve "positron"
quarto:dev: 
quarto:dev:     src/host/hooks.ts:19:23:
quarto:dev:       19 │ import * as hooks from 'positron';
quarto:dev:          ╵                        ~~~~~~~~~~

@DavisVaughan DavisVaughan requested a review from juliasilge March 11, 2026 18:25
Copy link
Collaborator

@juliasilge juliasilge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working great for me! 🙌

Image

I don't think it's possible to write any meaningful tests for this, without a Positron test runner build.

Can you please update CHANGELOG.md for the extension before merging?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants