Skip to content

Implement public facing StatementRangeSyntaxError#11907

Merged
DavisVaughan merged 22 commits intomainfrom
feature/statement-range-errors
Feb 24, 2026
Merged

Implement public facing StatementRangeSyntaxError#11907
DavisVaughan merged 22 commits intomainfrom
feature/statement-range-errors

Conversation

@DavisVaughan
Copy link
Contributor

@DavisVaughan DavisVaughan commented Feb 13, 2026

Addresses #8350

Builds on:

Summary

This PR adds a new throwable StatementRangeSyntaxError to positron.d.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:

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

Screen.Recording.2026-02-13.at.3.28.26.PM.mov

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":

Screen.Recording.2026-02-13.at.3.32.38.PM.mov

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.

Screen.Recording.2026-02-18.at.1.32.41.PM.mov

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

    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:

    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, Errors, 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:

test

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.

    /**
    * A type that filesystem providers should use to signal errors.
    *
    * This class has factory methods for common error-cases, like `FileNotFound` when
    * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.FileNotFound(someUri);`
    */
    export class FileSystemError extends Error {
    /**
    * Create an error to signal that a file or folder wasn't found.
    * @param messageOrUri Message or uri.
    */
    static FileNotFound(messageOrUri?: string | Uri): FileSystemError;

  • 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
      export interface Rejection {
      rejectReason?: string;
      }
      export interface RenameLocation {
      range: IRange;
      text: string;
      }
      export interface RenameProvider {
      provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): ProviderResult<WorkspaceEdit & Rejection>;
      resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<RenameLocation & Rejection>;
      }
    • Catching the error here
      async provideRenameEdits(resource: URI, position: IPosition, newName: string, token: CancellationToken): Promise<extHostProtocol.IWorkspaceEditDto & languages.Rejection | undefined> {
      const doc = this._documents.getDocument(resource);
      const pos = typeConvert.Position.to(position);
      try {
      const value = await this._provider.provideRenameEdits(doc, pos, newName, token);
      if (!value) {
      return undefined;
      }
      return typeConvert.WorkspaceEdit.from(value);
      } catch (err) {
      const rejectReason = RenameAdapter._asMessage(err);
      if (rejectReason) {
      return { rejectReason, edits: undefined! };
      } else {
      // generic error
      return Promise.reject<extHostProtocol.IWorkspaceEditDto>(err);
      }
      }
      }

QA

Release Notes

New Features

  • Stepping through code with Cmd + Enter now works more reliably when there are syntax errors in the document:

QA Notes

QA: It would be nice to have two integration tests

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

2 + \ 2
1 + \ 1

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

@:ark @:quarto @:console

@github-actions
Copy link

github-actions bot commented Feb 13, 2026

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:ark @:quarto @:console

readme  valid tags

@DavisVaughan DavisVaughan changed the title WIP: Implement StatementRangeError handling Implement StatementRangeError handling Feb 18, 2026
@DavisVaughan DavisVaughan changed the title Implement StatementRangeError handling Implement public facing StatementRangeSyntaxError Feb 18, 2026
@DavisVaughan DavisVaughan marked this pull request as ready for review February 18, 2026 19:15
Comment on lines +24 to 38
type StatementRangeResponse = StatementRangeSuccess | StatementRangeRejection;

interface StatementRangeSuccess {
readonly kind: StatementRangeKind.Success;
readonly range: Range;
readonly code?: string;
}

type StatementRangeRejection = StatementRangeSyntaxRejection;

