-
Notifications
You must be signed in to change notification settings - Fork 0
investigate: extension throws error during analysis β root cause findings #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,223 @@ | ||||||||||||||||||||||||
| # Investigation: Extension Throws Error During Analysis | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## Summary | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| This document captures all potential root causes identified in the source code | ||||||||||||||||||||||||
| that can lead to a visible error (or a silent failure) when the user triggers | ||||||||||||||||||||||||
| any of the **Explain** commands (`Explain Line`, `Explain Block`, | ||||||||||||||||||||||||
| `Explain File`). | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## 1. `ExplanationProvider.explain` β unguarded `LanguageModelError` codes | ||||||||||||||||||||||||
| **File:** `src/explanationProvider.ts` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||
| if (err instanceof vscode.LanguageModelError) { | ||||||||||||||||||||||||
| throw new Error(`Copilot error (${err.code}): ${err.message}`); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| throw err; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Issue | ||||||||||||||||||||||||
| `vscode.LanguageModelError` carries typed `code` values | ||||||||||||||||||||||||
| (`NoPermissions`, `Blocked`, `NotFound`, `RequestFailed`, β¦). The current | ||||||||||||||||||||||||
| handler re-throws them all with the same generic message, so the user sees | ||||||||||||||||||||||||
| cryptic strings like `Copilot error (NoPermissions): β¦` with no actionable | ||||||||||||||||||||||||
| guidance. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Additionally, if the model stream throws a plain `Error` (e.g. a network | ||||||||||||||||||||||||
| timeout), it propagates up uncaught to `runExplain`, which shows it in the | ||||||||||||||||||||||||
| status bar, but **the LRU cache is not written** β meaning every retry hits | ||||||||||||||||||||||||
| the model again instead of surfacing a friendlier retry prompt. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Suggested fix | ||||||||||||||||||||||||
| Map each `LanguageModelError.code` to a human-readable message and offer | ||||||||||||||||||||||||
| retry / re-auth guidance. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## 2. `ExplanationProvider.explain` β empty `result` stored in cache | ||||||||||||||||||||||||
| **File:** `src/explanationProvider.ts` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| let result = ''; | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| const response = await model.sendRequest([prompt], {}, token); | ||||||||||||||||||||||||
| for await (const chunk of response.text) { | ||||||||||||||||||||||||
| if (token.isCancellationRequested) { break; } | ||||||||||||||||||||||||
| result += chunk; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } catch (err) { β¦ } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| result = result.trim(); | ||||||||||||||||||||||||
| this.addToCache(key, result); // β stored even when result === '' | ||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Issue | ||||||||||||||||||||||||
| When the model returns an empty body (e.g. content-filtered, quota exhausted, | ||||||||||||||||||||||||
| or the token was cancelled mid-stream), `result` is `''` after `.trim()`. | ||||||||||||||||||||||||
| That empty string is written into the LRU cache and subsequently rendered as | ||||||||||||||||||||||||
| an invisible ghost-text decoration. The next identical request returns `''` | ||||||||||||||||||||||||
| from cache without ever hitting the model again, making it look like the | ||||||||||||||||||||||||
| feature is broken for that code snippet. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Suggested fix | ||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| result = result.trim(); | ||||||||||||||||||||||||
| if (!result) { | ||||||||||||||||||||||||
| throw new Error('Copilot returned an empty explanation. Please try again.'); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| this.addToCache(key, result); | ||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## 3. `codeLensProvider.ts` β double semicolon (typo / syntax smell) | ||||||||||||||||||||||||
| **File:** `src/codeLensProvider.ts`, `regexBlockLenses` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| const RE = isXml ? XAML_RE : CODE_RE;; // β double semicolon | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Issue | ||||||||||||||||||||||||
| While not a runtime error in TypeScript/JavaScript, this is a clear typo that | ||||||||||||||||||||||||
| may confuse linters (`no-extra-semi`) and obscure any future edits on that | ||||||||||||||||||||||||
| line. Should be removed. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## 4. `codeLensProvider.ts` β `isNoiseLine` comment-detection misses `//` mid-regex | ||||||||||||||||||||||||
| **File:** `src/codeLensProvider.ts`, `isNoiseLine` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| if (/^(\\/\\/|#|--|%%|;)/.test(trimmed)) { return true; } | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Issue | ||||||||||||||||||||||||
| The pattern correctly identifies single-line comments *at the start of a | ||||||||||||||||||||||||
| trimmed line*. However, import/use-statement guard placed **after** this check | ||||||||||||||||||||||||
| can still let through lines that begin with a comment prefix from some | ||||||||||||||||||||||||
| languages not listed (e.g. Lua `--`, Haskell `--`). These are unlikely to be | ||||||||||||||||||||||||
| active languages today but the `sourceDoc.languages` config is user-editable, | ||||||||||||||||||||||||
| so arbitrary language IDs can be added. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Comment on lines
+94
to
+108
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## 5. `extension.ts` β `languageRegistrations` never pushed to `context.subscriptions` | ||||||||||||||||||||||||
| **File:** `src/extension.ts` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| let languageRegistrations: vscode.Disposable[] = []; | ||||||||||||||||||||||||
| context.subscriptions.push( | ||||||||||||||||||||||||
| vscode.workspace.onDidChangeConfiguration(e => { | ||||||||||||||||||||||||
| if (e.affectsConfiguration('sourceDoc.languages')) { | ||||||||||||||||||||||||
| languageRegistrations.forEach(d => d.dispose()); | ||||||||||||||||||||||||
| languageRegistrations = []; | ||||||||||||||||||||||||
| registerCodeLensProviders(context, codeLensProvider, languageRegistrations); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Issue | ||||||||||||||||||||||||
| `registerCodeLensProviders` pushes the new `CodeLensProvider` registrations | ||||||||||||||||||||||||
| into the local `languageRegistrations` array (correctly). However, that array | ||||||||||||||||||||||||
| is **never added to `context.subscriptions`**. If the extension is deactivated | ||||||||||||||||||||||||
| between two configuration-change events, the registrations accumulated in that | ||||||||||||||||||||||||
| array will **not** be disposed. This leaks the provider registrations. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| The first call to `registerCodeLensProviders` (outside the listener) correctly | ||||||||||||||||||||||||
| passes `context.subscriptions` as the default, so only the _dynamic_ | ||||||||||||||||||||||||
| re-registration path is affected. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Suggested fix | ||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| registerCodeLensProviders(context, codeLensProvider, languageRegistrations); | ||||||||||||||||||||||||
| // After re-registration, track new disposables: | ||||||||||||||||||||||||
| languageRegistrations.forEach(d => context.subscriptions.push(d)); | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
| Or simply push the `languageRegistrations` array reference once so VS Code | ||||||||||||||||||||||||
| drains it on deactivation. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Comment on lines
+142
to
+146
|
||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ## 6. `extension.ts` β `explainFile` does not guard against zero non-noise lines | ||||||||||||||||||||||||
| **File:** `src/extension.ts`, `sourceDoc.explainFile` command handler | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||
| const results = await runWithConcurrency(lines, 5, async ({ line, code }) => { β¦ }); | ||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| ### Issue | ||||||||||||||||||||||||
| `runWithConcurrency` is called with `Math.min(limit, items.length)` workers. | ||||||||||||||||||||||||
| When `lines` is empty (e.g. a file that consists entirely of comments or | ||||||||||||||||||||||||
| blank lines), `items.length === 0`, which causes | ||||||||||||||||||||||||
| `Array.from({ length: 0 }, worker)` β effectively a no-op. The progress | ||||||||||||||||||||||||
| notification is shown and dismissed with `"0 / 0 done"` which is confusing. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| notification is shown and dismissed with `"0 / 0 done"` which is confusing. | |
| notification is created with the title `Source Doc: explaining 0 linesβ¦` and is dismissed without any per-line `"N / total done"` updates, which can look like a brief, confusing flicker. |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Section 7 motivates the document.lineAt(0) guard using βvirtual/untitled documentsβ, but this CodeLens provider is registered with { scheme: 'file' }, so it wonβt be invoked for untitled/virtual documents. If you want to keep this item, please reframe it as a general defensive guard (or cite a concrete case where a file-scheme TextDocument can have lineCount === 0).
| If an empty file is opened (`lineCount === 0`), calling `document.lineAt(0)` | |
| throws a `RangeError`. VS Code normally guarantees at least one line, but | |
| virtual/untitled documents and some language servers surface edge cases. | |
| `provideCodeLenses` assumes that `document.lineCount > 0` and calls | |
| `document.lineAt(0)` unconditionally. VS Code currently guarantees at least | |
| one line for normal `file`-scheme documents, so this should not occur in | |
| typical operation, but it still relies on that invariant β if it is ever | |
| violated (for example by an extension host or language feature bug that | |
| surfaces a `file` document with `lineCount === 0`), a `RangeError` would be | |
| thrown. A small `lineCount` check makes the code resilient to such edge | |
| cases at essentially zero cost. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In section 1, the doc says a non-LanguageModelError propagates up uncaught to
runExplainand is shown in the status bar. In the current implementation,runExplaincatches all errors and callsvscode.window.showErrorMessage(...), not a status bar message. Please adjust this description to match the actual error handling path/UI.