Skip to content

Commit

Permalink
feat: support clipboard context for value copies
Browse files Browse the repository at this point in the history
Fixes #423
  • Loading branch information
connor4312 committed Apr 8, 2020
1 parent ea9d6a4 commit a136ba5
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 93 deletions.
39 changes: 2 additions & 37 deletions demos/node/main.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,3 @@
console.log('hi');console.log('hi2');
const env = process.env;

const mm = require('./micromatch');

mm(['foo', 'bar', 'baz', 'qux'], ['f*', 'b*']);

async function bar() {
return 42;
}

async function foo() {
const result1 = await bar();
const result2 = await bar();
console.log(result1 + result2);
}

function throwIt() {
setTimeout(() => {
throw new Error('Oh my!');
}, 0);
}

let counter = 0;
setInterval(() => {
setTimeout(() => {
//console.log("a\nb\nc\nd" + (++counter));
}, 0);
}, 2000);

console.log('Hello world!');

var path = './.vscode/launch.json:4:2';
console.log(path);
var obj = {foo: path};
console.log(obj);
var arr = [path, obj];
console.log(arr);
foo();
console.log(env);
1 change: 1 addition & 0 deletions src/adapter/debugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export class DebugAdapter implements IDisposable {
supportsTerminateRequest: false,
completionTriggerCharacters: ['.', '[', '"', "'"],
supportsBreakpointLocationsRequest: true,
supportsClipboardContext: true,
//supportsDataBreakpoints: false,
//supportsReadMemoryRequest: false,
//supportsDisassembleRequest: false,
Expand Down
2 changes: 2 additions & 0 deletions src/adapter/objectPreview/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const enum PreviewContextType {
Hover = 'hover',
PropertyValue = 'propertyValue',
Copy = 'copy',
Clipboard = 'clipboard',
}

const repl: IPreviewContext = { budget: 1000, quoted: true };
Expand All @@ -47,6 +48,7 @@ export const getContextForType = (type: PreviewContextType | string | undefined)
case PreviewContextType.PropertyValue:
return hover;
case PreviewContextType.Copy:
case PreviewContextType.Clipboard:
return copy;
default:
// the type is received straight from the DAP, so it's possible we might
Expand Down
11 changes: 8 additions & 3 deletions src/adapter/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ export function templateFunction<Args extends unknown[]>(fn: string): (...args:
export function templateFunction<Args extends unknown[]>(
fn: string | ((...args: Args) => void),
): (...args: string[]) => string {
const stringified = '' + fn;
return templateFunctionStr('' + fn);
}

export function templateFunctionStr<Args extends string[]>(
stringified: string,
): (...args: Args) => string {
const sourceFile = ts.createSourceFile('test.js', stringified, ts.ScriptTarget.ESNext, true);

// 1. Find the function.
Expand All @@ -57,7 +62,7 @@ export function templateFunction<Args extends unknown[]>(
});

if (!decl || !('body' in decl) || !decl.body) {
throw new Error(`Could not find function declaration for ${fn}`);
throw new Error(`Could not find function declaration for:\n\n${stringified}`);
}

// 2. Get parameter names.
Expand Down Expand Up @@ -123,7 +128,7 @@ type RemoteObjectWithType<R, ByValue> = ByValue extends true
* that takes the CDP and arguments with which to invoke the function. The
* arguments should be simple objects.
*/
export function remoteFunction<Args extends unknown[], R>(fn: (...args: Args) => R) {
export function remoteFunction<Args extends unknown[], R>(fn: string | ((...args: Args) => R)) {
const stringified = '' + fn;

// Some ugly typing here, but it gets us type safety. Mainly we want to:
Expand Down
98 changes: 98 additions & 0 deletions src/adapter/templates/serializeForClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { templateFunctionStr, remoteFunction } from '.';

export type CycleReplacer = (
key: string,
value: unknown,
stack: unknown[],
keys: string[],
) => string;

/**
* Safe-stringifier. Modified from json-stringify-safe.
*
* @license
*
* The ISC License
*
* Copyright (c) Isaac Z. Schlueter and Contributors
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
export function safeSerializer(
replacer?: (key: string, value: unknown) => unknown,
cycleReplacer: CycleReplacer = (_key, value, stack, keys) =>
stack[0] === value
? '[Circular ~]'
: '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']',
) {
const stack: unknown[] = [];
const keys: string[] = [];

return function (this: unknown, key: string, value: unknown) {
if (stack.length > 0) {
const thisPos = stack.indexOf(this);
if (thisPos === -1) {
stack.push(this);
keys.push(key);
} else {
stack.splice(thisPos + 1);
keys.splice(thisPos, Infinity, key);
}

if (stack.indexOf(value) !== -1) {
value = cycleReplacer.call(this, key, value, stack, keys);
}
} else {
stack.push(value);
}

// Modification: avoid JSON.stringify throwing on bigints.
if (typeof value === 'bigint') {
value = value <= Number.MAX_SAFE_INTEGER ? Number(value) : String(value);
}

return replacer == null ? value : replacer.call(this, key, value);
};
}

/**
* Safe-stringifies the value as JSON, replacing
*/
export const serializeForClipboardTmpl = templateFunctionStr<[string, string]>(`
function (valueToStringify, spaces) {
try {
if (typeof valueToStringify === 'function') {
return '' + valueToStringify;
}
if (typeof Node !== 'undefined' && valueToStringify instanceof Node) {
return valueToStringify.outerHTML;
}
return JSON.stringify(valueToStringify, (${safeSerializer})(), spaces);
} catch (e) {
return '' + valueToStringify;
}
}
`);

export const serializeForClipboard = remoteFunction<[number], string>(`
function(spaces) {
const result = ${serializeForClipboardTmpl('this', 'spaces')};
return result;
}
`);
23 changes: 0 additions & 23 deletions src/adapter/templates/toStringForClipboard.ts

This file was deleted.

35 changes: 24 additions & 11 deletions src/adapter/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import { SmartStepper } from './smartStepping';
import { IPreferredUiLocation, Source, SourceContainer, IUiLocation, base1To0 } from './sources';
import { StackFrame, StackTrace } from './stackTrace';
import { VariableStore, IVariableStoreDelegate } from './variables';
import { toStringForClipboard } from './templates/toStringForClipboard';
import { previewThis } from './templates/previewThis';
import { UserDefinedBreakpoint } from './breakpoints/userDefinedBreakpoint';
import { ILogger } from '../common/logging';
import { AnyObject } from './objectPreview/betterTypes';
import { IEvaluator } from './evaluator';
import {
serializeForClipboardTmpl,
serializeForClipboard,
} from './templates/serializeForClipboard';

const localize = nls.loadMessageBundle();

Expand Down Expand Up @@ -345,14 +348,24 @@ export class Thread implements IVariableStoreDelegate {
}
}
}
// TODO: consider checking expression for side effects on hover.
const params: Cdp.Runtime.EvaluateParams = {
expression: args.expression,
includeCommandLineAPI: true,
objectGroup: 'console',
generatePreview: true,
timeout: args.context === 'hover' ? 500 : undefined,
};

// For clipboard evaluations, return a safe JSON-stringified string.
const params: Cdp.Runtime.EvaluateParams =
args.context === 'clipboard'
? {
expression: serializeForClipboardTmpl(args.expression, '2'),
includeCommandLineAPI: true,
returnByValue: true,
objectGroup: 'console',
}
: {
expression: args.expression,
includeCommandLineAPI: true,
objectGroup: 'console',
generatePreview: true,
timeout: args.context === 'hover' ? 500 : undefined,
};

if (args.context === 'repl') {
params.expression = sourceUtils.wrapObjectLiteral(params.expression);
if (params.expression.indexOf('await') !== -1) {
Expand Down Expand Up @@ -1233,10 +1246,10 @@ export class Thread implements IVariableStoreDelegate {
}

try {
const result = await toStringForClipboard({
const result = await serializeForClipboard({
cdp: this.cdp(),
objectId: object.objectId,
args: [object.subtype],
args: [2],
silent: true,
returnByValue: true,
});
Expand Down
35 changes: 35 additions & 0 deletions src/test/evaluate/evaluate-copy-via-evaluate-context.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
result : 123
type : string
variablesReference : <number>
}
{
result : null
type : string
variablesReference : <number>
}
{
result : { "foo": "bar", "baz": { "a": [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], "b": 123 } }
type : string
variablesReference : <number>
}
{
result : function hello() { return "world" }
type : string
variablesReference : <number>
}
{
result : { "foo": true, "recurse": "[Circular ~]" }
type : string
variablesReference : <number>
}
{
result : "1267650600228229401496703205376"
type : string
variablesReference : <number>
}
{
result : <div>hi</div>
type : string
variablesReference : <number>
}
24 changes: 24 additions & 0 deletions src/test/evaluate/evaluate-copy-via-function.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
text : hello
}
{
text : 123n
}
{
text : NaN
}
{
text : { "foo": "bar", "baz": { "a": [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], "b": 123 } }
}
{
text : function hello() { return "world" }
}
{
text : { "foo": true, "recurse": "[Circular ~]" }
}
{
text : 1267650600228229401496703205376n
}
{
text : <div>hi</div>
}
12 changes: 0 additions & 12 deletions src/test/evaluate/evaluate-copy.txt

This file was deleted.

32 changes: 25 additions & 7 deletions src/test/evaluate/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,34 @@ describe('evaluate', () => {
p.assertLog();
});

itIntegrates('copy', async ({ r }) => {
const copyExpressions = [
'123n',
'NaN',
'{foo: "bar", baz: { a: [1, 2, 3, 4, 5, 6, 7, 8, 9], b: 123n }}',
'function hello() { return "world" }',
'(() => { const n = { foo: true }; n.recurse = n; return n })()',
'1n << 100n',
'(() => { const node = document.createElement("div"); node.innerText = "hi"; return node })()',
];

itIntegrates('copy via function', async ({ r }) => {
const p = await r.launchAndLoad('blank');
p.dap.evaluate({ expression: 'var x = "hello"; copy(x)' });
p.log(await p.dap.once('copyRequested'));
p.dap.evaluate({ expression: 'copy(123n)' });
p.log(await p.dap.once('copyRequested'));
p.dap.evaluate({ expression: 'copy(NaN)' });
p.log(await p.dap.once('copyRequested'));
p.dap.evaluate({ expression: 'copy({foo: "bar"})' });
p.log(await p.dap.once('copyRequested'));
for (const expression of copyExpressions) {
p.dap.evaluate({ expression: `copy(${expression})` });
p.log(await p.dap.once('copyRequested'));
}

p.assertLog();
});

itIntegrates('copy via evaluate context', async ({ r }) => {
const p = await r.launchAndLoad('blank');
for (const expression of copyExpressions) {
p.log(await p.dap.evaluate({ expression, context: 'clipboard' }));
}

p.assertLog();
});

Expand Down

0 comments on commit a136ba5

Please sign in to comment.