interface StatementRangeSyntaxRejection {
readonly kind: StatementRangeKind.Rejection;
readonly rejectionKind: StatementRangeRejectionKind.Syntax;
readonly line?: number;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ark's custom LSP Request types for StatementRange

Mirrors the "error as data" model used by the main thread, and feels more correct from an LSP perspective compared to going through a jsonrpc error.

Comment on lines +846 to +847
StatementRangeKind: standaloneEnums.StatementRangeKind,
StatementRangeRejectionKind: standaloneEnums.StatementRangeRejectionKind,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like we needed to do this?

Comment on lines +1933 to +1943
try {
result = await this._provider.provideStatementRange(document, position, token);
} catch (err) {
if (err instanceof extHostTypes.StatementRangeSyntaxError) {
return {
kind: languages.StatementRangeKind.Rejection,
rejectionKind: languages.StatementRangeRejectionKind.Syntax,
line: err.line,
} satisfies languages.IStatementRangeSyntaxRejection;
}
throw err;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

StatementRangeAdapter handles the Extension->Main thread conversion glue for going from a thrown error to error-as-data

lionel-
lionel- previously approved these changes Feb 23, 2026
Copy link
Contributor

@lionel- lionel- left a comment

Choose a reason for hiding this comment

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

Looks great, I like where we landed with that "jump to error" notification!

My main comment is that ideally we'd always allow the statement range to advance to the next expression when evaluation succeeded. Currently the behaviour is confusing because the cursor doesn't advance without any explanation:

interrupted-jump.mov

I would also love for the new behaviour to also apply within { lists, e.g. within testthat, but the parser seems to hoist errors higher up in that case so I'm not sure we can pull this off while we depend on TS. And that's clearly not as high priority as fixing the top-level behaviour in any case.

I also noticed that sometimes line info is missing, do we know when that happens? E.g.:

missing-line.mov

line: err.line,
} satisfies languages.IStatementRangeSyntaxRejection;
}
throw err;
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens exactly when an unexpected error happens here?

I would expect to see a user notification as well, in the style of an internal error. If the unexpected error is simply logged to the console, maybe we should thread it back to the main thread in a similar fashion as the syntax rejection.

This seems especially important now that we've introduced error throwing as a normal thing to do from the provider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On unexpected error we currently catch, log, and then do one-line-at-a-time evaluation

logService.warn(`Failed to get statement range at ${position}: ${err}`);

This would likely be some really bad state, but I'm not sure that translating all unexpected errors into some kind of catch all StatementRangeRejectionKind.Unknown variant feels right or not.

I think at the call site in positronConsoleActions.ts we'd still probably do a try/catch just to be super safe, so it doesn't really buy us much extra security

Copy link
Contributor

Choose a reason for hiding this comment

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

To be clear my concern was about bubbling up the error to the user in a visible way, rather internal safety. I think we have learned that falling back automatically to the line by line behaviour is confusing to users, so a notification and invitation to open logs would probably be better. Either way that's not a big deal, as you say this would be a very unexpected path and as long as we have logged all relevant info somewhere discoverable we're good.

…leActions.ts

Co-authored-by: Lionel Henry <lionel.hry@proton.me>
Signed-off-by: Davis Vaughan <davis@posit.co>
@DavisVaughan
Copy link
Contributor Author

@lionel- when we execute the last successful statement range, but can't "advance" to the next location due to a syntax error, we now step forward 1 line past the last successful statement range that we just executed

This means that:

  • The successful range won't be reexecuted
  • The error notification pops up on the next Cmd+Enter execution
Screen.Recording.2026-02-23.at.10.58.38.AM.mov

@lionel-
Copy link
Contributor

lionel- commented Feb 23, 2026

@DavisVaughan perfect!

jmcphers
jmcphers previously approved these changes Feb 23, 2026
} else {
message = localize(
'positron.executeCode.syntaxRejection',
"Can't execute code due to a syntax error."
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we still execute code, just only one line a time since we don't have any richer information?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, anytime a provider throws an explicit StatementRangeSyntaxError, we now bail completely in favor of the notification and an optional jump-to-line button if we have that info (in this particular branch we don't have it, but still show the notification)

That's a major point of the PR, since that seems to have confused people quite a bit over in the linked issue

@DavisVaughan DavisVaughan merged commit d5d37bb into main Feb 24, 2026
25 checks passed
@DavisVaughan DavisVaughan deleted the feature/statement-range-errors branch February 24, 2026 20:44
@github-actions github-actions bot locked and limited conversation to collaborators Feb 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants