diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 8dd8cc5ee49..e01e0785746 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -161,6 +161,9 @@ const _allApiProposals = { customEditorMove: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', }, + dataChannels: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dataChannels.d.ts', + }, debugVisualization: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', }, diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index d5430634469..45ab13fd348 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -91,6 +91,7 @@ import './mainThreadAiEmbeddingVector.js'; import './mainThreadAiSettingsSearch.js'; import './mainThreadMcp.js'; import './mainThreadChatStatus.js'; +import './mainThreadDataChannels.js'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadDataChannels.ts b/src/vs/workbench/api/browser/mainThreadDataChannels.ts new file mode 100644 index 00000000000..26b63278252 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadDataChannels.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { IDataChannelService } from '../../services/dataChannel/common/dataChannel.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostContext, ExtHostDataChannelsShape, MainContext, MainThreadDataChannelsShape } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadDataChannels) +export class MainThreadDataChannels extends Disposable implements MainThreadDataChannelsShape { + + private readonly _proxy: ExtHostDataChannelsShape; + + constructor( + extHostContext: IExtHostContext, + @IDataChannelService private readonly _dataChannelService: IDataChannelService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDataChannels); + + this._register(this._dataChannelService.onDidSendData(e => { + this._proxy.$onDidReceiveData(e.channelId, e.data); + })); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 50641954f60..530d77d0ea0 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -34,8 +34,9 @@ import * as search from '../../contrib/search/common/search.js'; import * as typeh from '../../contrib/typeHierarchy/common/typeHierarchy.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol.js'; -import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { InlineCompletionEndOfLifeReasonKind } from '../common/extHostTypes.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { DataChannelForwardingTelemetryService } from '../../contrib/editTelemetry/browser/forwardingTelemetryService.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -49,7 +50,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -677,7 +678,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread : reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected ? 'rejected' : 'ignored' }; - this._telemetryService.publicLog2('inlineCompletion.endOfLife', endOfLifeSummary); + + const telemetryService = this._instantiationService.createInstance(DataChannelForwardingTelemetryService); + telemetryService.publicLog2('inlineCompletion.endOfLife', endOfLifeSummary); }, disposeInlineCompletions: (completions: IdentifiableInlineCompletions, reason: languages.InlineCompletionsDisposeReason): void => { this._proxy.$freeInlineCompletionsList(handle, completions.pid, reason); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fa91d4b6049..9ecae0591ae 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -43,6 +43,7 @@ import { IExtHostCommands } from './extHostCommands.js'; import { createExtHostComments } from './extHostComments.js'; import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration.js'; import { ExtHostCustomEditors } from './extHostCustomEditors.js'; +import { IExtHostDataChannels } from './extHostDataChannels.js'; import { IExtHostDebugService } from './extHostDebugService.js'; import { IExtHostDecorations } from './extHostDecorations.js'; import { ExtHostDiagnostics } from './extHostDiagnostics.js'; @@ -151,6 +152,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostAuthentication = accessor.get(IExtHostAuthentication); const extHostLanguageModels = accessor.get(IExtHostLanguageModels); const extHostMcp = accessor.get(IExtHostMpcService); + const extHostDataChannels = accessor.get(IExtHostDataChannels); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -169,6 +171,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostProgress, extHostProgress); rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); rpcProtocol.set(ExtHostContext.ExtHostChatProvider, extHostLanguageModels); + rpcProtocol.set(ExtHostContext.ExtHostDataChannels, extHostDataChannels); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -451,6 +454,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get appCommit(): string | undefined { checkProposedApiEnabled(extension, 'resolvers'); return initData.commit; + }, + getDataChannel(channelId: string): vscode.DataChannel { + checkProposedApiEnabled(extension, 'dataChannels'); + return extHostDataChannels.createDataChannel(extension, channelId); } }; if (!initData.environment.extensionTestsLocationURI) { diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index 23a60212db1..c63ea8ed77e 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -33,6 +33,7 @@ import { ExtHostTesting, IExtHostTesting } from './extHostTesting.js'; import { ExtHostMcpService, IExtHostMpcService } from './extHostMcp.js'; import { ExtHostUrls, IExtHostUrlsService } from './extHostUrls.js'; import { ExtHostProgress, IExtHostProgress } from './extHostProgress.js'; +import { ExtHostDataChannels, IExtHostDataChannels } from './extHostDataChannels.js'; registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed); registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed); @@ -62,3 +63,4 @@ registerSingleton(IExtHostSecretState, ExtHostSecretState, InstantiationType.Eag registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs, InstantiationType.Eager); registerSingleton(IExtHostVariableResolverProvider, ExtHostVariableResolverProviderService, InstantiationType.Eager); registerSingleton(IExtHostMpcService, ExtHostMcpService, InstantiationType.Eager); +registerSingleton(IExtHostDataChannels, ExtHostDataChannels, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2c471898036..ca43b1dad38 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3019,6 +3019,13 @@ export interface MainThreadMcpShape { $getTokenFromServerMetadata(id: number, authorizationServer: UriComponents, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined): Promise; } +export interface MainThreadDataChannelsShape extends IDisposable { +} + +export interface ExtHostDataChannelsShape { + $onDidReceiveData(channelId: string, data: unknown): void; +} + export interface ExtHostLocalizationShape { getMessage(extensionId: string, details: IStringDetails): string; getBundle(extensionId: string): { [key: string]: string } | undefined; @@ -3182,6 +3189,7 @@ export const MainContext = { MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), MainThreadAiSettingsSearch: createProxyIdentifier('MainThreadAiSettingsSearch'), + MainThreadDataChannels: createProxyIdentifier('MainThreadDataChannels'), }; export const ExtHostContext = { @@ -3254,4 +3262,5 @@ export const ExtHostContext = { ExtHostTelemetry: createProxyIdentifier('ExtHostTelemetry'), ExtHostLocalization: createProxyIdentifier('ExtHostLocalization'), ExtHostMcp: createProxyIdentifier('ExtHostMcp'), + ExtHostDataChannels: createProxyIdentifier('ExtHostDataChannels'), }; diff --git a/src/vs/workbench/api/common/extHostDataChannels.ts b/src/vs/workbench/api/common/extHostDataChannels.ts new file mode 100644 index 00000000000..44829679f8a --- /dev/null +++ b/src/vs/workbench/api/common/extHostDataChannels.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { ExtHostDataChannelsShape } from './extHost.protocol.js'; +import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; + +export interface IExtHostDataChannels extends ExtHostDataChannelsShape { + readonly _serviceBrand: undefined; + createDataChannel(extension: IExtensionDescription, channelId: string): vscode.DataChannel; +} + +export const IExtHostDataChannels = createDecorator('IExtHostDataChannels'); + +export class ExtHostDataChannels implements IExtHostDataChannels { + declare readonly _serviceBrand: undefined; + + private readonly _channels = new Map>(); + + constructor() { + } + + createDataChannel(extension: IExtensionDescription, channelId: string): vscode.DataChannel { + checkProposedApiEnabled(extension, 'dataChannels'); + + let channel = this._channels.get(channelId); + if (!channel) { + channel = new DataChannelImpl(channelId); + this._channels.set(channelId, channel); + } + return channel; + } + + $onDidReceiveData(channelId: string, data: any): void { + const channel = this._channels.get(channelId); + if (channel) { + channel._fireDidReceiveData(data); + } + } +} + +class DataChannelImpl extends Disposable implements vscode.DataChannel { + private readonly _onDidReceiveData = new Emitter>(); + public readonly onDidReceiveData: Event> = this._onDidReceiveData.event; + + constructor(private readonly channelId: string) { + super(); + this._register(this._onDidReceiveData); + } + + _fireDidReceiveData(data: T): void { + this._onDidReceiveData.fire({ data }); + } + + override toString(): string { + return `DataChannel(${this.channelId})`; + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts index 59e8d2245e8..7683dc7afe8 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editSourceTrackingFeature.ts @@ -17,13 +17,16 @@ import { IModelDeltaDecoration } from '../../../../editor/common/model.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; import { EditSource } from './documentWithAnnotatedEdits.js'; import { EditSourceTrackingImpl } from './editSourceTrackingImpl.js'; +import { DataChannelForwardingTelemetryService } from './forwardingTelemetryService.js'; import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; import { VSCodeWorkspace } from './vscodeObservableWorkspace.js'; @@ -71,7 +74,11 @@ export class EditTrackingFeature extends Disposable { return map; }); - const impl = this._register(this._instantiationService.createInstance(EditSourceTrackingImpl, this._workspace, (doc, reader) => { + const instantiationServiceWithInterceptedTelemetry = this._instantiationService.createChild(new ServiceCollection( + [ITelemetryService, this._instantiationService.createInstance(DataChannelForwardingTelemetryService)] + )); + + const impl = this._register(instantiationServiceWithInterceptedTelemetry.createInstance(EditSourceTrackingImpl, this._workspace, (doc, reader) => { const map = visibleUris.read(reader); return map.get(doc.uri.toString()) !== undefined; }, this._editSourceDetailsEnabled)); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/forwardingTelemetryService.ts b/src/vs/workbench/contrib/editTelemetry/browser/forwardingTelemetryService.ts new file mode 100644 index 00000000000..9a0a9a39538 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/forwardingTelemetryService.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ClassifiedEvent, OmitMetadata, IGDPRProperty, StrictPropertyCheck } from '../../../../platform/telemetry/common/gdprTypings.js'; +import { ITelemetryData, ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; +import { IDataChannelService } from '../../../services/dataChannel/common/dataChannel.js'; + +export class InterceptingTelemetryService implements ITelemetryService { + _serviceBrand: undefined; + + constructor( + private readonly _baseService: ITelemetryService, + private readonly _intercept: (eventName: string, data?: ITelemetryData) => void, + ) { } + + get telemetryLevel(): TelemetryLevel { + return this._baseService.telemetryLevel; + } + + get sessionId(): string { + return this._baseService.sessionId; + } + + get machineId(): string { + return this._baseService.machineId; + } + + get sqmId(): string { + return this._baseService.sqmId; + } + + get devDeviceId(): string { + return this._baseService.devDeviceId; + } + + get firstSessionDate(): string { + return this._baseService.firstSessionDate; + } + + get msftInternal(): boolean | undefined { + return this._baseService.msftInternal; + } + + get sendErrorTelemetry(): boolean { + return this._baseService.sendErrorTelemetry; + } + + publicLog(eventName: string, data?: ITelemetryData): void { + this._intercept(eventName, data); + this._baseService.publicLog(eventName, data); + } + + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void { + this._intercept(eventName, data); + this._baseService.publicLog2(eventName, data); + } + + publicLogError(errorEventName: string, data?: ITelemetryData): void { + this._intercept(errorEventName, data); + this._baseService.publicLogError(errorEventName, data); + } + + publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): void { + this._intercept(eventName, data); + this._baseService.publicLogError2(eventName, data); + } + + setExperimentProperty(name: string, value: string): void { + this._baseService.setExperimentProperty(name, value); + } +} + +export interface IEditTelemetryData { + eventName: string; + data: Record; +} + +export class DataChannelForwardingTelemetryService extends InterceptingTelemetryService { + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IDataChannelService dataChannelService: IDataChannelService, + ) { + super(telemetryService, (eventName, data) => { + dataChannelService.getDataChannel('editTelemetry').sendData({ eventName, data }); + }); + } +} diff --git a/src/vs/workbench/services/dataChannel/browser/dataChannelService.ts b/src/vs/workbench/services/dataChannel/browser/dataChannelService.ts new file mode 100644 index 00000000000..36e91fbb420 --- /dev/null +++ b/src/vs/workbench/services/dataChannel/browser/dataChannelService.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IDataChannelService, CoreDataChannel, IDataChannelEvent } from '../common/dataChannel.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; + +export class DataChannelService extends Disposable implements IDataChannelService { + declare readonly _serviceBrand: undefined; + + private readonly _onDidSendData = this._register(new Emitter()); + readonly onDidSendData = this._onDidSendData.event; + + constructor() { + super(); + } + + getDataChannel(channelId: string): CoreDataChannel { + return new CoreDataChannelImpl(channelId, this._onDidSendData); + } +} + +class CoreDataChannelImpl implements CoreDataChannel { + constructor( + private readonly channelId: string, + private readonly _onDidSendData: Emitter + ) { } + + sendData(data: T): void { + this._onDidSendData.fire({ + channelId: this.channelId, + data + }); + } +} + +registerSingleton(IDataChannelService, DataChannelService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/dataChannel/common/dataChannel.ts b/src/vs/workbench/services/dataChannel/common/dataChannel.ts new file mode 100644 index 00000000000..8e5b4aa98b1 --- /dev/null +++ b/src/vs/workbench/services/dataChannel/common/dataChannel.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IDataChannelService = createDecorator('dataChannelService'); + +export interface IDataChannelService { + readonly _serviceBrand: undefined; + + readonly onDidSendData: Event; + + getDataChannel(channelId: string): CoreDataChannel; +} + +export interface CoreDataChannel { + sendData(data: T): void; +} + +export interface IDataChannelEvent { + channelId: string; + data: T; +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 0711a4f4442..95286bd0669 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -130,6 +130,7 @@ import './services/userActivity/browser/userActivityBrowser.js'; import './services/editor/browser/editorPaneService.js'; import './services/editor/common/customEditorLabelService.js'; import './services/coreExperimentation/common/coreExperimentationService.js'; +import './services/dataChannel/browser/dataChannelService.js'; import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; import { GlobalExtensionEnablementService } from '../platform/extensionManagement/common/extensionEnablementService.js'; diff --git a/src/vscode-dts/vscode.proposed.dataChannels.d.ts b/src/vscode-dts/vscode.proposed.dataChannels.d.ts new file mode 100644 index 00000000000..593c2de5955 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.dataChannels.d.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export namespace env { + export function getDataChannel(channelId: string): DataChannel; + } + + export interface DataChannel { + onDidReceiveData: Event>; + } + + export interface DataChannelEvent { + data: T; + } +}