diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index cebbbd3562081..546ecf27dcdf0 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -284,3 +284,104 @@ export function prefixedBufferReadable(prefix: VSBuffer, readable: VSBufferReada export function prefixedBufferStream(prefix: VSBuffer, stream: VSBufferReadableStream): VSBufferReadableStream { return streams.prefixedStream(prefix, stream, chunks => VSBuffer.concat(chunks)); } + +/** Decodes base64 to a uint8 array. URL-encoded and unpadded base64 is allowed. */ +export function decodeBase64(encoded: string) { + let building = 0; + let remainder = 0; + let bufi = 0; + + // The simpler way to do this is `Uint8Array.from(atob(str), c => c.charCodeAt(0))`, + // but that's about 10-20x slower than this function in current Chromium versions. + + const buffer = new Uint8Array(Math.floor(encoded.length / 4 * 3)); + const append = (value: number) => { + switch (remainder) { + case 3: + buffer[bufi++] = building | value; + remainder = 0; + break; + case 2: + buffer[bufi++] = building | (value >>> 2); + building = value << 6; + remainder = 3; + break; + case 1: + buffer[bufi++] = building | (value >>> 4); + building = value << 4; + remainder = 2; + break; + default: + building = value << 2; + remainder = 1; + } + }; + + for (let i = 0; i < encoded.length; i++) { + const code = encoded.charCodeAt(i); + // See https://datatracker.ietf.org/doc/html/rfc4648#section-4 + // This branchy code is about 3x faster than an indexOf on a base64 char string. + if (code >= 65 && code <= 90) { + append(code - 65); // A-Z starts ranges from char code 65 to 90 + } else if (code >= 97 && code <= 122) { + append(code - 97 + 26); // a-z starts ranges from char code 97 to 122, starting at byte 26 + } else if (code >= 48 && code <= 57) { + append(code - 48 + 52); // 0-9 starts ranges from char code 48 to 58, starting at byte 52 + } else if (code === 43 || code === 45) { + append(62); // "+" or "-" for URLS + } else if (code === 47 || code === 95) { + append(63); // "/" or "_" for URLS + } else if (code === 61) { + break; // "=" + } else { + throw new SyntaxError(`Unexpected base64 character ${encoded[i]}`); + } + } + + const unpadded = bufi; + while (remainder > 0) { + append(0); + } + + // slice is needed to account for overestimation due to padding + return VSBuffer.wrap(buffer).slice(0, unpadded); +} + +const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + +/** Encodes a buffer to a base64 string. */ +export function encodeBase64({ buffer }: VSBuffer, padded = true, urlSafe = false) { + const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet; + let output = ''; + + const remainder = buffer.byteLength % 3; + + let i = 0; + for (; i < buffer.byteLength - remainder; i += 3) { + const a = buffer[i + 0]; + const b = buffer[i + 1]; + const c = buffer[i + 2]; + + output += dictionary[a >>> 2]; + output += dictionary[(a << 4 | b >>> 4) & 0b111111]; + output += dictionary[(b << 2 | c >>> 6) & 0b111111]; + output += dictionary[c & 0b111111]; + } + + if (remainder === 1) { + const a = buffer[i + 0]; + output += dictionary[a >>> 2]; + output += dictionary[(a << 4) & 0b111111]; + if (padded) { output += '=='; } + } else if (remainder === 2) { + const a = buffer[i + 0]; + const b = buffer[i + 1]; + output += dictionary[a >>> 2]; + output += dictionary[(a << 4 | b >>> 4) & 0b111111]; + output += dictionary[(b << 2) & 0b111111]; + if (padded) { output += '='; } + } + + return output; +} diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index 461002be924cd..c2ff953d3c499 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; -import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; +import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, decodeBase64, encodeBase64, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; import { peekStream } from 'vs/base/common/stream'; suite('Buffer', () => { @@ -412,4 +412,53 @@ suite('Buffer', () => { assert.strictEqual(u2[0], 17); } }); + + suite('base64', () => { + /* + Generated with: + + const crypto = require('crypto'); + + for (let i = 0; i < 16; i++) { + const buf = crypto.randomBytes(i); + console.log(`[new Uint8Array([${Array.from(buf).join(', ')}]), '${buf.toString('base64')}'],`) + } + + */ + + const testCases: [Uint8Array, string][] = [ + [new Uint8Array([]), ''], + [new Uint8Array([56]), 'OA=='], + [new Uint8Array([209, 4]), '0QQ='], + [new Uint8Array([19, 57, 119]), 'Ezl3'], + [new Uint8Array([199, 237, 207, 112]), 'x+3PcA=='], + [new Uint8Array([59, 193, 173, 26, 242]), 'O8GtGvI='], + [new Uint8Array([81, 226, 95, 231, 116, 126]), 'UeJf53R+'], + [new Uint8Array([11, 164, 253, 85, 8, 6, 56]), 'C6T9VQgGOA=='], + [new Uint8Array([164, 16, 88, 88, 224, 173, 144, 114]), 'pBBYWOCtkHI='], + [new Uint8Array([0, 196, 99, 12, 21, 229, 78, 101, 13]), 'AMRjDBXlTmUN'], + [new Uint8Array([167, 114, 225, 116, 226, 83, 51, 48, 88, 114]), 'p3LhdOJTMzBYcg=='], + [new Uint8Array([75, 33, 118, 10, 77, 5, 168, 194, 59, 47, 59]), 'SyF2Ck0FqMI7Lzs='], + [new Uint8Array([203, 182, 165, 51, 208, 27, 123, 223, 112, 198, 127, 147]), 'y7alM9Abe99wxn+T'], + [new Uint8Array([154, 93, 222, 41, 117, 234, 250, 85, 95, 144, 16, 94, 18]), 'ml3eKXXq+lVfkBBeEg=='], + [new Uint8Array([246, 186, 88, 105, 192, 57, 25, 168, 183, 164, 103, 162, 243, 56]), '9rpYacA5Gai3pGei8zg='], + [new Uint8Array([149, 240, 155, 96, 30, 55, 162, 172, 191, 187, 33, 124, 169, 183, 254]), 'lfCbYB43oqy/uyF8qbf+'], + ]; + + test('encodes', () => { + for (const [bytes, expected] of testCases) { + assert.strictEqual(encodeBase64(VSBuffer.wrap(bytes)), expected); + } + }); + + test('decodes', () => { + for (const [expected, encoded] of testCases) { + assert.deepStrictEqual(new Uint8Array(decodeBase64(encoded).buffer), expected); + } + }); + + test('throws error on invalid encoding', () => { + assert.throws(() => decodeBase64('invalid!')); + }); + }); }); diff --git a/src/vs/base/test/common/mock.ts b/src/vs/base/test/common/mock.ts index 3b31f0fb3d55f..7a0d9fbefd72f 100644 --- a/src/vs/base/test/common/mock.ts +++ b/src/vs/base/test/common/mock.ts @@ -13,11 +13,11 @@ export function mock(): Ctor { return function () { } as any; } -export type MockObject = { [K in keyof T]: K extends keyof TP ? TP[K] : SinonStub }; +export type MockObject = { [K in keyof T]: K extends ExceptProps ? T[K] : SinonStub }; // Creates an object object that returns sinon mocks for every property. Optionally // takes base properties. -export function mockObject>(properties?: TP): MockObject { +export const mockObject = () => = {}>(properties?: TP): MockObject => { return new Proxy({ ...properties } as any, { get(target, key) { if (!target.hasOwnProperty(key)) { @@ -31,4 +31,4 @@ export function mockObject>(properties?: return true; }, }); -} +}; diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 91e9625030dbc..48a01082eb1d8 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,7 +9,7 @@ import { Expression, Variable, ExpressionContainer } from 'vs/workbench/contrib/ import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; -import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -19,6 +19,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; import { once } from 'vs/base/common/functional'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -131,7 +132,8 @@ export interface IExpressionTemplateData { name: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; - toDispose: IDisposable; + actionBar?: ActionBar; + elementDisposable: IDisposable[]; label: HighlightedLabel; } @@ -153,20 +155,26 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer, index: number, data: IExpressionTemplateData): void { - data.toDispose.dispose(); - data.toDispose = Disposable.None; const { element } = node; this.renderExpression(element, data, createMatches(node.filterData)); + if (data.actionBar) { + this.renderActionBar!(data.actionBar, element, data); + } const selectedExpression = this.debugService.getViewModel().getSelectedExpression(); if (element === selectedExpression?.expression || (element instanceof Variable && element.errorMessage)) { const options = this.getInputBoxOptions(element, !!selectedExpression?.settingWatch); if (options) { - data.toDispose = this.renderInputBox(data.name, data.value, data.inputBoxContainer, options); - return; + data.elementDisposable.push(this.renderInputBox(data.name, data.value, data.inputBoxContainer, options)); } } } @@ -226,11 +234,15 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer, index: number, templateData: IExpressionTemplateData): void { - templateData.toDispose.dispose(); + dispose(templateData.elementDisposable); + templateData.elementDisposable = []; } disposeTemplate(templateData: IExpressionTemplateData): void { - templateData.toDispose.dispose(); + dispose(templateData.elementDisposable); + templateData.actionBar?.dispose(); } } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 435bcf9adfe8d..c779d9521595a 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/debug.contribution'; import 'vs/css!./media/debugHover'; import * as nls from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, Icon } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -16,7 +16,7 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY, } from 'vs/workbench/contrib/debug/common/debug'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; @@ -32,7 +32,7 @@ import { launchSchemaId } from 'vs/workbench/services/configuration/common/confi import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; import { RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; -import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID, VIEW_MEMORY_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; @@ -118,14 +118,16 @@ registerDebugCommandPaletteItem(SELECT_AND_START_ID, SELECT_AND_START_LABEL, Con // Debug callstack context menu -const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { +const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation', icon?: Icon) => { MenuRegistry.appendMenuItem(menuId, { group, when, order, + icon, command: { id, title, + icon, precondition } }); @@ -142,6 +144,8 @@ registerDebugViewMenuItem(MenuId.DebugCallStackContext, TERMINATE_THREAD_ID, nls registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED), CONTEXT_STACK_FRAME_SUPPORTS_RESTART); registerDebugViewMenuItem(MenuId.DebugCallStackContext, COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), undefined, '3_modification'); +registerDebugViewMenuItem(MenuId.DebugVariablesContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 15, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, 'inline', icons.debugInspectMemory); + registerDebugViewMenuItem(MenuId.DebugVariablesContext, SET_VARIABLE_ID, nls.localize('setValue', "Set Value"), 10, ContextKeyExpr.or(CONTEXT_SET_VARIABLE_SUPPORTED, ContextKeyExpr.and(CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_SET_EXPRESSION_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 10, undefined, undefined, '5_cutcopypaste'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_EVALUATE_PATH_ID, nls.localize('copyAsExpression', "Copy as Expression"), 20, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, '5_cutcopypaste'); @@ -154,6 +158,7 @@ registerDebugViewMenuItem(MenuId.DebugWatchContext, ADD_WATCH_ID, ADD_WATCH_LABE registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, nls.localize('editWatchExpression', "Edit Expression"), 20, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, SET_EXPRESSION_COMMAND_ID, nls.localize('setValue', "Set Value"), 30, ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_SET_EXPRESSION_SUPPORTED), ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable'), CONTEXT_SET_VARIABLE_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 40, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification'); +registerDebugViewMenuItem(MenuId.DebugWatchContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 50, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands'); registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands'); diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index 50e3f86473393..ac91e22355ab4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -83,3 +83,5 @@ export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon. export const debugConsoleEvaluationInput = registerIcon('debug-console-evaluation-input', Codicon.arrowSmallRight, localize('debugConsoleEvaluationInput', 'Icon for the debug evaluation input marker.')); export const debugConsoleEvaluationPrompt = registerIcon('debug-console-evaluation-prompt', Codicon.chevronRight, localize('debugConsoleEvaluationPrompt', 'Icon for the debug evaluation prompt.')); + +export const debugInspectMemory = registerIcon('debug-inspect-memory', Codicon.fileBinary, localize('debugInspectMemory', 'Icon for the inspect memory action.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/src/vs/workbench/contrib/debug/browser/debugMemory.ts new file mode 100644 index 0000000000000..ea17e487be4b6 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugMemory.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { clamp } from 'vs/base/common/numbers'; +import { assertNever } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { FileChangeType, FileOpenOptions, FilePermission, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileChange, IFileSystemProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { DEBUG_MEMORY_SCHEME, IDebugService, IDebugSession, IMemoryInvalidationEvent, IMemoryRegion, MemoryRange, MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; + +const rangeRe = /range=([0-9]+):([0-9]+)/; + +export class DebugMemoryFileSystemProvider implements IFileSystemProvider { + private memoryFdCounter = 0; + private readonly fdMemory = new Map(); + private readonly changeEmitter = new Emitter(); + + /** @inheritdoc */ + public readonly onDidChangeCapabilities = Event.None; + + /** @inheritdoc */ + public readonly onDidChangeFile = this.changeEmitter.event; + + /** @inheritdoc */ + public readonly capabilities = 0 + | FileSystemProviderCapabilities.PathCaseSensitive + | FileSystemProviderCapabilities.FileOpenReadWriteClose; + + constructor(private readonly debugService: IDebugService) { + debugService.onDidEndSession(session => { + for (const [fd, memory] of this.fdMemory) { + if (memory.session === session) { + this.close(fd); + } + } + }); + } + + public watch(resource: URI, opts: IWatchOptions) { + if (opts.recursive) { + return toDisposable(() => { }); + } + + const { session, memoryReference } = this.parseUri(resource); + return session.onDidInvalidateMemory(e => { + if (e.body.memoryReference === memoryReference) { + this.changeEmitter.fire([{ resource, type: FileChangeType.UPDATED }]); + } + }); + } + + /** @inheritdoc */ + public stat(file: URI): Promise { + const { readOnly } = this.parseUri(file); + return Promise.resolve({ + type: FileType.File, + mtime: 0, + ctime: 0, + size: 0, + permissions: readOnly ? FilePermission.Readonly : undefined, + }); + } + + /** @inheritdoc */ + public mkdir(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public readdir(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public delete(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public rename(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public open(resource: URI, _opts: FileOpenOptions): Promise { + const { session, memoryReference, offset } = this.parseUri(resource); + const fd = this.memoryFdCounter++; + let region = session.getMemory(memoryReference); + if (offset) { + region = new MemoryRegionView(region, offset); + } + + this.fdMemory.set(fd, { session, region }); + return Promise.resolve(fd); + } + + /** @inheritdoc */ + public close(fd: number) { + this.fdMemory.get(fd)?.region.dispose(); + this.fdMemory.delete(fd); + return Promise.resolve(); + } + + /** @inheritdoc */ + public async writeFile(resource: URI, content: Uint8Array) { + const { offset } = this.parseUri(resource); + if (!offset) { + throw new FileSystemProviderError(`Range must be present to read a file`, FileSystemProviderErrorCode.FileNotFound); + } + + const fd = await this.open(resource, { create: false }); + + try { + await this.write(fd, offset.fromOffset, content, 0, content.length); + } finally { + this.close(fd); + } + } + + /** @inheritdoc */ + public async readFile(resource: URI) { + const { offset } = this.parseUri(resource); + if (!offset) { + throw new FileSystemProviderError(`Range must be present to read a file`, FileSystemProviderErrorCode.FileNotFound); + } + + const data = new Uint8Array(offset.toOffset - offset.fromOffset); + const fd = await this.open(resource, { create: false }); + + try { + await this.read(fd, offset.fromOffset, data, 0, data.length); + return data; + } finally { + this.close(fd); + } + } + + /** @inheritdoc */ + public async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const memory = this.fdMemory.get(fd); + if (!memory) { + throw new FileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable); + } + + const ranges = await memory.region.read(pos, length); + let readSoFar = 0; + for (const range of ranges) { + switch (range.type) { + case MemoryRangeType.Unreadable: + return readSoFar; + case MemoryRangeType.Error: + if (readSoFar > 0) { + return readSoFar; + } else { + throw new FileSystemProviderError(range.error, FileSystemProviderErrorCode.Unknown); + } + case MemoryRangeType.Valid: { + const start = Math.max(0, pos - range.offset); + const toWrite = range.data.slice(start, Math.min(range.data.byteLength, start + (length - readSoFar))); + data.set(toWrite.buffer, offset + readSoFar); + readSoFar += toWrite.byteLength; + break; + } + default: + assertNever(range); + } + } + + return readSoFar; + } + + /** @inheritdoc */ + public write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const memory = this.fdMemory.get(fd); + if (!memory) { + throw new FileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable); + } + + return memory.region.write(pos, VSBuffer.wrap(data).slice(offset, offset + length)); + } + + protected parseUri(uri: URI) { + if (uri.scheme !== DEBUG_MEMORY_SCHEME) { + throw new FileSystemProviderError(`Cannot open file with scheme ${uri.scheme}`, FileSystemProviderErrorCode.FileNotFound); + } + + const session = this.debugService.getModel().getSession(uri.authority); + if (!session) { + throw new FileSystemProviderError(`Debug session not found`, FileSystemProviderErrorCode.FileNotFound); + } + + let offset: { fromOffset: number; toOffset: number } | undefined; + const rangeMatch = rangeRe.exec(uri.query); + if (rangeMatch) { + offset = { fromOffset: Number(rangeMatch[1]), toOffset: Number(rangeMatch[2]) }; + } + + const [, memoryReference] = uri.path.split('/'); + + return { + session, + offset, + readOnly: !session.capabilities.supportsWriteMemoryRequest, + sessionId: uri.authority, + memoryReference: decodeURIComponent(memoryReference), + }; + } +} + +/** A wrapper for a MemoryRegion that references a subset of data in another region. */ +class MemoryRegionView extends Disposable implements IMemoryRegion { + private readonly invalidateEmitter = new Emitter(); + + public readonly onDidInvalidate = this.invalidateEmitter.event; + public readonly writable: boolean; + private readonly width = this.range.toOffset - this.range.fromOffset; + + constructor(private readonly parent: IMemoryRegion, public readonly range: { fromOffset: number; toOffset: number }) { + super(); + this.writable = parent.writable; + + this._register(parent.onDidInvalidate(e => { + const fromOffset = clamp(e.fromOffset - range.fromOffset, 0, this.width); + const toOffset = clamp(e.toOffset - range.fromOffset, 0, this.width); + if (toOffset > fromOffset) { + this.invalidateEmitter.fire({ fromOffset, toOffset }); + } + })); + } + + public read(fromOffset: number, toOffset: number): Promise { + if (fromOffset < 0) { + throw new RangeError(`Invalid fromOffset: ${fromOffset}`); + } + + return this.parent.read( + this.range.fromOffset + fromOffset, + this.range.fromOffset + Math.min(toOffset, this.width), + ); + } + + public write(offset: number, data: VSBuffer): Promise { + return this.parent.write(this.range.fromOffset + offset, data); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 1868f0cf87598..8cc5722084218 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -34,9 +34,10 @@ import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; +import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -138,6 +139,7 @@ export class DebugService implements IDebugService { this.viewModel = new ViewModel(contextKeyService); this.taskRunner = this.instantiationService.createInstance(DebugTaskRunner); + this.disposables.add(this.fileService.registerProvider(DEBUG_MEMORY_SCHEME, new DebugMemoryFileSystemProvider(this))); this.disposables.add(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); this.disposables.add(this.lifecycleService.onWillShutdown(this.dispose, this)); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 429f9bde87f43..2b6cedacd13cf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -3,41 +3,41 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; -import * as platform from 'vs/base/common/platform'; -import severity from 'vs/base/common/severity'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Position, IPosition } from 'vs/editor/common/core/position'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { distinct } from 'vs/base/common/arrays'; +import { Queue, RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; -import { Thread, ExpressionContainer, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; -import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler, Queue } from 'vs/base/common/async'; +import * as platform from 'vs/base/common/platform'; +import * as resources from 'vs/base/common/resources'; +import severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; -import { distinct } from 'vs/base/common/arrays'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { localize } from 'vs/nls'; -import { canceled } from 'vs/base/common/errors'; -import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugger, IDebugService, IDebugSession, IDebugSessionOptions, IExceptionBreakpoint, IExceptionInfo, IExpression, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IReplElementSource, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; export class DebugSession implements IDebugSession { @@ -65,6 +65,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidProgressStart = new Emitter(); private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); + private readonly _onDidInvalidMemory = new Emitter(); private readonly _onDidChangeREPLElements = new Emitter(); @@ -139,6 +140,10 @@ export class DebugSession implements IDebugSession { this._subId = subId; } + getMemory(memoryReference: string): IMemoryRegion { + return new MemoryRegion(memoryReference, this); + } + get subId(): string | undefined { return this._subId; } @@ -247,6 +252,10 @@ export class DebugSession implements IDebugSession { return this._onDidProgressEnd.event; } + get onDidInvalidateMemory(): Event { + return this._onDidInvalidMemory.event; + } + //---- DAP requests /** @@ -768,6 +777,22 @@ export class DebugSession implements IDebugSession { return response?.body?.instructions; } + readMemory(memoryReference: string, offset: number, count: number): Promise { + if (!this.raw) { + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'readMemory'))); + } + + return this.raw.readMemory({ count, memoryReference, offset }); + } + + writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise { + if (!this.raw) { + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'disassemble'))); + } + + return this.raw.writeMemory({ memoryReference, offset, allowPartial, data }); + } + //---- threads getThread(threadId: number): Thread | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 7cdd149a3d716..e32056677e8c2 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -216,9 +216,21 @@ font-size: 11px; } +.debug-pane .monaco-list-row .expression { + display: flex; +} + +.debug-pane .monaco-list-row .expression .actionbar-spacer { + flex-grow: 1; +} + +.debug-pane .monaco-list-row .expression .value { + overflow: hidden; + white-space: pre; + text-overflow: ellipsis; +} + .debug-pane .monaco-list-row .expression .value.changed { - padding: 2px; - margin: 4px; border-radius: 4px; } @@ -230,6 +242,7 @@ .debug-pane .inputBoxContainer { box-sizing: border-box; flex-grow: 1; + display: none; } .debug-pane .debug-watch .monaco-inputbox { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 0aa9f6bbe9cdf..2d8bf4fdddfe6 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -521,6 +521,22 @@ export class RawDebugSession implements IDisposable { return Promise.reject(new Error('disassemble is not supported')); } + async readMemory(args: DebugProtocol.ReadMemoryArguments): Promise { + if (this.capabilities.supportsReadMemoryRequest) { + return await this.send('readMemory', args); + } + + return Promise.reject(new Error('readMemory is not supported')); + } + + async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { + if (this.capabilities.supportsWriteMemoryRequest) { + return await this.send('writeMemory', args); + } + + return Promise.reject(new Error('writeMemory is not supported')); + } + cancel(args: DebugProtocol.CancelArguments): Promise { return this.send('cancel', args); } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 447971aefc307..42f6f53c37b81 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -3,40 +3,43 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope, ErrorScope, StackFrame, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; +import { coalesce } from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { withUndefinedAsNull } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; -import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { dispose } from 'vs/base/common/lifecycle'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { withUndefinedAsNull } from 'vs/base/common/types'; -import { IMenuService, IMenu, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; -import { coalesce } from 'vs/base/common/arrays'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, CONTEXT_VARIABLES_FOCUSED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_VARIABLE_IS_READONLY, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { ErrorScope, Expression, getUriForDebugMemory, Scope, StackFrame, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const $ = dom.$; let forgetScopes = true; @@ -45,6 +48,7 @@ let variableInternalContext: Variable | undefined; let dataBreakpointInfoResponse: IDataBreakpointInfoResponse | undefined; interface IVariablesContext { + sessionId: string | undefined; container: DebugProtocol.Variable | DebugProtocol.Scope; variable: DebugProtocol.Variable; } @@ -56,13 +60,6 @@ export class VariablesView extends ViewPane { private tree!: WorkbenchAsyncDataTree; private savedViewState = new Map(); private autoExpandedScopes = new Set(); - private menu: IMenu; - private debugProtocolVariableMenuContext: IContextKey; - private breakWhenValueChangesSupported: IContextKey; - private breakWhenValueIsAccessedSupported: IContextKey; - private breakWhenValueIsReadSupported: IContextKey; - private variableEvaluateName: IContextKey; - private variableReadonly: IContextKey; constructor( options: IViewletViewOptions, @@ -76,19 +73,10 @@ export class VariablesView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, - @IMenuService menuService: IMenuService + @IMenuService private readonly menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.menu = menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); - this._register(this.menu); - this.debugProtocolVariableMenuContext = CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.bindTo(contextKeyService); - this.breakWhenValueChangesSupported = CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.bindTo(contextKeyService); - this.breakWhenValueIsAccessedSupported = CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.bindTo(contextKeyService); - this.breakWhenValueIsReadSupported = CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.bindTo(contextKeyService); - this.variableEvaluateName = CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.bindTo(contextKeyService); - this.variableReadonly = CONTEXT_VARIABLE_IS_READONLY.bindTo(contextKeyService); - // Use scheduler to prevent unnecessary flashing this.updateTreeScheduler = new RunOnceScheduler(async () => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; @@ -208,55 +196,89 @@ export class VariablesView extends ViewPane { private async onContextMenu(e: ITreeContextMenuEvent): Promise { const variable = e.element; - if (variable instanceof Variable && !!variable.value) { - this.debugProtocolVariableMenuContext.set(variable.variableMenuContext || ''); - variableInternalContext = variable; - const session = this.debugService.getViewModel().focusedSession; - this.variableEvaluateName.set(!!variable.evaluateName); - const attributes = variable.presentationHint?.attributes; - this.variableReadonly.set(!!attributes && attributes.indexOf('readOnly') >= 0); - this.breakWhenValueChangesSupported.reset(); - this.breakWhenValueIsAccessedSupported.reset(); - this.breakWhenValueIsReadSupported.reset(); - if (session && session.capabilities.supportsDataBreakpoints) { - dataBreakpointInfoResponse = await session.dataBreakpointInfo(variable.name, variable.parent.reference); - const dataBreakpointId = dataBreakpointInfoResponse?.dataId; - const dataBreakpointAccessTypes = dataBreakpointInfoResponse?.accessTypes; - if (!dataBreakpointAccessTypes) { - // Assumes default behaviour: Supports breakWhenValueChanges - this.breakWhenValueChangesSupported.set(!!dataBreakpointId); - } else { - dataBreakpointAccessTypes.forEach(accessType => { - switch (accessType) { - case 'read': - this.breakWhenValueIsReadSupported.set(!!dataBreakpointId); - break; - case 'write': - this.breakWhenValueChangesSupported.set(!!dataBreakpointId); - break; - case 'readWrite': - this.breakWhenValueIsAccessedSupported.set(!!dataBreakpointId); - break; - } - }); - } - } + if (!(variable instanceof Variable) || !variable.value) { + return; + } + + const toDispose = new DisposableStore(); - const context: IVariablesContext = { - container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), - variable: variable.toDebugProtocolObject() - }; - const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions); + try { + const contextKeyService = toDispose.add(await getContextForVariableMenuWithDataAccess(this.contextKeyService, variable)); + const menu = toDispose.add(this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService)); + + const context: IVariablesContext = getVariablesContext(variable); + const secondary: IAction[] = []; + const actionsDisposable = createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => secondary, onHide: () => dispose(actionsDisposable) }); + } finally { + toDispose.dispose(); } } } +const getVariablesContext = (variable: Variable): IVariablesContext => ({ + sessionId: variable.getSession()?.getId(), + container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), + variable: variable.toDebugProtocolObject() +}); + +/** + * Gets a context key overlay that has context for the given variable, including data access info. + */ +async function getContextForVariableMenuWithDataAccess(parentContext: IContextKeyService, variable: Variable) { + const session = variable.getSession(); + if (!session || !session.capabilities.supportsDataBreakpoints) { + return getContextForVariableMenu(parentContext, variable); + } + + const contextKeys: [string, unknown][] = []; + dataBreakpointInfoResponse = await session.dataBreakpointInfo(variable.name, variable.parent.reference); + const dataBreakpointId = dataBreakpointInfoResponse?.dataId; + const dataBreakpointAccessTypes = dataBreakpointInfoResponse?.accessTypes; + + if (!dataBreakpointAccessTypes) { + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]); + } else { + for (const accessType of dataBreakpointAccessTypes) { + switch (accessType) { + case 'read': + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.key, !!dataBreakpointId]); + break; + case 'write': + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]); + break; + case 'readWrite': + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.key, !!dataBreakpointId]); + break; + } + } + } + + return getContextForVariableMenu(parentContext, variable, contextKeys); +} + +/** + * Gets a context key overlay that has context for the given variable. + */ +function getContextForVariableMenu(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) { + const session = variable.getSession(); + const contextKeys: [string, unknown][] = [ + [CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.key, variable.variableMenuContext || ''], + [CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.key, !!variable.evaluateName], + [CONTEXT_CAN_VIEW_MEMORY.key, !!session?.capabilities.supportsReadMemoryRequest && variable.memoryReference !== undefined], + [CONTEXT_VARIABLE_IS_READONLY.key, !!variable.presentationHint?.attributes?.includes('readOnly')], + ...additionalContext, + ]; + + variableInternalContext = variable; + + return parentContext.createOverlay(contextKeys); +} + function isStackFrame(obj: any): obj is IStackFrame { return obj instanceof StackFrame; } @@ -364,6 +386,8 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { constructor( private readonly linkDetector: LinkDetector, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IThemeService themeService: IThemeService, @@ -402,6 +426,20 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { } }; } + + protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) { + const variable = expression as Variable; + const contextKeyService = getContextForVariableMenu(this.contextKeyService, variable); + const menu = this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); + + const primary: IAction[] = []; + const context = getVariablesContext(variable); + data.elementDisposable.push(createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline')); + + actionBar.clear(); + actionBar.context = context; + actionBar.push(primary, { icon: true, label: false }); + } } class VariablesAccessibilityProvider implements IListAccessibilityProvider { @@ -434,7 +472,7 @@ CommandsRegistry.registerCommand({ export const COPY_VALUE_ID = 'workbench.debug.viewlet.action.copyValue'; CommandsRegistry.registerCommand({ id: COPY_VALUE_ID, - handler: async (accessor: ServicesAccessor, arg: Variable | Expression | unknown, ctx?: (Variable | Expression)[]) => { + handler: async (accessor: ServicesAccessor, arg: Variable | Expression | IVariablesContext, ctx?: (Variable | Expression)[]) => { const debugService = accessor.get(IDebugService); const clipboardService = accessor.get(IClipboardService); let elementContext = ''; @@ -469,6 +507,35 @@ CommandsRegistry.registerCommand({ } }); +export const VIEW_MEMORY_ID = 'workbench.debug.viewlet.action.viewMemory'; + +const HEX_EDITOR_EXTENSION_ID = 'ms-vscode.hexeditor'; +const HEX_EDITOR_EDITOR_ID = 'hexEditor.hexedit'; + +CommandsRegistry.registerCommand({ + id: VIEW_MEMORY_ID, + handler: async (accessor: ServicesAccessor, arg: IVariablesContext, ctx?: (Variable | Expression)[]) => { + if (!arg.sessionId || !arg.variable.memoryReference) { + return; + } + + const commandService = accessor.get(ICommandService); + const editorService = accessor.get(IEditorService); + const ext = await accessor.get(IExtensionService).getExtension(HEX_EDITOR_EXTENSION_ID); + if (!ext) { + await commandService.executeCommand('workbench.extensions.search', `@id:${HEX_EDITOR_EXTENSION_ID}`); + } else { + await editorService.openEditor({ + resource: getUriForDebugMemory(arg.sessionId, arg.variable.memoryReference), + options: { + revealIfOpened: true, + override: HEX_EDITOR_EDITOR_ID, + }, + }); + } + } +}); + export const BREAK_WHEN_VALUE_CHANGES_ID = 'debug.breakWhenValueChanges'; CommandsRegistry.registerCommand({ id: BREAK_WHEN_VALUE_CHANGES_ID, diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 944941375f17d..4b691f887edf0 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -3,29 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { URI as uri } from 'vs/base/common/uri'; -import severity from 'vs/base/common/severity'; +import { IAction } from 'vs/base/common/actions'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import severity from 'vs/base/common/severity'; +import { URI as uri } from 'vs/base/common/uri'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel as EditorIModel } from 'vs/editor/common/model'; -import { IEditorPane } from 'vs/workbench/common/editor'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { Range, IRange } from 'vs/editor/common/core/range'; +import * as nls from 'vs/nls'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IAction } from 'vs/base/common/actions'; -import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -58,6 +59,7 @@ export const CONTEXT_CALLSTACK_SESSION_IS_ATTACH = new RawContextKey('c export const CONTEXT_CALLSTACK_ITEM_STOPPED = new RawContextKey('callStackItemStopped', false, { type: 'boolean', description: nls.localize('callStackItemStopped', "True when the focused item in the CALL STACK is stopped. Used internaly for inline menus in the CALL STACK view.") }); export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('callStackSessionHasOneThread', false, { type: 'boolean', description: nls.localize('callStackSessionHasOneThread', "True when the focused session in the CALL STACK view has exactly one thread. Used internally for inline menus in the CALL STACK view.") }); export const CONTEXT_WATCH_ITEM_TYPE = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); +export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); export const CONTEXT_BREAKPOINT_ACCESS_TYPE = new RawContextKey('breakpointAccessType', undefined, { type: 'string', description: nls.localize('breakpointAccessType', "Represents the access type of the focused data breakpoint in the BREAKPOINTS view. For example: 'read', 'readWrite', 'write'") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); @@ -204,6 +206,77 @@ export interface IDataBreakpointInfoResponse { accessTypes?: DebugProtocol.DataBreakpointAccessType[]; } +export interface IMemoryInvalidationEvent { + fromOffset: number; + toOffset: number; +} + +export const enum MemoryRangeType { + Valid, + Unreadable, + Error, +} + +export interface IMemoryRange { + type: MemoryRangeType; + offset: number; + length: number; +} + +export interface IValidMemoryRange extends IMemoryRange { + type: MemoryRangeType.Valid; + offset: number; + length: number; + data: VSBuffer +} + +export interface IUnreadableMemoryRange extends IMemoryRange { + type: MemoryRangeType.Unreadable; +} + +export interface IErrorMemoryRange extends IMemoryRange { + type: MemoryRangeType.Error; + error: string; +} + +/** + * Union type of memory that can be returned from read(). Since a read request + * could encompass multiple previously-read ranges, multiple of these types + * are possible to return. + */ +export type MemoryRange = IValidMemoryRange | IUnreadableMemoryRange | IErrorMemoryRange; + +export const DEBUG_MEMORY_SCHEME = 'vscode-debug-memory'; + +/** + * An IMemoryRegion corresponds to a contiguous range of memory referred to + * by a DAP `memoryReference`. + */ +export interface IMemoryRegion extends IDisposable { + /** + * Event that fires when memory changes. Can be a result of memory events or + * `write` requests. + */ + readonly onDidInvalidate: Event; + + /** + * Whether writes are supported on this memory region. + */ + readonly writable: boolean; + + /** + * Requests memory ranges from the debug adapter. It returns a list of memory + * ranges that overlap (but may exceed!) the given offset. Use the `offset` + * and `length` of each range for display. + */ + read(fromOffset: number, toOffset: number): Promise; + + /** + * Writes memory to the debug adapter at the given offset. + */ + write(offset: number, data: VSBuffer): Promise; +} + export interface IDebugSession extends ITreeElement { readonly configuration: IConfig; @@ -219,6 +292,8 @@ export interface IDebugSession extends ITreeElement { setSubId(subId: string | undefined): void; + getMemory(memoryReference: string): IMemoryRegion; + setName(name: string): void; readonly onDidChangeName: Event; getLabel(): string; @@ -256,6 +331,7 @@ export interface IDebugSession extends ITreeElement { readonly onDidProgressStart: Event; readonly onDidProgressUpdate: Event; readonly onDidProgressEnd: Event; + readonly onDidInvalidateMemory: Event; // DAP request @@ -282,6 +358,8 @@ export interface IDebugSession extends ITreeElement { customRequest(request: string, args: any): Promise; cancel(progressId: string): Promise; disassemble(memoryReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise; + readMemory(memoryReference: string, offset: number, count: number): Promise; + writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise; restartFrame(frameId: number, threadId: number): Promise; next(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index d9da5ab01bec0..3fd47ea1867e7 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -3,28 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { URI as uri } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; -import { Event, Emitter } from 'vs/base/common/event'; -import { generateUuid } from 'vs/base/common/uuid'; +import { distinct, lastIndex } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { mixin } from 'vs/base/common/objects'; +import * as resources from 'vs/base/common/resources'; import { isString, isUndefinedOrNull } from 'vs/base/common/types'; -import { distinct, lastIndex } from 'vs/base/common/arrays'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { - ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, - IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint, IInstructionBreakpoint -} from 'vs/workbench/contrib/debug/common/debug'; -import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { URI, URI as uri } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import * as nls from 'vs/nls'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { mixin } from 'vs/base/common/objects'; +import { DEBUG_MEMORY_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointsChangeEvent, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugSession, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { getUriFromSource, Source, UNKNOWN_SOURCE_LABEL } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { __vscodeVariableMenuContext?: string; @@ -48,6 +47,7 @@ export class ExpressionContainer implements IExpressionContainer { private id: string, public namedVariables: number | undefined = 0, public indexedVariables: number | undefined = 0, + public memoryReference: string | undefined = undefined, private startOfVariables: number | undefined = 0 ) { } @@ -92,7 +92,7 @@ export class ExpressionContainer implements IExpressionContainer { for (let i = 0; i < numberOfChunks; i++) { const start = (this.startOfVariables || 0) + i * chunkSize; const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); - children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, undefined, true, start)); + children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, undefined, { kind: 'virtual' }, undefined, undefined, true, start)); } return children; @@ -132,12 +132,12 @@ export class ExpressionContainer implements IExpressionContainer { const count = nameCount.get(v.name) || 0; const idDuplicationIndex = count > 0 ? count.toString() : ''; nameCount.set(v.name, count + 1); - return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type, v.__vscodeVariableMenuContext, true, 0, idDuplicationIndex); + return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.memoryReference, v.presentationHint, v.type, v.__vscodeVariableMenuContext, true, 0, idDuplicationIndex); } - return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, undefined, false); + return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false); }); } catch (e) { - return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, undefined, false)]; + return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false)]; } } @@ -178,6 +178,7 @@ export class ExpressionContainer implements IExpressionContainer { this.reference = response.body.variablesReference; this.namedVariables = response.body.namedVariables; this.indexedVariables = response.body.indexedVariables; + this.memoryReference = response.body.memoryReference; this.type = response.body.type || this.type; return true; } @@ -197,6 +198,7 @@ function handleSetResponse(expression: ExpressionContainer, response: DebugProto expression.reference = response.body.variablesReference; expression.namedVariables = response.body.namedVariables; expression.indexedVariables = response.body.indexedVariables; + // todo @weinand: the set responses contain most properties, but not memory references. Should they? } } @@ -248,6 +250,7 @@ export class Variable extends ExpressionContainer implements IExpression { value: string | undefined, namedVariables: number | undefined, indexedVariables: number | undefined, + memoryReference: string | undefined, public presentationHint: DebugProtocol.VariablePresentationHint | undefined, type: string | undefined = undefined, public variableMenuContext: string | undefined = undefined, @@ -255,7 +258,7 @@ export class Variable extends ExpressionContainer implements IExpression { startOfVariables = 0, idDuplicationIndex = '', ) { - super(session, threadId, reference, `variable:${parent.getId()}:${name}:${idDuplicationIndex}`, namedVariables, indexedVariables, startOfVariables); + super(session, threadId, reference, `variable:${parent.getId()}:${name}:${idDuplicationIndex}`, namedVariables, indexedVariables, memoryReference, startOfVariables); this.value = value || ''; this.type = type; } @@ -295,6 +298,7 @@ export class Variable extends ExpressionContainer implements IExpression { return { name: this.name, variablesReference: this.reference || 0, + memoryReference: this.memoryReference, value: this.value, evaluateName: this.evaluateName }; @@ -588,6 +592,93 @@ export class Thread implements IThread { } } +/** + * Gets a URI to a memory in the given session ID. + */ +export const getUriForDebugMemory = ( + sessionId: string, + memoryReference: string, + range?: { fromOffset: number, toOffset: number }, + displayName = 'memory' +) => { + return URI.from({ + scheme: DEBUG_MEMORY_SCHEME, + authority: sessionId, + path: '/' + encodeURIComponent(memoryReference) + `/${encodeURIComponent(displayName)}.bin`, + query: range ? `?range=${range.fromOffset}:${range.toOffset}` : undefined, + }); +}; + +export class MemoryRegion extends Disposable implements IMemoryRegion { + private readonly invalidateEmitter = this._register(new Emitter()); + + /** @inheritdoc */ + public readonly onDidInvalidate = this.invalidateEmitter.event; + + /** @inheritdoc */ + public readonly writable = !!this.session.capabilities.supportsWriteMemoryRequest; + + constructor(private readonly memoryReference: string, private readonly session: IDebugSession) { + super(); + this._register(session.onDidInvalidateMemory(e => { + if (e.body.memoryReference === memoryReference) { + this.invalidate(e.body.offset, e.body.count - e.body.offset); + } + })); + } + + public async read(fromOffset: number, toOffset: number): Promise { + const length = toOffset - fromOffset; + const offset = fromOffset; + const result = await this.session.readMemory(this.memoryReference, offset, length); + + if (result === undefined || !result.body?.data) { + return [{ type: MemoryRangeType.Unreadable, offset, length }]; + } + + let data: VSBuffer; + try { + data = decodeBase64(result.body.data); + } catch { + return [{ type: MemoryRangeType.Error, offset, length, error: 'Invalid base64 data from debug adapter' }]; + } + + const unreadable = result.body.unreadableBytes || 0; + const dataLength = length - unreadable; + if (data.byteLength < dataLength) { + const pad = VSBuffer.alloc(dataLength - data.byteLength); + pad.buffer.fill(0); + data = VSBuffer.concat([data, pad], dataLength); + } else if (data.byteLength > dataLength) { + data = data.slice(0, dataLength); + } + + if (!unreadable) { + return [{ type: MemoryRangeType.Valid, offset, length, data }]; + } + + return [ + { type: MemoryRangeType.Valid, offset, length: dataLength, data }, + { type: MemoryRangeType.Unreadable, offset: offset + dataLength, length: unreadable }, + ]; + } + + public async write(offset: number, data: VSBuffer): Promise { + const result = await this.session.writeMemory(this.memoryReference, offset, encodeBase64(data), true); + const written = result?.body?.bytesWritten ?? data.byteLength; + this.invalidate(offset, offset + written); + return written; + } + + public override dispose() { + super.dispose(); + } + + private invalidate(fromOffset: number, toOffset: number) { + this.invalidateEmitter.fire({ fromOffset, toOffset }); + } +} + export class Enablement implements IEnablement { constructor( public enabled: boolean, @@ -989,6 +1080,10 @@ export class ThreadAndSessionIds implements ITreeElement { } } +export class Memory { + +} + export class DebugModel implements IDebugModel { private sessions: IDebugSession[]; diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index ee0e186583749..68bd9a62171aa 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_SET_EXPRESSION_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index c498cffe2caf0..7860d8a80a5a3 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -90,7 +90,7 @@ suite('Debug - Base Debug View', () => { const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: undefined!, endColumn: undefined! }, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined!, 0, 0, {}, 'string'); + let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined!, 0, 0, undefined, {}, 'string'); let expression = $('.'); let name = $('.'); let value = $('.'); @@ -118,7 +118,7 @@ suite('Debug - Base Debug View', () => { assert.ok(value.querySelector('a')); assert.strictEqual(value.querySelector('a')!.textContent, variable.value); - variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, { kind: 'virtual' }); + variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); expression = $('.'); name = $('.'); value = $('.'); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts index 89aa91e164c28..4a0ed4554beee 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts @@ -45,8 +45,8 @@ suite('Debug - Hover', () => { override getChildren(): Promise { return Promise.resolve([variableB]); } - }(session, 1, scope, 2, 'A', 'A', undefined!, 0, 0, {}, 'string'); - const variableB = new Variable(session, 1, scope, 2, 'B', 'A.B', undefined!, 0, 0, {}, 'string'); + }(session, 1, scope, 2, 'A', 'A', undefined!, 0, 0, undefined, {}, 'string'); + const variableB = new Variable(session, 1, scope, 2, 'B', 'A.B', undefined!, 0, 0, undefined, {}, 'string'); assert.strictEqual(await findExpressionInStackFrame(stackFrame, []), undefined); assert.strictEqual(await findExpressionInStackFrame(stackFrame, ['A']), variableA); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts new file mode 100644 index 0000000000000..7ee7788c6c673 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; +import { mockObject, MockObject } from 'vs/base/test/common/mock'; +import { MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; +import { MemoryRegion } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockSession } from 'vs/workbench/contrib/debug/test/browser/mockDebug'; + +suite('Debug - Memory', () => { + const dapResponseCommon = { + command: 'someCommand', + type: 'response', + seq: 1, + request_seq: 1, + success: true, + }; + + suite('MemoryRegion', () => { + let memory: VSBuffer; + let unreadable: number; + let invalidateMemoryEmitter: Emitter; + let session: MockObject; + let region: MemoryRegion; + + setup(() => { + const memoryBuf = new Uint8Array(1024); + for (let i = 0; i < memoryBuf.length; i++) { + memoryBuf[i] = i; // will be 0-255 + } + memory = VSBuffer.wrap(memoryBuf); + invalidateMemoryEmitter = new Emitter(); + unreadable = 0; + + session = mockObject()({ + onDidInvalidateMemory: invalidateMemoryEmitter.event + }); + + session.readMemory.callsFake((ref: string, fromOffset: number, count: number) => { + const res: DebugProtocol.ReadMemoryResponse = ({ + ...dapResponseCommon, + body: { + address: '0', + data: encodeBase64(memory.slice(fromOffset, fromOffset + Math.max(0, count - unreadable))), + unreadableBytes: unreadable + } + }); + + unreadable = 0; + + return Promise.resolve(res); + }); + + session.writeMemory.callsFake((ref: string, fromOffset: number, data: string): DebugProtocol.WriteMemoryResponse => { + const decoded = decodeBase64(data); + for (let i = 0; i < decoded.byteLength; i++) { + memory.buffer[fromOffset + i] = decoded.buffer[i]; + } + + return ({ + ...dapResponseCommon, + body: { + bytesWritten: decoded.byteLength, + offset: fromOffset, + } + }); + }); + + region = new MemoryRegion('ref', session as any); + }); + + teardown(() => { + region.dispose(); + }); + + test('reads a simple range', async () => { + assert.deepStrictEqual(await region.read(10, 14), [ + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) } + ]); + }); + + test('reads a non-contiguous range', async () => { + unreadable = 3; + assert.deepStrictEqual(await region.read(10, 14), [ + { type: MemoryRangeType.Valid, offset: 10, length: 1, data: VSBuffer.wrap(new Uint8Array([10])) }, + { type: MemoryRangeType.Unreadable, offset: 11, length: 3 }, + ]); + }); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 60b8da2c38d48..af18ed46c5c30 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as uri } from 'vs/base/common/uri'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions, IEvaluate, IAdapterManager, IRawStoppedDetails, IInstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import Severity from 'vs/base/common/severity'; +import { URI as uri } from 'vs/base/common/uri'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; -import { ExceptionBreakpoint, Expression, DataBreakpoint, FunctionBreakpoint, Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugger, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEvaluate, IExceptionBreakpoint, IExceptionInfo, IExpression, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IReplElementSource, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { Breakpoint, DataBreakpoint, DebugModel, ExceptionBreakpoint, Expression, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { ITextModel } from 'vs/editor/common/model'; const fileService = new TestFileService(); export const mockUriIdentityService = new UriIdentityService(fileService); @@ -170,6 +170,22 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + getMemory(memoryReference: string): IMemoryRegion { + throw new Error('Method not implemented.'); + } + + get onDidInvalidateMemory(): Event { + throw new Error('Not implemented'); + } + + readMemory(memoryReference: string, offset: number, count: number): Promise { + throw new Error('Method not implemented.'); + } + + writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise { + throw new Error('Method not implemented.'); + } + get compoundRoot(): DebugCompoundRoot | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 1400828370ce3..609d6eb83989e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -6,7 +6,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IAction } from 'vs/base/common/actions'; import { coalesce } from 'vs/base/common/arrays'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { decodeBase64 } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { getExtensionForMimeType } from 'vs/base/common/mime'; @@ -830,13 +830,7 @@ var requirejs = (function() { return; } - const decoded = atob(splitData); - const typedArray = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - typedArray[i] = decoded.charCodeAt(i); - } - - const buff = VSBuffer.wrap(typedArray); + const buff = decodeBase64(splitData); await this.fileService.writeFile(newFileUri, buff); await this.openerService.open(newFileUri); } diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 7a5398a7eefb6..a5d8dc2bbf9e7 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -462,11 +462,11 @@ suite('ExtHost Testing', () => { let dto: TestRunDto; setup(async () => { - proxy = mockObject(); + proxy = mockObject()(); cts = new CancellationTokenSource(); c = new TestRunCoordinator(proxy); - configuration = new TestRunProfileImpl(mockObject(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); + configuration = new TestRunProfileImpl(mockObject()(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); await single.expand(single.root.id, Infinity); single.collectDiff();