diff --git a/media/options-widget.css b/media/options-widget.css index d77c40d..d2ec083 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -77,19 +77,20 @@ padding-bottom: 16px; } -.form-textfield > input { +.form-textfield>input { width: 150px; } -.form-texfield-long > input { +.form-texfield-long>input { width: 200px; } .form-options button[type="submit"] { /* Match height of inputs */ height: calc(var(--input-minHeight) * 1px); - margin-top: 1rem; + margin-top: 1.1rem; align-self: start; + margin-right: auto; } .form-options-memory-read-argument-hint { @@ -106,7 +107,7 @@ font-size: 11px; text-transform: uppercase; line-height: 22px; - margin:0; + margin: 0; font-weight: normal; } @@ -145,10 +146,14 @@ padding-bottom: 0.5rem; } +.advanced-options-session { + align-self: start; + width: 250px; +} + .advanced-options-toggle { - margin-left: auto; align-self: start; - margin-top: 1.1rem + margin-top: 1.3rem } .p-accordion-content .advanced-options-content { @@ -187,6 +192,7 @@ margin-top: 0px; width: 210px; } + .advanced-options-panel::-webkit-scrollbar-track { background-color: var(--vscode-dropdown-listBackground); } diff --git a/src/common/messaging.ts b/src/common/messaging.ts index fb5954d..df57f02 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -38,6 +38,11 @@ export type StoreMemoryResult = void; export type ApplyMemoryArguments = URI | undefined; export type ApplyMemoryResult = MemoryOptions; +export interface Session { + id: string + name: string; +} + export interface SessionContext { sessionId?: string; canRead: boolean; @@ -50,6 +55,8 @@ export const readyType: NotificationType = { method: 'ready' }; export const setMemoryViewSettingsType: NotificationType> = { method: 'setMemoryViewSettings' }; export const setTitleType: NotificationType = { method: 'setTitle' }; export const memoryWrittenType: NotificationType = { method: 'memoryWritten' }; +export const sessionsChangedType: NotificationType = { method: 'sessionsChanged' }; +export const setSessionType: NotificationType = { method: 'setSession' }; export const sessionContextChangedType: NotificationType = { method: 'sessionContextChanged' }; // Requests diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index d8c6905..5adf924 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -18,7 +18,7 @@ import * as vscode from 'vscode'; import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; import { ContextTracker } from '../../plugin/context-tracker'; -import { MemoryProvider } from '../../plugin/memory-provider'; +import { MemoryProviderManager } from '../../plugin/memory-provider-manager'; import { MemoryStorage } from '../../plugin/memory-storage'; import { MemoryWebview } from '../../plugin/memory-webview-main'; import { SessionTracker } from '../../plugin/session-tracker'; @@ -27,14 +27,14 @@ export const activate = async (context: vscode.ExtensionContext): Promise(); + + constructor(protected adapterRegistry: AdapterRegistry, protected sessionTracker: SessionTracker) { + } + + public activate(context: vscode.ExtensionContext): void { + const createDebugAdapterTracker = (session: vscode.DebugSession): vscode.ProviderResult => { + const handlerForSession = this.adapterRegistry.getHandlerForSession(session.type); + const contributedTracker = handlerForSession?.initializeAdapterTracker?.(session); + return contributedTracker; + }; + context.subscriptions.push(vscode.debug.registerDebugAdapterTrackerFactory('*', { createDebugAdapterTracker })); + } + + public getProvider(sessionId: string | undefined): MemoryProvider { + const session = this.sessionTracker.assertSession(sessionId); + + if (!this.memoryProviders.has(session)) { + this.memoryProviders.set(session, new MemoryProvider(session.id, this.adapterRegistry, this.sessionTracker)); + } + + return this.memoryProviders.get(session)!; + } +} diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 71584a2..88ab12a 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -15,7 +15,6 @@ ********************************************************************************/ import { DebugProtocol } from '@vscode/debugprotocol'; -import * as vscode from 'vscode'; import { sendRequest } from '../common/debug-requests'; import { stringToBytesMemory } from '../common/memory'; import { VariableRange } from '../common/memory-range'; @@ -24,36 +23,24 @@ import { MemoryDisplaySettingsContribution } from '../common/webview-configurati import { AdapterRegistry } from './adapter-registry/adapter-registry'; import { isSessionEvent, SessionTracker } from './session-tracker'; -export interface LabeledUint8Array extends Uint8Array { - label?: string; -} - export class MemoryProvider { protected scheduledOnDidMemoryWriteEvents: { [sessionidmemoryReference: string]: ((response: WriteMemoryResult) => void) | undefined } = {}; - constructor(protected adapterRegistry: AdapterRegistry, protected sessionTracker: SessionTracker) { + constructor(protected sessionId: string, protected adapterRegistry: AdapterRegistry, protected sessionTracker: SessionTracker) { this.sessionTracker.onSessionEvent(event => { - if (isSessionEvent('memory-written', event)) { + if (isSessionEvent('memory-written', event) && event.session.raw.id === this.sessionId) { delete this.scheduledOnDidMemoryWriteEvents[event.session.raw.id + '_' + event.data.memoryReference]; } }); } - public activate(context: vscode.ExtensionContext): void { - const createDebugAdapterTracker = (session: vscode.DebugSession): vscode.ProviderResult => { - const handlerForSession = this.adapterRegistry.getHandlerForSession(session.type); - const contributedTracker = handlerForSession?.initializeAdapterTracker?.(session); - return contributedTracker; - }; - context.subscriptions.push(vscode.debug.registerDebugAdapterTrackerFactory('*', { createDebugAdapterTracker })); - } - public async readMemory(args: DebugProtocol.ReadMemoryArguments): Promise { - return sendRequest(this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsReadMemoryRequest', 'read memory'), 'readMemory', args); + const session = this.sessionTracker.assertSession(this.sessionId); + return sendRequest(this.sessionTracker.assertDebugCapability(session, 'supportsReadMemoryRequest', 'read memory'), 'readMemory', args); } public async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { - const session = this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsWriteMemoryRequest', 'write memory'); + const session = this.sessionTracker.assertDebugCapability(this.sessionTracker.assertSession(this.sessionId), 'supportsWriteMemoryRequest', 'write memory'); // Schedule a emit in case we don't retrieve a memory event this.scheduledOnDidMemoryWriteEvents[session.id + '_' + args.memoryReference] = response => { // We only send out a custom event if we don't expect the client to handle the memory event @@ -72,26 +59,26 @@ export class MemoryProvider { } public async getVariables(variableArguments: DebugProtocol.ReadMemoryArguments): Promise { - const session = this.sessionTracker.assertActiveSession('get variables'); + const session = this.sessionTracker.assertSession(this.sessionId, 'get variables'); const handler = this.adapterRegistry?.getHandlerForSession(session.type); if (handler?.getResidents) { return handler.getResidents(session, variableArguments); } return handler?.getVariables?.(session) ?? []; } public async getAddressOfVariable(variableName: string): Promise { - const session = this.sessionTracker.assertActiveSession('get address of variable'); + const session = this.sessionTracker.assertSession(this.sessionId, 'get address of variable'); const handler = this.adapterRegistry?.getHandlerForSession(session.type); return handler?.getAddressOfVariable?.(session, variableName); } public async getSizeOfVariable(variableName: string): Promise { - const session = this.sessionTracker.assertActiveSession('get size of variable'); + const session = this.sessionTracker.assertSession(this.sessionId, 'get size of variable'); const handler = this.adapterRegistry?.getHandlerForSession(session.type); return handler?.getSizeOfVariable?.(session, variableName); } public async getMemoryDisplaySettingsContribution(): Promise { - const session = this.sessionTracker.assertActiveSession('get memory display settings contribution'); + const session = this.sessionTracker.assertSession(this.sessionId, 'get memory display settings contribution'); const handler = this.adapterRegistry?.getHandlerForSession(session.type); return handler?.getMemoryDisplaySettings?.(session) ?? {}; } diff --git a/src/plugin/memory-storage.ts b/src/plugin/memory-storage.ts index a19036b..f645172 100644 --- a/src/plugin/memory-storage.ts +++ b/src/plugin/memory-storage.ts @@ -28,9 +28,11 @@ import { toHexStringWithRadixMarker } from '../common/memory-range'; import { ApplyMemoryArguments, ApplyMemoryResult, MemoryOptions, StoreMemoryArguments } from '../common/messaging'; import { isWebviewContext } from '../common/webview-context'; import { MemoryProvider } from './memory-provider'; +import { MemoryProviderManager } from './memory-provider-manager'; +import { SessionTracker } from './session-tracker'; -export const StoreCommandType = `${manifest.PACKAGE_NAME}.store-file`; -export const ApplyCommandType = `${manifest.PACKAGE_NAME}.apply-file`; +const StoreCommandType = `${manifest.PACKAGE_NAME}.store-file`; +const ApplyCommandType = `${manifest.PACKAGE_NAME}.apply-file`; const VALID_FILE_NAME_CHARS = /[^a-zA-Z0-9 _-]/g; @@ -49,19 +51,26 @@ interface ApplyMemoryOptions { uri: vscode.Uri; } +const getActiveSession = () => vscode.debug.activeDebugSession; + export class MemoryStorage { - constructor(protected memoryProvider: MemoryProvider) { + constructor(protected sessionTracker: SessionTracker, protected memoryProviderManager: MemoryProviderManager) { } public activate(context: vscode.ExtensionContext): void { context.subscriptions.push( - vscode.commands.registerCommand(StoreCommandType, args => this.storeMemory(args)), - vscode.commands.registerCommand(ApplyCommandType, args => this.applyMemory(args)) + vscode.commands.registerCommand(StoreCommandType, args => this.storeMemory(getActiveSession()?.id, args)), + vscode.commands.registerCommand(ApplyCommandType, args => this.applyMemory(getActiveSession()?.id, args)) ); } - public async storeMemory(args?: StoreMemoryArguments): Promise { - const providedDefaultOptions = await this.storeArgsToOptions(args); + public async storeMemory(sessionId: string | undefined, args?: StoreMemoryArguments): Promise { + // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. + // However, we want to fail early in case the user tries to execute a disabled command + this.sessionTracker.assertDebugCapability(this.sessionTracker.assertSession(sessionId), 'supportsReadMemoryRequest', 'store memory'); + + const memoryProvider = this.memoryProviderManager.getProvider(sessionId); + const providedDefaultOptions = await this.storeArgsToOptions(memoryProvider, args); const options = await this.getStoreMemoryOptions(providedDefaultOptions); if (!options) { // user aborted process @@ -70,7 +79,7 @@ export class MemoryStorage { const { outputFile, ...readArgs } = options; try { - const memoryResponse = await this.memoryProvider.readMemory(readArgs); + const memoryResponse = await memoryProvider.readMemory(readArgs); const memory = createMemoryFromRead(memoryResponse); const memoryMap = new MemoryMap({ [Number(memory.address)]: memory.bytes }); await vscode.workspace.fs.writeFile(outputFile, new TextEncoder().encode(memoryMap.asHexString())); @@ -89,7 +98,7 @@ export class MemoryStorage { } } - protected async storeArgsToOptions(args?: StoreMemoryArguments): Promise> { + protected async storeArgsToOptions(memoryProvider: MemoryProvider, args?: StoreMemoryArguments): Promise> { if (!args) { return {}; } @@ -99,8 +108,8 @@ export class MemoryStorage { if (isVariablesContext(args)) { try { const variableName = args.variable.evaluateName ?? args.variable.name; - const count = await this.memoryProvider.getSizeOfVariable(variableName); - const memoryReference = args.variable.memoryReference ?? await this.memoryProvider.getAddressOfVariable(variableName); + const count = await memoryProvider.getSizeOfVariable(variableName); + const memoryReference = args.variable.memoryReference ?? await memoryProvider.getAddressOfVariable(variableName); return { count: Number(count), memoryReference, offset: 0, proposedOutputName: variableName }; } catch (error) { // ignore, we are just using them as default values @@ -153,7 +162,12 @@ export class MemoryStorage { return { memoryReference, offset: Number(offset), count: Number(count), outputFile }; } - public async applyMemory(args?: ApplyMemoryArguments): Promise { + public async applyMemory(sessionId: string | undefined, args?: ApplyMemoryArguments): Promise { + // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. + // However, we want to fail early in case the user tries to execute a disabled command + this.sessionTracker.assertDebugCapability(this.sessionTracker.assertSession(sessionId), 'supportsWriteMemoryRequest', 'apply memory'); + + const memoryProvider = this.memoryProviderManager.getProvider(sessionId); const providedDefaultOptions = await this.applyArgsToOptions(args); const options = await this.getApplyMemoryOptions(providedDefaultOptions); if (!options) { @@ -169,7 +183,7 @@ export class MemoryStorage { memoryReference = toHexStringWithRadixMarker(address); count = memory.length; const data = bytesToStringMemory(memory); - await this.memoryProvider.writeMemory({ memoryReference, data }); + await memoryProvider.writeMemory({ memoryReference, data }); } await vscode.window.showInformationMessage(`Memory from '${vscode.workspace.asRelativePath(options.uri)}' applied.`); return { memoryReference, count, offset: 0 }; diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 9d396db..2ba550d 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -21,6 +21,7 @@ import { isVariablesContext } from '../common/external-views'; import * as manifest from '../common/manifest'; import { VariableRange } from '../common/memory-range'; import { + ApplyMemoryResult, applyMemoryType, getVariablesType, getWebviewSelectionType, @@ -31,10 +32,13 @@ import { ReadMemoryResult, readMemoryType, readyType, + Session, SessionContext, sessionContextChangedType, + sessionsChangedType, setMemoryViewSettingsType, setOptionsType, + setSessionType, setTitleType, showAdvancedOptionsType, StoreMemoryArguments, @@ -48,8 +52,9 @@ import { MemoryDisplaySettings, MemoryDisplaySettingsContribution, MemoryViewSet import { getVisibleColumns, isWebviewVariableContext, WebviewContext } from '../common/webview-context'; import { AddressPaddingOptions } from '../webview/utils/view-types'; import { outputChannelLogger } from './logger'; -import { MemoryProvider } from './memory-provider'; -import { ApplyCommandType, StoreCommandType } from './memory-storage'; +import type { MemoryProvider } from './memory-provider'; +import type { MemoryProviderManager } from './memory-provider-manager'; +import type { MemoryStorage } from './memory-storage'; import { isSessionEvent, SessionEvent, SessionTracker } from './session-tracker'; const CONFIGURABLE_COLUMNS = [ @@ -71,11 +76,15 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { public static GetWebviewSelectionCommandType = `${manifest.PACKAGE_NAME}.get-webview-selection`; protected messenger: Messenger; - protected panelIndices: number = 1; - - public constructor(protected extensionUri: vscode.Uri, protected memoryProvider: MemoryProvider, protected sessionTracker: SessionTracker) { - this.messenger = new Messenger(); + protected participantSessions = new Map(); + + public constructor( + protected extensionUri: vscode.Uri, + protected memoryProviderManager: MemoryProviderManager, + protected sessionTracker: SessionTracker, + protected memoryStorage: MemoryStorage) { + this.messenger = new Messenger({ ignoreHiddenViews: false }); } public activate(context: vscode.ExtensionContext): void { @@ -84,7 +93,9 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { vscode.commands.registerCommand(MemoryWebview.ShowCommandType, () => this.show()), vscode.commands.registerCommand(MemoryWebview.VariableCommandType, async args => { if (isVariablesContext(args)) { - const memoryReference = args.variable.memoryReference ?? await this.memoryProvider.getAddressOfVariable(args.variable.name); + const sessionId = args.sessionId || vscode.debug.activeDebugSession?.id; + const memoryProvider = this.memoryProviderManager.getProvider(sessionId); + const memoryReference = args.variable.memoryReference ?? await memoryProvider.getAddressOfVariable(args.variable.name); this.show({ memoryReference }); } }), @@ -203,18 +214,16 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: MemoryOptions): void { const participant = this.messenger.registerWebviewPanel(panel); const disposables = [ - this.messenger.onNotification(readyType, async () => { - this.setSessionContext(participant, this.createContext()); - await this.setMemoryDisplaySettings(participant, panel.title); - }, { sender: participant }), + this.messenger.onNotification(readyType, () => this.ready(participant, panel), { sender: participant }), this.messenger.onRequest(setOptionsType, newOptions => { options = { ...options, ...newOptions }; }, { sender: participant }), this.messenger.onRequest(logMessageType, message => outputChannelLogger.info('[webview]:', message), { sender: participant }), - this.messenger.onRequest(readMemoryType, request => this.readMemory(request), { sender: participant }), - this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), - this.messenger.onRequest(getVariablesType, request => this.getVariables(request), { sender: participant }), + this.messenger.onRequest(readMemoryType, request => this.readMemory(participant, request), { sender: participant }), + this.messenger.onRequest(writeMemoryType, request => this.writeMemory(participant, request), { sender: participant }), + this.messenger.onRequest(getVariablesType, request => this.getVariables(participant, request), { sender: participant }), this.messenger.onNotification(setTitleType, title => { panel.title = title; }, { sender: participant }), - this.messenger.onRequest(storeMemoryType, args => this.storeMemory(args), { sender: participant }), - this.messenger.onRequest(applyMemoryType, () => this.applyMemory(), { sender: participant }), + this.messenger.onNotification(setSessionType, sessionId => this.setSession(participant, sessionId), { sender: participant }), + this.messenger.onRequest(storeMemoryType, args => this.storeMemory(participant, args), { sender: participant }), + this.messenger.onRequest(applyMemoryType, () => this.applyMemory(participant), { sender: participant }), this.sessionTracker.onSessionEvent(event => this.handleSessionEvent(participant, event)) ]; panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); @@ -222,7 +231,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected async setMemoryDisplaySettings(messageParticipant: WebviewIdMessageParticipant, title?: string, includeContributions: boolean = true): Promise { const defaultSettings = this.getDefaultMemoryDisplaySettings(); - const settingsContribution = includeContributions ? await this.getMemoryDisplaySettingsContribution() : {}; + const settingsContribution = includeContributions ? await this.getMemoryDisplaySettingsContribution(messageParticipant) : {}; const settings = settingsContribution.settings ? { ...settingsContribution.settings, hasDebuggerDefaults: true } : {}; this.setMemoryViewSettings(messageParticipant, { messageParticipant, @@ -241,6 +250,10 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, settings); } + protected setSessions(webviewParticipant: WebviewIdMessageParticipant, sessions: Session[]): void { + this.messenger.sendNotification(sessionsChangedType, webviewParticipant, sessions); + } + protected setSessionContext(webviewParticipant: WebviewIdMessageParticipant, context: SessionContext): void { this.messenger.sendNotification(sessionContextChangedType, webviewParticipant, context); } @@ -266,32 +279,70 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { }; } - protected async getMemoryDisplaySettingsContribution(): Promise { + protected async getMemoryDisplaySettingsContribution(messageParticipant: WebviewIdMessageParticipant): Promise { const memoryInspectorSettings = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const allowDebuggerOverwriteSettings = memoryInspectorSettings.get(manifest.CONFIG_ALLOW_DEBUGGER_OVERWRITE_SETTINGS, true); if (allowDebuggerOverwriteSettings) { - return this.memoryProvider.getMemoryDisplaySettingsContribution(); + const memoryProvider = this.getMemoryProvider(messageParticipant); + return memoryProvider.getMemoryDisplaySettingsContribution(); } return { settings: {}, message: undefined }; } protected handleSessionEvent(participant: WebviewIdMessageParticipant, event: SessionEvent): void { - if (isSessionEvent('active', event)) { - this.setSessionContext(participant, this.createContext(event.session?.raw)); + const sessionId = this.participantSessions.get(participant); + + if (isSessionEvent('sessions-changed', event)) { + this.setSessions(participant, this.sessionTracker.getSessions()); + + // Current session may have stopped + if (!this.sessionTracker.validSession(sessionId)) { + this.participantSessions.delete(participant); + this.setSessionContext(participant, { + sessionId: undefined, + canRead: false, + canWrite: false, + stopped: false + }); + } + return; } - // we are only interested in the events of the active session - if (!event.session?.active) { + + if (isSessionEvent('active', event)) { + // If our participanr is not associated with a session, set it + if (!this.participantSessions.has(participant)) { + const session = this.sessionTracker.assertSession(event.session?.raw.id); + this.participantSessions.set(participant, session.id); + this.setSessionContext(participant, this.createContext(session)); + } + return; } - if (isSessionEvent('memory-written', event)) { - this.messenger.sendNotification(memoryWrittenType, participant, event.data); - } else { - this.setSessionContext(participant, this.createContext(event.session.raw)); + + // we are only interested in the events of the current session + if (sessionId && event.session && sessionId === event.session.raw.id) { + if (isSessionEvent('memory-written', event)) { + this.messenger.sendNotification(memoryWrittenType, participant, event.data); + } else { + this.setSessionContext(participant, this.createContext(event.session.raw)); + } } } - protected createContext(session = this.sessionTracker.activeSession): SessionContext { + protected async ready(participant: WebviewIdMessageParticipant, panel: vscode.WebviewPanel): Promise { + this.setSession(participant, vscode.debug.activeDebugSession?.id); + this.setSessions(participant, this.sessionTracker.getSessions()); + await this.setMemoryDisplaySettings(participant, panel.title); + } + + protected async setSession(participant: WebviewIdMessageParticipant, sessionId: string | undefined): Promise { + const session = this.sessionTracker.assertSession(sessionId); + this.participantSessions.set(participant, session.id); + this.setSessionContext(participant, this.createContext(session)); + } + + protected createContext(session: vscode.DebugSession): SessionContext { const sessionId = session?.id; return { sessionId, @@ -301,25 +352,28 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { }; } - protected async readMemory(request: ReadMemoryArguments): Promise { + protected async readMemory(participant: WebviewIdMessageParticipant, request: ReadMemoryArguments): Promise { + const memoryProvider = this.getMemoryProvider(participant); try { - return await this.memoryProvider.readMemory(request); + return await memoryProvider.readMemory(request); } catch (err) { this.logError('Error fetching memory', err); } } - protected async writeMemory(request: WriteMemoryArguments): Promise { + protected async writeMemory(participant: WebviewIdMessageParticipant, request: WriteMemoryArguments): Promise { + const memoryProvider = this.getMemoryProvider(participant); try { - return await this.memoryProvider.writeMemory(request); + return await memoryProvider.writeMemory(request); } catch (err) { this.logError('Error writing memory', err); } } - protected async getVariables(request: ReadMemoryArguments): Promise { + protected async getVariables(participant: WebviewIdMessageParticipant, request: ReadMemoryArguments): Promise { + const memoryProvider = this.getMemoryProvider(participant); try { - return await this.memoryProvider.getVariables(request); + return await memoryProvider.getVariables(request); } catch (err) { this.logError('Error fetching variables', err); return []; @@ -342,21 +396,22 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns }); } - protected async storeMemory(storeArguments: StoreMemoryArguments): Promise { - // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. - // However, we want to fail early in case the user tries to execute a disabled command - this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsReadMemoryRequest', 'store memory'); - return vscode.commands.executeCommand(StoreCommandType, storeArguments); + protected async storeMemory(participant: WebviewIdMessageParticipant, args: StoreMemoryArguments): Promise { + const sessionId = this.participantSessions.get(participant); + this.memoryStorage.storeMemory(sessionId, args); } - protected async applyMemory(): Promise { - // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. - // However, we want to fail early in case the user tries to execute a disabled command - this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsWriteMemoryRequest', 'apply memory'); - return vscode.commands.executeCommand(ApplyCommandType); + protected async applyMemory(participant: WebviewIdMessageParticipant): Promise { + const sessionId = this.participantSessions.get(participant); + return this.memoryStorage.applyMemory(sessionId); } protected logError(msg: string, err: unknown): void { outputChannelLogger.error(msg, err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); } + + protected getMemoryProvider(messageParticipant: WebviewIdMessageParticipant): MemoryProvider { + const sessionId = this.participantSessions.get(messageParticipant); + return this.memoryProviderManager.getProvider(sessionId); + } } diff --git a/src/plugin/session-tracker.ts b/src/plugin/session-tracker.ts index d10529f..a0233ca 100644 --- a/src/plugin/session-tracker.ts +++ b/src/plugin/session-tracker.ts @@ -17,6 +17,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import * as vscode from 'vscode'; import { isDebugEvent, isDebugRequest, isDebugResponse } from '../common/debug-requests'; import { WrittenMemory } from '../common/memory-range'; +import type { Session } from '../common/messaging'; export interface SessionInfo { raw: vscode.DebugSession; @@ -52,11 +53,16 @@ export interface SessionContinuedEvent extends SessionEvent { session: SessionInfo; } +export interface SessionsChangedEvent extends SessionEvent { + event: 'sessions-changed'; +} + export interface SessionEvents { 'active': ActiveSessionChangedEvent, 'memory-written': SessionMemoryWrittenEvent, 'continued': SessionContinuedEvent, 'stopped': SessionStoppedEvent + 'sessions-changed': SessionsChangedEvent } export type DebugCapability = keyof DebugProtocol.Capabilities; @@ -95,7 +101,10 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { let info = this._sessionInfo.get(session.id); if (!info) { info = { raw: session }; - this._sessionInfo.set(session.id, info); + if (this._sessionInfo.has(session.id)) { + // Only update session if it is active + this._sessionInfo.set(session.id, info); + } } return info; } @@ -113,10 +122,12 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { protected async sessionWillStart(session: vscode.DebugSession): Promise { this._sessionInfo.set(session.id, { raw: session }); + this.fireSessionEvent(session, 'sessions-changed', undefined); } protected sessionWillStop(session: vscode.DebugSession): void { this._sessionInfo.delete(session.id); + this.fireSessionEvent(session, 'sessions-changed', undefined); } protected willSendClientMessage(session: vscode.DebugSession, message: unknown): void { @@ -139,30 +150,31 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { } } - get activeSession(): vscode.DebugSession | undefined { - return vscode.debug.activeDebugSession; + public getSessions(): Session[] { + return Array.from(this._sessionInfo.values()) + .map(info => ({ id: info.raw.id, name: info.raw.name })); } - assertActiveSession(action: string = 'get session'): vscode.DebugSession { - if (!this.activeSession) { - throw new Error(`Cannot ${action}. No active debug session.`); - } - return this.activeSession; + validSession(sessionId: string | undefined): boolean { + return !!sessionId && this._sessionInfo.has(sessionId); } - isActive(session = this.activeSession): boolean { - return !!session && vscode.debug.activeDebugSession?.id === session?.id; + assertSession(sessionId: string | undefined, action: string = 'get session'): vscode.DebugSession { + if (!this.validSession) { + throw new Error(`Cannot ${action}. No active debug session.`); + } + return this._sessionInfo.get(sessionId!)!.raw; } - isStopped(session = this.activeSession): boolean { + isStopped(session: vscode.DebugSession): boolean { return !!session && !!this.sessionInfo(session).stopped; } - hasDebugCapability(session = this.activeSession, capability: DebugCapability): boolean { + hasDebugCapability(session: vscode.DebugSession, capability: DebugCapability): boolean { return !!session && !!this.sessionInfo(session).debugCapabilities?.[capability]; } - assertDebugCapability(session = this.assertActiveSession(), capability: DebugCapability, action: string = 'execute action'): vscode.DebugSession { + assertDebugCapability(session: vscode.DebugSession, capability: DebugCapability, action: string = 'execute action'): vscode.DebugSession { if (!this.hasDebugCapability(session, capability)) { throw new Error(`Cannot ${action}. Session does not have capability '${capability}'.`); } @@ -173,7 +185,7 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { return !!session && !!this.sessionInfo(session).clientCapabilities?.[capability]; } - assertClientCapability(session = this.assertActiveSession(), capability: ClientCapability, action: string = 'execute action'): vscode.DebugSession { + assertClientCapability(session: vscode.DebugSession, capability: ClientCapability, action: string = 'execute action'): vscode.DebugSession { if (!this.hasClientCapability(session, capability)) { throw new Error(`Cannot ${action}. Client does not have capability '${capability}'.`); } diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index 1c46fbf..952af30 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import * as manifest from '../../common/manifest'; import { Memory } from '../../common/memory'; -import { WebviewSelection } from '../../common/messaging'; +import { Session, WebviewSelection } from '../../common/messaging'; import { MemoryOptions, ReadMemoryArguments, SessionContext } from '../../common/messaging'; import { MemoryDataDisplaySettings } from '../../common/webview-configuration'; import { ColumnStatus } from '../columns/column-contribution-service'; @@ -30,6 +30,8 @@ import { OptionsWidget } from './options-widget'; interface MemoryWidgetProps extends MemoryDataDisplaySettings { messageParticipant: WebviewIdMessageParticipant; + sessions: Session[]; + updateSession: (sessionId: string) => void; sessionContext: SessionContext; configuredReadArguments: Required; activeReadArguments: Required; @@ -87,6 +89,8 @@ export class MemoryWidget extends React.Component { + sessions: Session[] + updateSession: (sessionId: string) => void; sessionContext: SessionContext; configuredReadArguments: Required; activeReadArguments: Required; @@ -74,6 +76,7 @@ const enum InputId { RefreshOnStop = 'refresh-on-stop', PeriodicRefresh = 'periodic-refresh', PeriodicRefreshInterval = 'periodic-refresh-interval', + SessionSelect = 'session-select' } interface OptionsForm { @@ -165,6 +168,23 @@ export class OptionsWidget extends React.Component + + ({ label: session.name, value: session.id }))} + value={this.props.sessionContext.sessionId} + onChange={this.handleSessionDropdownChange} + /> + ; + return (
@@ -289,6 +309,7 @@ export class OptionsWidget extends React.Component Go + {sessionSelector}