feat: offer inline-script env setup as an unresolved-import quick fix - #29
Closed
StellaHuang95 wants to merge 1 commit into
Closed
StellaHuang95 wants to merge 1 commit into
StellaHuang95 wants to merge 1 commit into
Conversation
The PEP 723 inline-script feature's only discovery surface is a CodeLens, and provideCodeLenses returns [] while document.isDirty (codeLens.ts:51). That hides it at the one moment a user most needs it: right after typing `import requests` and seeing the squiggle. Add a CodeActionProvider that offers "Set up this script's Python environment" on an unresolved-import diagnostic in a .py file that declares a `# /// script` block and has no environment yet. It parses the in-memory buffer, so it works on an unsaved edit. The action makes no promise it cannot keep: the title says what it does rather than that the squiggle will clear, and `diagnostics`/`isPreferred` are both left unset so VS Code is not told the action resolves anything. Setup reads the block from disk (envManager.ts:308), so the handler now saves a dirty document first and seeds routing metadata from the saved bytes, closing a race where a just-typed block would be misread as a mid-setup edit and have its association silently skipped. Adds an `inlineScript.setupInvoked` telemetry event with a low-cardinality `trigger` (codelens | codeaction | bulk) to measure whether the new surface actually improves adoption. Everything stays behind `python-envs.inlineScripts.enabled`, so none of this is user-visible yet. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Owner
Author
|
Closing for now - not ready to send. |
StellaHuang95
deleted the
stellahuang-microsoft-inline-script-code-action
branch
September 15, 2026 00:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The PEP 723 inline-script feature has exactly one discovery surface — a CodeLens — and
provideCodeLensesreturns[]whenever the document is dirty (src/features/inlineScript/codeLens.ts:51).So the CodeLens is absent at the one moment a user most needs it: right after typing
import requestsand seeing the red squiggle. Users who don't already know PEP 723 never discover the feature at all.Change
Register a
CodeActionProvider(src/features/inlineScript/setupCodeAction.ts) that offers "Set up this script's Python environment" as a quick fix when:.pythat could carry an inline-script environment,routing.shouldRoute(uri)is false), and# /// scriptblock.Gates are ordered cheapest-first because
provideCodeActionscan fire on cursor movement — thecontext.diagnosticsscan happens before any parsing, so the common case (no unresolved import) costs nothing but a short array scan.Metadata is parsed from the in-memory buffer, so unlike the CodeLens this works while the document is dirty.
Honest labeling — wording and mechanics
The title says what the action does, not that the squiggle will clear. It may well not: the unresolved module might be undeclared in the block, or declared under a different distribution name (
PILvspillow). Correspondingly:action.diagnosticsis left unset — populating it tells VS Code the action resolves those diagnostics and opts it into fix-all affordances.action.isPreferredis left unset, so it never pre-empts a genuine import fix such as "add import".There is deliberately no import-name → package-name mapping. Setup installs the block's declared
dependenciesverbatim; the import name is never an input.Diagnostic codes
Matched on
codeonly, neversource— Pyrefly-backed Pylance reports its source as the literal stringpylance + pyrefly(pylance-internal/src/common/diagnosticCodeMapper.ts:102), so any source allow-list would be wrong somewhere.Diagnostic.codeisstring | number | {value, target}; the union is normalized and lowercased before comparison.reportMissingImports,reportMissingModuleSourceunresolved-import,possibly-missing-importdiagnosticCodeMapper.ts:164-165missing-import,missing-source,missing-source-for-stubsdiagnosticCodeMapper.ts:380-382import-not-found,import-untypedms-python.mypy-type-checkerreportMissingModuleSourceis included on purposePylance's own
isMissingImportDiagnostic()excludes it (diagnosticCodeMapper.ts:72-78) — and rightly so for their fix, since a stub-resolved module is already spelled correctly and has nothing for a "change spelling" action to suggest. For us the meaning is the opposite kind of useful: a stub was found but the source was not, i.e. the package isn't installed — precisely what setting up the environment fixes. The divergence is commented in the code.Reappears after a block edit — intentional
Once the script is set up the action disappears. If the user then edits the
# /// scriptblock (e.g. to add the dependency that was missing), it comes back by itself:InlineScriptRoutingRegistry.setMetadataresetsvalidatedAssociationtofalsewhen the metadata identity changes (src/common/inlineScript/routingRegistry.ts:71-73). This is desired behavior and is covered by a test.Save before setup — and a race it closes
InlineScriptEnvManager.createresolves the block from disk viareadInlineScriptMetadataFromFile(src/managers/builtin/inlineScript/envManager.ts:308). Since the quick fix is offered on a dirty buffer, the handler now saves first.Saving alone wasn't enough.
setUpInlineScriptEnvironmentcompares the metadata identity before and aftercreate, and silently skips the association if it changed mid-setup. For a block the user had just typed, routing metadata is stillundefined(the lazy detector only clears on block-touching edits,src/features/inlineScript/lazyDetector.ts:278-291) — the detector's own save handler would then populate it duringcreate, the identity would goundefined → X, and the association would be silently skipped. The user would wait for an environment and get nothing.saveScriptBeforeSetup()closes this by re-reading the file it just wrote and seedingrouting.setMetadata(). The detector's later write computes the same identity from the same bytes, so the identity stays stable, and an unchanged identity preserves any existing validated association.Telemetry
New event
inlineScript.setupInvokedwith a low-cardinalitytrigger(codelens|codeaction|bulk) andoutcome(created|notCreated|error), emitted at the setup entry points. Measuring whether the code action actually improves adoption is the entire premise of this change.A new event rather than threading
triggerthrough the existinginlineScript.envCreated/envReuseHit/envErrorevents: those fire deep insideInlineScriptEnvManager, and plumbing a trigger down would mean changing the publicCreateEnvironmentOptions/EnvironmentManager.createAPI surface for a telemetry detail.outcome: 'notCreated'intentionally lumps cancelled/skipped/failed together, since the failure taxonomy already ships onenvError'scategory.normalizeSetupTrigger()coerces anything unrecognized tocodelensso the property stays low-cardinality even if the command is invoked with junk. The__GDPR__block insrc/common/telemetry/constants.tsis updated.Tests
22 new tests in
setupCodeAction.unit.test.tsplus an 11-testsetupInlineScriptEnvironmentHandlersuite insetupEnvironment.unit.test.ts. Coverage includes every gating branch, all four diagnostic dialects, the{value, target}object form, case-insensitivity, thepylance + pyreflysource,diagnostics/isPreferredleft unset, reappear-after-block-edit, in-memory parse while dirty, and the header byte bound.Guard tests verified to fail without their guards
Per the convention established in microsoft#1772, each guard was removed one at a time and the covering test confirmed to fail — not merely to pass today. All 11 were detected:
.py) gateshouldRoute) gateaction.diagnosticsleft unsetThat last one caught a real bug introduced in this PR:
promptUpdateExtensionsForInlineScripts()sat inside the setuptry, so a prompt failure emitted a second telemetry event withoutcome: 'error'and showed the user a "setup failed" message even though the environment had been created successfully. It's now outside the try with its own.catch, mirroring what the bulk path already did.Notes
python-envs.setupInlineScriptEnvremains out ofpackage.jsonby design; it's invoked only by the two UI surfaces.ms-python.pythonor Pylance are needed.isInlineScriptsFeatureEnabled()is re-read per call in the provider, whereas registration is latched at activation. Flipping the setting mid-session without a reload makes the code action go inert while the CodeLens keeps showing. That's the safer direction, and the brief's gate ordering requires the per-call check.Validation
npm run lint✓ ·npm run compile-tests✓ ·npm run unittest✓ (2267 passing, 6 pending, 0 failing)Nothing here is user-visible: the whole surface stays behind the undeclared internal flag
python-envs.inlineScripts.enabled, which defaults tofalse.