Implement public facing StatementRangeSyntaxError#11907
Conversation
|
E2E Tests 🚀 |
StatementRangeError handlingStatementRangeError handling
StatementRangeError handlingStatementRangeSyntaxError
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| StatementRangeKind: standaloneEnums.StatementRangeKind, | ||
| StatementRangeRejectionKind: standaloneEnums.StatementRangeRejectionKind, |
There was a problem hiding this comment.
It seems like we needed to do this?
| 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; |
There was a problem hiding this comment.
StatementRangeAdapter handles the Extension->Main thread conversion glue for going from a thrown error to error-as-data
lionel-
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
On unexpected error we currently catch, log, and then do one-line-at-a-time evaluation
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
There was a problem hiding this comment.
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.
src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts
Outdated
Show resolved
Hide resolved
src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts
Outdated
Show resolved
Hide resolved
…leActions.ts Co-authored-by: Lionel Henry <lionel.hry@proton.me> Signed-off-by: Davis Vaughan <davis@posit.co>
|
@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:
Screen.Recording.2026-02-23.at.10.58.38.AM.mov |
|
@DavisVaughan perfect! |
src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts
Show resolved
Hide resolved
| } else { | ||
| message = localize( | ||
| 'positron.executeCode.syntaxRejection', | ||
| "Can't execute code due to a syntax error." |
There was a problem hiding this comment.
Don't we still execute code, just only one line a time since we don't have any richer information?
There was a problem hiding this comment.
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
Addresses #8350
StatementRangeSuccessandStatementRangeRejectionark#1040StatementRangeSyntaxErrorquarto-dev/quarto#919Builds on:
provideStatementRange()create a single chunk virtual doc quarto-dev/quarto#914Summary
This PR adds a new throwable
StatementRangeSyntaxErrortopositron.d.ts.This is the only public facing API change from this PR.
The idea is that from extension land (
positron-r,positron-python, andquarto) you should be able to throw aStatementRangeSyntaxErrorfrom yourprovideStatementRange()provider, with an optionallinenumber of where the syntax error roughly starts. This gets propagated up through the main thread and into ourCmd+Enterhandling inpositronConsoleActions.ts, where we now detect this special case and:linenumber is present, we give them a button to jump to that line.Cmd+Enterhandling altogether. We don't even do one-line-at-a-time execution. Sending pipelines with line breaks to console from a script with incomplete expression further down in the script doesn't work #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:
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
linenumber 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
StatementRangeandthrow StatementRangeSyntaxErrorMain thread side models errors as data, i.e. a single
IStatementRangetype made up as:Some rationale:
Backwards compatible and public facing
StatementRangedoesn't changeCan add new error/rejection variants as needed
Can't easily model main thread side errors as, well,
Errors, because theStatementRangeSyntaxErrordoesn'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
StatementRangeLSP 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:
Prior art
There isn't much prior art for exactly what I'm trying to do here, but there is a little bit:
FileSystemErrorwith its various flavors. Not quite the same as us because none of the flavors have any metadata to pass through, likeline. That metadata is what steered me away from trying to have one overarchingStatementRangeErrorclass that could contain something likestatic SyntaxError(line?: number): StatementRangeError. I did try this, but the boilerplate didn't feel worth it.positron/src/vscode-dts/vscode.d.ts
Lines 9474 to 9486 in 31c0111
Rejection, which is used by theRenameProvider. Catches an error from the LSP side of things and converts it into aRejection"data" type, kind of like we do. Strangely doesRenameProvider & Rejectionas the return value type.positron/src/vs/editor/common/languages.ts
Lines 2127 to 2138 in 9754ca9
positron/src/vs/workbench/api/common/extHostLanguageFeatures.ts
Lines 882 to 903 in 9754ca9
QA
Release Notes
New Features
Cmd + Enternow works more reliably when there are syntax errors in the document:Cmd + EnterQA Notes
QA: It would be nice to have two integration tests
@:ark@:quarto@:console