diff --git a/extensions/copilot/src/extension/prompt/node/chatMLFetcherTelemetry.ts b/extensions/copilot/src/extension/prompt/node/chatMLFetcherTelemetry.ts index ea50562771440d..ff35a497306de9 100644 --- a/extensions/copilot/src/extension/prompt/node/chatMLFetcherTelemetry.ts +++ b/extensions/copilot/src/extension/prompt/node/chatMLFetcherTelemetry.ts @@ -149,6 +149,8 @@ export class ChatMLFetcherTelemetrySender { "clientPromptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, locally counted", "isMeasurement": true }, "promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, server side counted", "isMeasurement": true }, "promptCacheTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens hitting cache as reported by server", "isMeasurement": true }, + "promptCacheCreation1hTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Cache-creation input tokens written with the 1h (extended) TTL, billed at 2x base rate. Only populated when Anthropic reports the cache_creation breakdown.", "isMeasurement": true }, + "promptCacheCreation5mTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Cache-creation input tokens written with the default 5m TTL, billed at 1.25x base rate. Only populated when Anthropic reports the cache_creation breakdown.", "isMeasurement": true }, "tokenCountMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum generated tokens", "isMeasurement": true }, "tokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of generated tokens", "isMeasurement": true }, "reasoningTokens": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of reasoning tokens", "isMeasurement": true }, @@ -227,6 +229,8 @@ export class ChatMLFetcherTelemetrySender { tokenCountMax: maxResponseTokens, promptTokenCount: chatCompletion.usage?.prompt_tokens, promptCacheTokenCount: chatCompletion.usage?.prompt_tokens_details?.cached_tokens, + promptCacheCreation1hTokenCount: chatCompletion.usage?.prompt_tokens_details?.anthropic_cache_creation?.ephemeral_1h_input_tokens, + promptCacheCreation5mTokenCount: chatCompletion.usage?.prompt_tokens_details?.anthropic_cache_creation?.ephemeral_5m_input_tokens, clientPromptTokenCount: promptTokenCount, tokenCount: chatCompletion.usage?.total_tokens, reasoningTokens: chatCompletion.usage?.completion_tokens_details?.reasoning_tokens, diff --git a/extensions/copilot/src/platform/endpoint/node/messagesApi.ts b/extensions/copilot/src/platform/endpoint/node/messagesApi.ts index 4d652790ae18b6..5b649bbeae8cae 100644 --- a/extensions/copilot/src/platform/endpoint/node/messagesApi.ts +++ b/extensions/copilot/src/platform/endpoint/node/messagesApi.ts @@ -66,6 +66,10 @@ interface AnthropicStreamEvent { output_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; + cache_creation?: { + ephemeral_1h_input_tokens?: number; + ephemeral_5m_input_tokens?: number; + }; }; }; index?: number; @@ -92,6 +96,10 @@ interface AnthropicStreamEvent { input_tokens?: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; + cache_creation?: { + ephemeral_1h_input_tokens?: number; + ephemeral_5m_input_tokens?: number; + }; }; copilot_usage?: { total_nano_aiu: number; @@ -666,6 +674,8 @@ interface AnthropicCompletionState { readonly inputTokens: number; readonly outputTokens: number; readonly cacheCreationTokens: number; + readonly cacheCreation1hTokens: number | undefined; + readonly cacheCreation5mTokens: number | undefined; readonly cacheReadTokens: number; readonly requestId: string; readonly ghRequestId: string; @@ -724,6 +734,14 @@ function buildAnthropicCompletion(state: AnthropicCompletionState, logService: I prompt_tokens_details: { cached_tokens: state.cacheReadTokens, cache_creation_input_tokens: state.cacheCreationTokens, + ...(state.cacheCreation1hTokens !== undefined || state.cacheCreation5mTokens !== undefined + ? { + anthropic_cache_creation: { + ...(state.cacheCreation1hTokens !== undefined ? { ephemeral_1h_input_tokens: state.cacheCreation1hTokens } : {}), + ...(state.cacheCreation5mTokens !== undefined ? { ephemeral_5m_input_tokens: state.cacheCreation5mTokens } : {}), + }, + } + : {}), }, completion_tokens_details: { reasoning_tokens: 0, @@ -776,6 +794,10 @@ type AnthropicNonStreamingResponse = output_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; + cache_creation?: { + ephemeral_1h_input_tokens?: number; + ephemeral_5m_input_tokens?: number; + }; }; } | { @@ -908,6 +930,8 @@ export async function processNonStreamingResponseFromMessagesEndpoint( inputTokens: usage?.input_tokens ?? 0, outputTokens: usage?.output_tokens ?? 0, cacheCreationTokens: usage?.cache_creation_input_tokens ?? 0, + cacheCreation1hTokens: usage?.cache_creation?.ephemeral_1h_input_tokens, + cacheCreation5mTokens: usage?.cache_creation?.ephemeral_5m_input_tokens, cacheReadTokens: usage?.cache_read_input_tokens ?? 0, requestId, ghRequestId, @@ -956,6 +980,8 @@ export class AnthropicMessagesProcessor { private inputTokens: number = 0; private outputTokens: number = 0; private cacheCreationTokens: number = 0; + private cacheCreation1hTokens: number | undefined; + private cacheCreation5mTokens: number | undefined; private cacheReadTokens: number = 0; private copilotUsage?: { total_nano_aiu: number }; private contextManagementResponse?: ContextManagementResponse; @@ -1036,6 +1062,8 @@ export class AnthropicMessagesProcessor { this.inputTokens = chunk.message.usage.input_tokens ?? 0; this.outputTokens = chunk.message.usage.output_tokens ?? 0; this.cacheCreationTokens = chunk.message.usage.cache_creation_input_tokens ?? 0; + this.cacheCreation1hTokens = chunk.message.usage.cache_creation?.ephemeral_1h_input_tokens ?? this.cacheCreation1hTokens; + this.cacheCreation5mTokens = chunk.message.usage.cache_creation?.ephemeral_5m_input_tokens ?? this.cacheCreation5mTokens; this.cacheReadTokens = chunk.message.usage.cache_read_input_tokens ?? 0; } return; @@ -1146,6 +1174,8 @@ export class AnthropicMessagesProcessor { this.outputTokens = chunk.usage.output_tokens; this.inputTokens = chunk.usage.input_tokens ?? this.inputTokens; this.cacheCreationTokens = chunk.usage.cache_creation_input_tokens ?? this.cacheCreationTokens; + this.cacheCreation1hTokens = chunk.usage.cache_creation?.ephemeral_1h_input_tokens ?? this.cacheCreation1hTokens; + this.cacheCreation5mTokens = chunk.usage.cache_creation?.ephemeral_5m_input_tokens ?? this.cacheCreation5mTokens; this.cacheReadTokens = chunk.usage.cache_read_input_tokens ?? this.cacheReadTokens; } if (chunk.copilot_usage && typeof chunk.copilot_usage.total_nano_aiu === 'number') { @@ -1239,6 +1269,8 @@ export class AnthropicMessagesProcessor { inputTokens: this.inputTokens, outputTokens: this.outputTokens, cacheCreationTokens: this.cacheCreationTokens, + cacheCreation1hTokens: this.cacheCreation1hTokens, + cacheCreation5mTokens: this.cacheCreation5mTokens, cacheReadTokens: this.cacheReadTokens, requestId: this.requestId, ghRequestId: this.ghRequestId, diff --git a/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts b/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts index 1fe0697e5845cb..7d53f9bd28eb79 100644 --- a/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts +++ b/extensions/copilot/src/platform/endpoint/test/node/messagesApi.spec.ts @@ -13,7 +13,7 @@ import { AnthropicMessagesTool, CUSTOM_TOOL_SEARCH_NAME, isExtendedCacheTtlEnabl import { IChatEndpoint, ICreateEndpointBodyOptions } from '../../../networking/common/networking'; import { IToolDeferralService } from '../../../networking/common/toolDeferralService'; import { createPlatformServices } from '../../../test/node/services'; -import { addMessagesApiCacheControl, addToolsAndSystemCacheControl, buildToolInputSchema, clearAllCacheControl, createMessagesRequestBody, processNonStreamingResponseFromMessagesEndpoint, processResponseFromMessagesEndpoint, rawMessagesToMessagesAPI } from '../../node/messagesApi'; +import { addMessagesApiCacheControl, addToolsAndSystemCacheControl, AnthropicMessagesProcessor, buildToolInputSchema, clearAllCacheControl, createMessagesRequestBody, processNonStreamingResponseFromMessagesEndpoint, processResponseFromMessagesEndpoint, rawMessagesToMessagesAPI } from '../../node/messagesApi'; import { HeadersImpl, Response } from '../../../networking/common/fetcherService'; import { TelemetryData } from '../../../telemetry/common/telemetryData'; import { TestLogService } from '../../../testing/common/testLogService'; @@ -1404,6 +1404,81 @@ suite('processNonStreamingResponseFromMessagesEndpoint', () => { expect(results[0].usage?.prompt_tokens_details?.cached_tokens).toBe(30); }); + test('surfaces 1h/5m cache_creation split when present', async () => { + const response = createNonStreamingResponse({ + id: 'msg_cache_ttl', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'cached' }], + model: 'claude-sonnet-4-20250514', + stop_reason: 'end_turn', + usage: { + input_tokens: 50, + output_tokens: 10, + cache_creation_input_tokens: 25, + cache_read_input_tokens: 0, + cache_creation: { + ephemeral_1h_input_tokens: 17, + ephemeral_5m_input_tokens: 8, + }, + }, + }); + + const telemetryData = TelemetryData.createAndMarkAsIssued(); + const completions = await processNonStreamingResponseFromMessagesEndpoint( + new NullTelemetryService(), + new TestLogService(), + response, + async () => undefined, + telemetryData, + ); + + const results = []; + for await (const c of completions) { + results.push(c); + } + + const details = results[0].usage?.prompt_tokens_details; + expect(details?.cache_creation_input_tokens).toBe(25); + expect(details?.anthropic_cache_creation?.ephemeral_1h_input_tokens).toBe(17); + expect(details?.anthropic_cache_creation?.ephemeral_5m_input_tokens).toBe(8); + }); + + test('omits 1h/5m split fields when Anthropic does not report them', async () => { + const response = createNonStreamingResponse({ + id: 'msg_cache_no_split', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'cached' }], + model: 'claude-sonnet-4-20250514', + stop_reason: 'end_turn', + usage: { + input_tokens: 50, + output_tokens: 10, + cache_creation_input_tokens: 20, + cache_read_input_tokens: 30, + }, + }); + + const telemetryData = TelemetryData.createAndMarkAsIssued(); + const completions = await processNonStreamingResponseFromMessagesEndpoint( + new NullTelemetryService(), + new TestLogService(), + response, + async () => undefined, + telemetryData, + ); + + const results = []; + for await (const c of completions) { + results.push(c); + } + + const details = results[0].usage?.prompt_tokens_details; + expect(details?.cache_creation_input_tokens).toBe(20); + expect(details?.anthropic_cache_creation).toBeUndefined(); + }); + test('rejects on malformed JSON', async () => { const response = Response.fromText(200, 'OK', createNonStreamingHeaders(), 'not json at all', 'node-fetch'); const telemetryData = TelemetryData.createAndMarkAsIssued(); @@ -1555,3 +1630,121 @@ suite('processResponseFromMessagesEndpoint routing', () => { expect(results[0].message.content).toHaveLength(1); }); }); + +suite('AnthropicMessagesProcessor streaming cache_creation', () => { + function makeProcessor(): AnthropicMessagesProcessor { + return new AnthropicMessagesProcessor( + TelemetryData.createAndMarkAsIssued(), + 'req-1', + 'gh-req-1', + '', + new TestLogService(), + new NullTelemetryService(), + ); + } + + test('message_start cache_creation survives a message_delta that omits the breakdown', () => { + // Production happy path: Anthropic only emits the cache_creation breakdown + // in message_start. message_delta updates other usage fields but typically + // has no cache_creation. The ?? fallback in the processor must preserve + // the values seen in message_start — including 0 (a common control-arm + // value) which would be wiped out by a `||` regression. + const processor = makeProcessor(); + const noop = async () => undefined; + + processor.push({ + type: 'message_start', + message: { + id: 'msg_stream', + type: 'message', + role: 'assistant', + content: [], + model: 'claude-sonnet-4-20250514', + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 5, + output_tokens: 0, + cache_creation_input_tokens: 12336, + cache_read_input_tokens: 391352, + cache_creation: { + ephemeral_1h_input_tokens: 0, + ephemeral_5m_input_tokens: 12336, + }, + }, + }, + }, noop); + + // message_delta with usage but no cache_creation breakdown — mirrors + // what every observed backend (Anthropic 1P, Bedrock, Vertex) emits in + // the final delta of a stream. + processor.push({ + type: 'message_delta', + delta: { type: 'message_delta', stop_reason: 'end_turn' }, + usage: { + output_tokens: 42, + input_tokens: 5, + cache_creation_input_tokens: 12336, + cache_read_input_tokens: 391352, + }, + }, noop); + + const completion = processor.push({ type: 'message_stop' }, noop); + expect(completion).toBeDefined(); + + const details = completion!.usage?.prompt_tokens_details; + expect(details?.anthropic_cache_creation?.ephemeral_1h_input_tokens).toBe(0); + expect(details?.anthropic_cache_creation?.ephemeral_5m_input_tokens).toBe(12336); + }); + + test('message_delta cache_creation overrides message_start values', () => { + // Defensive: if a backend ever did emit the breakdown in message_delta, + // the later values should win (matches the existing overwrite pattern + // for cache_creation_input_tokens / cache_read_input_tokens). + const processor = makeProcessor(); + const noop = async () => undefined; + + processor.push({ + type: 'message_start', + message: { + id: 'msg_stream_override', + type: 'message', + role: 'assistant', + content: [], + model: 'claude-sonnet-4-20250514', + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 5, + output_tokens: 0, + cache_creation_input_tokens: 10000, + cache_read_input_tokens: 0, + cache_creation: { + ephemeral_1h_input_tokens: 0, + ephemeral_5m_input_tokens: 10000, + }, + }, + }, + }, noop); + + processor.push({ + type: 'message_delta', + delta: { type: 'message_delta', stop_reason: 'end_turn' }, + usage: { + output_tokens: 10, + input_tokens: 5, + cache_creation_input_tokens: 15000, + cache_read_input_tokens: 0, + cache_creation: { + ephemeral_1h_input_tokens: 5000, + ephemeral_5m_input_tokens: 10000, + }, + }, + }, noop); + + const completion = processor.push({ type: 'message_stop' }, noop); + const details = completion!.usage?.prompt_tokens_details; + expect(details?.anthropic_cache_creation?.ephemeral_1h_input_tokens).toBe(5000); + expect(details?.anthropic_cache_creation?.ephemeral_5m_input_tokens).toBe(10000); + }); +}); diff --git a/extensions/copilot/src/platform/networking/common/openai.ts b/extensions/copilot/src/platform/networking/common/openai.ts index 307b1fb49a2a07..ced94087deb473 100644 --- a/extensions/copilot/src/platform/networking/common/openai.ts +++ b/extensions/copilot/src/platform/networking/common/openai.ts @@ -43,6 +43,19 @@ export interface APIUsage { prompt_tokens_details?: { cached_tokens: number; cache_creation_input_tokens?: number; + /** + * Anthropic-specific: per-TTL breakdown of cache-creation (write) input + * tokens. Mirrors Anthropic's `usage.cache_creation` object verbatim. + * Only populated for Anthropic Messages API responses where the server + * reports the split; absent for all other providers and for older + * Anthropic responses that don't include the breakdown. + */ + anthropic_cache_creation?: { + /** Cache-creation tokens written with the 1h (extended) TTL — billed at 2x base input rate. */ + ephemeral_1h_input_tokens?: number; + /** Cache-creation tokens written with the default 5m TTL — billed at 1.25x base input rate. */ + ephemeral_5m_input_tokens?: number; + }; }; /** * Breakdown of tokens used in a completion. diff --git a/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts b/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts index a9c7c6697a6062..07ed056ac83339 100644 --- a/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts +++ b/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts @@ -28,7 +28,7 @@ export class ReviewServiceImpl implements IReviewService { private readonly _repositoryDisposables = new DisposableStore(); private _reviewDiffReposString: string | undefined; private _diagnosticCollection: vscode.DiagnosticCollection | undefined; - private _commentController = vscode.comments.createCommentController('github-copilot-review', 'Code Review'); + private _commentController = this._disposables.add(vscode.comments.createCommentController('github-copilot-review', 'Code Review')); private _comments: InternalComment[] = []; private _monitorActiveThread: any | undefined; private _activeThread: vscode.CommentThread | undefined; diff --git a/extensions/copilot/src/platform/survey/vscode/surveyServiceImpl.ts b/extensions/copilot/src/platform/survey/vscode/surveyServiceImpl.ts index c1fd39aef92850..26b87930d08680 100644 --- a/extensions/copilot/src/platform/survey/vscode/surveyServiceImpl.ts +++ b/extensions/copilot/src/platform/survey/vscode/surveyServiceImpl.ts @@ -72,7 +72,7 @@ export class SurveyService implements ISurveyService { this.lastLanguageId = languageId; } - if (!this.debounceTimeout) { + if (this.debounceTimeout === undefined) { this.debounceTimeout = setTimeout(async () => { const eligible = await this.checkEligibility(); if (eligible) { diff --git a/extensions/php-language-features/src/features/validationProvider.ts b/extensions/php-language-features/src/features/validationProvider.ts index 8679cce3091f0a..f48384f6a5c6c0 100644 --- a/extensions/php-language-features/src/features/validationProvider.ts +++ b/extensions/php-language-features/src/features/validationProvider.ts @@ -125,6 +125,12 @@ export default class PHPValidationProvider { this.documentListener.dispose(); this.documentListener = null; } + if (this.delayers) { + for (const key in this.delayers) { + this.delayers[key].cancel(); + } + this.delayers = undefined; + } } private async loadConfiguration(): Promise { diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 090db2da3f00ee..471031e2f53f09 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -230,6 +230,7 @@ export function extract(zipPath: string, targetPath: string, options: IExtractOp function read(zipPath: string, filePath: string): Promise { return openZip(zipPath).then(zipfile => { return new Promise((c, e) => { + zipfile.once('error', err => e(toExtractError(err))); zipfile.on('entry', (entry: Entry) => { if (entry.fileName === filePath) { openZipStream(zipfile, entry).then(stream => c(stream), err => e(err)); diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 533cb26ae8e5c8..e52f0625b23340 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -400,6 +400,12 @@ class LocalStorageURLCallbackProvider extends Disposable implements IURLCallback this.lastTimeChecked = Date.now(); } + + override dispose(): void { + clearTimeout(this.checkCallbacksTimeout); + this.stopListening(); + super.dispose(); + } } class WorkspaceProvider implements IWorkspaceProvider { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index af10b515098d41..8c8065d65de6a2 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -106,7 +106,7 @@ export class SuggestWidget implements IDisposable { private _state: State = State.Hidden; private _isAuto: boolean = false; - private _loadingTimeout?: IDisposable; + private readonly _loadingTimeout = new MutableDisposable(); private readonly _pendingLayout = new MutableDisposable(); private readonly _pendingShowDetails = new MutableDisposable(); private _currentSuggestionDetails?: CancelablePromise; @@ -313,7 +313,7 @@ export class SuggestWidget implements IDisposable { this._list.dispose(); this._status.dispose(); this._disposables.dispose(); - this._loadingTimeout?.dispose(); + this._loadingTimeout.dispose(); this._pendingLayout.dispose(); this._pendingShowDetails.dispose(); this._showTimeout.dispose(); @@ -536,14 +536,14 @@ export class SuggestWidget implements IDisposable { this._isAuto = !!auto; if (!this._isAuto) { - this._loadingTimeout = disposableTimeout(() => this._setState(State.Loading), delay); + this._loadingTimeout.value = disposableTimeout(() => this._setState(State.Loading), delay); } } showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean, noFocus: boolean): void { this._contentWidget.setPosition(this.editor.getPosition()); - this._loadingTimeout?.dispose(); + this._loadingTimeout.clear(); this._currentSuggestionDetails?.cancel(); this._currentSuggestionDetails = undefined; @@ -776,7 +776,7 @@ export class SuggestWidget implements IDisposable { hideWidget(): void { this._pendingLayout.clear(); this._pendingShowDetails.clear(); - this._loadingTimeout?.dispose(); + this._loadingTimeout.clear(); this._setState(State.Hidden); this._onDidHide.fire(this); diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index f71937903f37e5..530f0570bf4b90 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -19,7 +19,7 @@ import { Checkbox, createToggleActionViewItemProvider, IToggleStyles } from '../ import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; import { IObjectTreeElement, ITreeNode, ITreeRenderer, TreeVisibility } from '../../../base/browser/ui/tree/tree.js'; import { equals } from '../../../base/common/arrays.js'; -import { ThrottledDelayer } from '../../../base/common/async.js'; +import { disposableTimeout, ThrottledDelayer } from '../../../base/common/async.js'; import { compareAnything } from '../../../base/common/comparers.js'; import { memoize } from '../../../base/common/decorators.js'; import { isCancellationError } from '../../../base/common/errors.js'; @@ -1157,7 +1157,7 @@ export class QuickInputList extends Disposable { // Accessibility hack, unfortunately on next tick // https://github.com/microsoft/vscode/issues/211976 if (this.accessibilityService.isScreenReaderOptimized()) { - setTimeout(() => { + disposableTimeout(() => { // eslint-disable-next-line no-restricted-syntax const focusedElement = this._tree.getHTMLElement().querySelector(`.monaco-list-row.focused`); const parent = focusedElement?.parentNode; @@ -1166,7 +1166,7 @@ export class QuickInputList extends Disposable { focusedElement.remove(); parent.insertBefore(focusedElement, nextSibling); } - }, 0); + }, 0, this._elementDisposable); } } diff --git a/src/vs/sessions/contrib/chat/browser/worktreeCreatedTaskDispatcher.ts b/src/vs/sessions/contrib/chat/browser/worktreeCreatedTaskDispatcher.ts index 49b76733456d7a..13a4903735c854 100644 --- a/src/vs/sessions/contrib/chat/browser/worktreeCreatedTaskDispatcher.ts +++ b/src/vs/sessions/contrib/chat/browser/worktreeCreatedTaskDispatcher.ts @@ -72,9 +72,6 @@ export class WorktreeCreatedTaskDispatcher extends Disposable implements IWorkbe const store = new DisposableStore(); this._sessionDisposables.set(session.sessionId, store); - // Wait for the session to finish loading and report an actual worktree, - // then dispatch any pending worktreeCreated tasks once. When dispatched, - // dispose the per-session subscription store to tear down this autorun. registerAutorunSelfDisposable(store, reader => { if (session.loading.read(reader)) { return; @@ -85,7 +82,7 @@ export class WorktreeCreatedTaskDispatcher extends Disposable implements IWorkbe if (!session.workspace.read(reader)?.folders.some(folder => !!folder.gitRepository?.workTreeUri)) { return; } - this._sessionDisposables.deleteAndDispose(session.sessionId); + reader.dispose(); this._dispatchWorktreeCreatedTasks(session); }); } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 54cfab506840e7..a5b082d63d3769 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -1198,6 +1198,9 @@ export class EditorPart extends Part implements IEditorPart, onDragEnd: () => clearAllTimeouts(), onDrop: () => clearAllTimeouts() })); + + // Make sure pending opener timeouts are cleared when the part is disposed + this._register(toDisposable(() => clearAllTimeouts())); } centerLayout(active: boolean): void { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts index 2ccb62051f9566..48e8e1bc3f494c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../../../../base/common/event.js'; import { disposableTimeout } from '../../../../../../../base/common/async.js'; @@ -62,8 +62,8 @@ export const enum TerminalToolAutoExpandTimeout { export class TerminalToolAutoExpand extends Disposable { private _commandFinished = false; private _receivedData = false; - private _dataEventTimeout: IDisposable | undefined; - private _noDataTimeout: IDisposable | undefined; + private readonly _dataEventTimeout = this._register(new MutableDisposable()); + private readonly _noDataTimeout = this._register(new MutableDisposable()); private readonly _onDidRequestExpand = this._register(new Emitter()); readonly onDidRequestExpand: Event = this._onDidRequestExpand.event; @@ -80,9 +80,9 @@ export class TerminalToolAutoExpand extends Disposable { store.add(this._options.onCommandExecuted(() => { // Auto-expand for long-running commands: - if (this._options.shouldAutoExpand() && !this._noDataTimeout) { - this._noDataTimeout = disposableTimeout(() => { - this._noDataTimeout = undefined; + if (this._options.shouldAutoExpand() && !this._noDataTimeout.value) { + this._noDataTimeout.value = disposableTimeout(() => { + this._noDataTimeout.clear(); const shouldExpand = this._options.shouldAutoExpand(); const hasOutput = this._options.hasRealOutput(); // Don't check receivedData here - data events can fire before onCommandExecuted @@ -90,8 +90,7 @@ export class TerminalToolAutoExpand extends Disposable { // if hasRealOutput was false at that time if (shouldExpand && hasOutput) { // Cancel the DataEvent timeout since we're expanding via the NoData path - this._dataEventTimeout?.dispose(); - this._dataEventTimeout = undefined; + this._dataEventTimeout.clear(); this._onDidRequestExpand.fire(); } }, TerminalToolAutoExpandTimeout.NoData, store); @@ -109,15 +108,14 @@ export class TerminalToolAutoExpand extends Disposable { } this._receivedData = true; // Wait 50ms and expand if command hasn't finished yet and has real output - if (this._options.shouldAutoExpand() && !this._dataEventTimeout) { - this._dataEventTimeout = disposableTimeout(() => { - this._dataEventTimeout = undefined; + if (this._options.shouldAutoExpand() && !this._dataEventTimeout.value) { + this._dataEventTimeout.value = disposableTimeout(() => { + this._dataEventTimeout.clear(); const shouldExpand = this._options.shouldAutoExpand(); const hasOutput = this._options.hasRealOutput(); if (!this._commandFinished && shouldExpand && hasOutput) { // Cancel the NoData timeout since we're expanding via the DataEvent path - this._noDataTimeout?.dispose(); - this._noDataTimeout = undefined; + this._noDataTimeout.clear(); this._onDidRequestExpand.fire(); } }, TerminalToolAutoExpandTimeout.DataEvent, store); @@ -131,9 +129,7 @@ export class TerminalToolAutoExpand extends Disposable { } private _clearAutoExpandTimeouts(): void { - this._dataEventTimeout?.dispose(); - this._dataEventTimeout = undefined; - this._noDataTimeout?.dispose(); - this._noDataTimeout = undefined; + this._dataEventTimeout.clear(); + this._noDataTimeout.clear(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index ee1177e1eb3e3e..a48b0c8f789aba 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -9,6 +9,7 @@ import * as languages from '../../../../editor/common/languages.js'; import { ActionsOrientation, ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Action, IAction, Separator, ActionRunner } from '../../../../base/common/actions.js'; import { Disposable, DisposableStore, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { TimeoutTimer } from '../../../../base/common/async.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IMarkdownRendererExtraOptions, IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js'; @@ -63,7 +64,7 @@ export class CommentNode extends Disposable { private _avatar: HTMLElement; private readonly _md: MutableDisposable = this._register(new MutableDisposable()); private _plainText: HTMLElement | undefined; - private _clearTimeout: Timeout | null; + private readonly _focusClearTimer = this._register(new TimeoutTimer()); private _editAction: Action | null = null; private _commentEditContainer: HTMLElement | null = null; @@ -149,7 +150,6 @@ export class CommentNode extends Disposable { this._domNode.setAttribute('aria-label', `${comment.userName}, ${this.commentBodyValue}`); this._domNode.setAttribute('role', 'treeitem'); - this._clearTimeout = null; this._register(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, () => this.isEditing || this._onDidClick.fire(this))); this._register(dom.addDisposableListener(this._domNode, dom.EventType.CONTEXT_MENU, e => { @@ -730,16 +730,8 @@ export class CommentNode extends Disposable { focus() { this.domNode.focus(); - if (!this._clearTimeout) { - this.domNode.classList.add('focus'); - this._clearTimeout = setTimeout(() => { - this.domNode.classList.remove('focus'); - }, 3000); - } - } - - override dispose(): void { - super.dispose(); + this.domNode.classList.add('focus'); + this._focusClearTimer.setIfNotSet(() => this.domNode.classList.remove('focus'), 3000); } } diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 845a916f0a56a7..4f5155d1da2e60 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -15,6 +15,7 @@ import { localize } from '../../../../nls.js'; export abstract class AbstractDebugAdapter implements IDebugAdapter { private sequence: number; private pendingRequests = new Map void>(); + private pendingRequestTimers = new Map(); private requestCallback: ((request: DebugProtocol.Request) => void) | undefined; private eventCallback: ((request: DebugProtocol.Event) => void) | undefined; private messageCallback: ((message: DebugProtocol.ProtocolMessage) => void) | undefined; @@ -79,7 +80,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { this.internalSend('request', request); if (typeof timeout === 'number') { const timer = setTimeout(() => { - clearTimeout(timer); + this.pendingRequestTimers.delete(request.seq); const clb = this.pendingRequests.get(request.seq); if (clb) { this.pendingRequests.delete(request.seq); @@ -94,6 +95,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { clb(err); } }, timeout); + this.pendingRequestTimers.set(request.seq, timer); } if (clb) { // store callback for this request @@ -165,6 +167,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { const clb = this.pendingRequests.get(response.request_seq); if (clb) { this.pendingRequests.delete(response.request_seq); + this.clearPendingRequestTimer(response.request_seq); clb(response); } break; @@ -198,14 +201,24 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { }; callback(err); this.pendingRequests.delete(request_seq); + this.clearPendingRequestTimer(request_seq); }); } + private clearPendingRequestTimer(requestSeq: number): void { + clearTimeout(this.pendingRequestTimers.get(requestSeq)); + this.pendingRequestTimers.delete(requestSeq); + } + getPendingRequestIds(): number[] { return Array.from(this.pendingRequests.keys()); } dispose(): void { + for (const timer of this.pendingRequestTimers.values()) { + clearTimeout(timer); + } + this.pendingRequestTimers.clear(); this._onError.dispose(); this._onExit.dispose(); this.queue = []; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 5dee85d99bcf18..59c8b4c80fbff9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -567,9 +567,9 @@ const CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED = new RawContextKey const CONTEXT_GALLERY_HAS_EXTENSION_LINK = new RawContextKey('galleryHasExtensionLink', false); const CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY = new RawContextKey('extensionsAutoUpdatePolicy', false); -async function runAction(action: IAction): Promise { +async function runAction(action: IAction): Promise { try { - await action.run(); + return await action.run() as T; } finally { if (isDisposable(action)) { action.dispose(); @@ -1397,7 +1397,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(SetColorThemeAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1418,7 +1418,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(SetFileIconThemeAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1439,7 +1439,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(SetProductIconThemeAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1498,7 +1498,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(ToggleAutoUpdateForExtensionAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1521,7 +1521,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(ToggleAutoUpdatesForPublisherAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1543,7 +1543,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(TogglePreReleaseExtensionAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1565,7 +1565,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(TogglePreReleaseExtensionAction); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1585,7 +1585,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi const extension = (await extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; const action = instantiationService.createInstance(ClearLanguageAction); action.extension = extension; - return action.run(); + return runAction(action); } }); @@ -1606,7 +1606,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (extension) { const action = instantiationService.createInstance(InstallAction, { installPreReleaseVersion: this.extensionManagementService.preferPreReleases }); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1630,7 +1630,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi isMachineScoped: true, }); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1654,7 +1654,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi preRelease: true }); action.extension = extension; - return action.run(); + return runAction(action); } } }); @@ -1673,7 +1673,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; if (extension) { - return instantiationService.createInstance(InstallAnotherVersionAction, extension, false).run(); + return runAction(instantiationService.createInstance(InstallAnotherVersionAction, extension, false)); } } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8c917ba4fac32e..c24ea1c2fca444 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -3156,14 +3156,24 @@ export class InstallSpecificVersionOfExtensionAction extends Action { const extensionPick = await this.quickInputService.pick(this.getExtensionEntries(), { placeHolder: localize('selectExtension', "Select Extension"), matchOnDetail: true }); if (extensionPick && extensionPick.extension) { const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extensionPick.extension, true); - await action.run(); + // TODO: replace with `using` once available + try { + await action.run(); + } finally { + action.dispose(); + } await this.extensionsWorkbenchService.openSearch(extensionPick.extension.identifier.id); } } private isEnabled(extension: IExtension): boolean { const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extension, true); - return action.enabled && !!extension.local && this.extensionEnablementService.isEnabled(extension.local); + // TODO: replace with `using` once available + try { + return action.enabled && !!extension.local && this.extensionEnablementService.isEnabled(extension.local); + } finally { + action.dispose(); + } } private async getExtensionEntries(): Promise { diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts b/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts index b08d0cf9f38cae..8b704044b57e1c 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts @@ -7,7 +7,7 @@ import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/key import { ResolvedKeybinding } from '../../../../base/common/keybindings.js'; import { OS } from '../../../../base/common/platform.js'; import './media/issueReporterOverlay.css'; -import { $, addDisposableListener, append, EventType, getWindow } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, disposableWindowInterval, EventType, getWindow } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js'; @@ -385,12 +385,12 @@ export class IssueReporterOverlay { let remaining = this.screenshotDelay; captureBtn.label = `${remaining}...`; const targetWindow = getWindow(this.container); - const interval = targetWindow.setInterval(() => { + const intervalDisposable = this.disposables.add(disposableWindowInterval(targetWindow, () => { remaining--; if (remaining > 0) { captureBtn.label = `${remaining}...`; } else { - targetWindow.clearInterval(interval); + this.disposables.delete(intervalDisposable); captureBtn.label = `$(device-camera) ${localize('screenshot', "Screenshot")}`; captureBtn.element.style.minWidth = ''; captureBtn.enabled = true; @@ -399,7 +399,7 @@ export class IssueReporterOverlay { this.updateAttachmentButtons(); this._onDidRequestScreenshot.fire(); } - }, 1000); + }, 1000)); } else { this._onDidRequestScreenshot.fire(); } diff --git a/src/vs/workbench/contrib/scm/browser/scmInput.ts b/src/vs/workbench/contrib/scm/browser/scmInput.ts index 6adf03c636d8b2..63642985fc7262 100644 --- a/src/vs/workbench/contrib/scm/browser/scmInput.ts +++ b/src/vs/workbench/contrib/scm/browser/scmInput.ts @@ -832,6 +832,7 @@ export class SCMInputWidget { this.input = undefined; this.repositoryDisposables.dispose(); this.clearValidation(); + clearTimeout(this._validationTimer); this.disposables.dispose(); } } diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts index 60c3e91e87546e..c55c9aa68da056 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, PauseableEmitter } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; @@ -25,8 +25,8 @@ export class SearchResultImpl extends Disposable implements ISearchResult { merge: mergeSearchResultEvents })); readonly onChange: Event = this._onChange.event; - private _onWillChangeModelListener: IDisposable | undefined; - private _onDidChangeModelListener: IDisposable | undefined; + private readonly _onWillChangeModelListener = this._register(new MutableDisposable()); + private readonly _onDidChangeModelListener = this._register(new MutableDisposable()); private _plainTextSearchResult: PlainTextSearchHeadingImpl; private _aiTextSearchResult: AITextSearchHeadingImpl; @@ -157,8 +157,7 @@ export class SearchResultImpl extends Disposable implements ISearchResult { private onDidAddNotebookEditorWidget(widget: NotebookEditorWidget): void { - this._onWillChangeModelListener?.dispose(); - this._onWillChangeModelListener = widget.onWillChangeModel( + this._onWillChangeModelListener.value = widget.onWillChangeModel( (model) => { if (model) { this.onNotebookEditorWidgetRemoved(widget, model?.uri); @@ -166,9 +165,8 @@ export class SearchResultImpl extends Disposable implements ISearchResult { } ); - this._onDidChangeModelListener?.dispose(); // listen to view model change as we are searching on both inputs and outputs - this._onDidChangeModelListener = widget.onDidAttachViewModel( + this._onDidChangeModelListener.value = widget.onDidAttachViewModel( () => { if (widget.hasModel()) { this.onNotebookEditorWidgetAdded(widget, widget.textModel.uri); @@ -291,8 +289,6 @@ export class SearchResultImpl extends Disposable implements ISearchResult { override async dispose(): Promise { this._aiTextSearchResult?.dispose(); this._plainTextSearchResult?.dispose(); - this._onWillChangeModelListener?.dispose(); - this._onDidChangeModelListener?.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts index 211eb8bbe9bd5f..e2711d36aeead4 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts @@ -221,8 +221,8 @@ suite('Terminal Contrib Shell Integration Recordings', () => { const promptInputModel = capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel; if (promptInputModel && promptInputModel.getCombinedString() !== event.data) { await Promise.race([ - await timeout(1000).then(() => { throw new Error(`Prompt input change timed out current="${promptInputModel.getCombinedString()}", expected="${event.data}"`); }), - await new Promise(r => { + timeout(1000).then(() => { throw new Error(`Prompt input change timed out current="${promptInputModel.getCombinedString()}", expected="${event.data}"`); }), + new Promise(r => { const d = promptInputModel.onDidChangeInput(() => { if (promptInputModel.getCombinedString() === event.data) { d.dispose(); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index d38efb60031b08..0cb22b9cf17410 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -18,7 +18,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { splitRecentLabel } from '../../../../base/common/labels.js'; -import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ILink, LinkedText } from '../../../../base/common/linkedText.js'; import { parse } from '../../../../base/common/marshalling.js'; import { Schemas, matchesScheme } from '../../../../base/common/network.js'; @@ -141,7 +141,7 @@ export class GettingStartedPage extends EditorPane { private categoriesPageScrollbar: DomScrollableElement | undefined; private detailsPageScrollbar: DomScrollableElement | undefined; - private detailsScrollbar: DomScrollableElement | undefined; + private readonly detailsScrollbar = this._register(new MutableDisposable()); private buildSlideThrottle = this._register(new Throttler()); @@ -149,9 +149,9 @@ export class GettingStartedPage extends EditorPane { private contextService: IContextKeyService; - private recentlyOpenedList?: GettingStartedIndexList; - private startList?: GettingStartedIndexList; - private gettingStartedList?: GettingStartedIndexList; + private readonly recentlyOpenedList = this._register(new MutableDisposable>()); + private readonly startList = this._register(new MutableDisposable>()); + private readonly gettingStartedList = this._register(new MutableDisposable>()); private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -510,7 +510,7 @@ export class GettingStartedPage extends EditorPane { const selectedCategory = this.gettingStartedCategories.find(category => category.id === categoryId); if (!selectedCategory) { throw Error('Could not find category with ID ' + categoryId); } this.setHiddenCategories([...this.getHiddenCategories().add(categoryId)]); - this.gettingStartedList?.rerender(); + this.gettingStartedList.value?.rerender(); } private markAllStepsComplete() { @@ -859,7 +859,7 @@ export class GettingStartedPage extends EditorPane { } this.detailsPageScrollbar?.scanDomNode(); - this.detailsScrollbar?.scanDomNode(); + this.detailsScrollbar.value?.scanDomNode(); } private updateMediaSourceForColorMode(element: HTMLImageElement, sources: { hcDark: URI; hcLight: URI; dark: URI; light: URI }) { @@ -1097,9 +1097,7 @@ export class GettingStartedPage extends EditorPane { return li; }; - if (this.recentlyOpenedList) { this.recentlyOpenedList.dispose(); } - - const recentlyOpenedList = this.recentlyOpenedList = new GettingStartedIndexList( + const recentlyOpenedList = this.recentlyOpenedList.value = new GettingStartedIndexList( { title: localize('recent', "Recent"), klass: 'recently-opened', @@ -1141,13 +1139,13 @@ export class GettingStartedPage extends EditorPane { } private refreshRecentlyOpened(): void { - if (!this.recentlyOpenedList) { + if (!this.recentlyOpenedList.value) { return; } this.recentlyOpened.then(({ workspaces }) => { const workspacesWithID = this.filterRecentlyOpened(workspaces); - this.recentlyOpenedList?.setEntries(workspacesWithID); + this.recentlyOpenedList.value?.setEntries(workspacesWithID); }).catch(onUnexpectedError); } @@ -1162,9 +1160,7 @@ export class GettingStartedPage extends EditorPane { this.iconWidgetFor(entry), $('span', {}, entry.title))); - if (this.startList) { this.startList.dispose(); } - - const startList = this.startList = new GettingStartedIndexList( + const startList = this.startList.value = new GettingStartedIndexList( { title: localize('start', "Start"), klass: 'start-container', @@ -1226,7 +1222,7 @@ export class GettingStartedPage extends EditorPane { $('.progress-bar-inner')))); }; - if (this.gettingStartedList) { this.gettingStartedList.dispose(); } + const rankWalkthrough = (e: IResolvedWalkthrough) => { let rank: number | null = e.order; @@ -1240,7 +1236,7 @@ export class GettingStartedPage extends EditorPane { return rank; }; - const gettingStartedList = this.gettingStartedList = new GettingStartedIndexList( + const gettingStartedList = this.gettingStartedList.value = new GettingStartedIndexList( { title: localize('walkthroughs', "Walkthroughs"), klass: 'getting-started', @@ -1267,14 +1263,14 @@ export class GettingStartedPage extends EditorPane { } layout(size: Dimension) { - this.detailsScrollbar?.scanDomNode(); + this.detailsScrollbar.value?.scanDomNode(); this.categoriesPageScrollbar?.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); - this.startList?.layout(size); - this.gettingStartedList?.layout(size); - this.recentlyOpenedList?.layout(size); + this.startList.value?.layout(size); + this.gettingStartedList.value?.layout(size); + this.recentlyOpenedList.value?.layout(size); if (this.editorInput?.selectedStep && this.currentMediaType) { this.mediaDisposables.clear(); @@ -1290,7 +1286,7 @@ export class GettingStartedPage extends EditorPane { this.categoriesPageScrollbar?.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); - this.detailsScrollbar?.scanDomNode(); + this.detailsScrollbar.value?.scanDomNode(); } private updateCategoryProgress() { @@ -1505,7 +1501,7 @@ export class GettingStartedPage extends EditorPane { if (!this.editorInput) { return; } - if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } + this.extensionService.whenInstalledExtensionsRegistered().then(() => { // Remove internal extension id specifier from exposed id's @@ -1636,8 +1632,8 @@ export class GettingStartedPage extends EditorPane { : []), ) ); - this.detailsScrollbar = this._register(new DomScrollableElement(stepsContainer, { className: 'steps-container' })); - const stepListComponent = this.detailsScrollbar.getDomNode(); + this.detailsScrollbar.value = new DomScrollableElement(stepsContainer, { className: 'steps-container' }); + const stepListComponent = this.detailsScrollbar.value.getDomNode(); const categoryFooter = $('.getting-started-footer'); if (this.editorInput.showTelemetryNotice && getTelemetryLevel(this.configurationService) !== TelemetryLevel.NONE && this.productService.enableTelemetry) { @@ -1649,7 +1645,7 @@ export class GettingStartedPage extends EditorPane { const toExpand = category.steps.find(step => this.contextService.contextMatchesRules(step.when) && !step.done) ?? category.steps[0]; this.selectStep(selectedStep ?? toExpand.id, !selectedStep, preserveFocus); - this.detailsScrollbar.scanDomNode(); + this.detailsScrollbar.value?.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); this.registerDispatchListeners(); @@ -1701,7 +1697,7 @@ export class GettingStartedPage extends EditorPane { this.editorInput.walkthroughPageTitle = undefined; } - if (this.gettingStartedCategories.length !== this.gettingStartedList?.itemCount) { + if (this.gettingStartedCategories.length !== this.gettingStartedList.value?.itemCount) { // extensions may have changed in the time since we last displayed the walkthrough list // rebuild the list this.buildCategoriesSlide(); diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index a7b67f152e3bd2..ae304b2386194a 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -141,13 +141,14 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { this.handleURL(URI.revive(JSON.parse(urlToHandleValue)), { trusted: true }); } + const cache = ExtensionUrlBootstrapHandler.cache; + const drainTimeout = setTimeout(() => cache.forEach(([uri, option]) => this.handleURL(uri, option))); + this.disposable = combinedDisposable( urlService.registerHandler(this), - interval + interval, + toDisposable(() => clearTimeout(drainTimeout)) ); - - const cache = ExtensionUrlBootstrapHandler.cache; - setTimeout(() => cache.forEach(([uri, option]) => this.handleURL(uri, option))); } async handleURL(uri: URI, options?: IOpenURLOptions): Promise { diff --git a/src/vs/workbench/services/userAttention/browser/userAttentionBrowser.ts b/src/vs/workbench/services/userAttention/browser/userAttentionBrowser.ts index a2470b6cad4122..fadd10a7863fe2 100644 --- a/src/vs/workbench/services/userAttention/browser/userAttentionBrowser.ts +++ b/src/vs/workbench/services/userAttention/browser/userAttentionBrowser.ts @@ -127,6 +127,11 @@ export class UserAttentionServiceEnv extends Disposable { this._activityDebounceTimeout = undefined; }, 500); } + + override dispose(): void { + clearTimeout(this._activityDebounceTimeout); + super.dispose(); + } } const eventListenerOptions: AddEventListenerOptions = {