mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
Expression format specifier support (#87)
* initial support for format specifier * add readme notes for format specifiers * add support for showing arrays and objects with format specifiers * create unique object variable references for different display formats * add notes on applying formatting to objects and arrays
This commit is contained in:
33
README.md
33
README.md
@@ -129,6 +129,39 @@ Add a new task to run the build command:
|
||||
}
|
||||
```
|
||||
|
||||
## Expression evaluation
|
||||
|
||||
Format specifiers can be appended to watch and repl expressions to change how the evaluated result is displayed.
|
||||
The specifiers work with the same syntax used in Visual Studio.
|
||||
See https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp for examples.
|
||||
|
||||
```
|
||||
123 123
|
||||
123,x 0x0000007b
|
||||
123,xb 0000007b
|
||||
123,X 0x0000007B
|
||||
123,o 000000000173
|
||||
123,b 0b00000000000000000000000001111011
|
||||
123,bb 00000000000000000000000001111011
|
||||
123,c '{'
|
||||
"one\ntwo" "one\ntwo"
|
||||
"one\ntwo",sb one\ntwo
|
||||
"one\ntwo",! one
|
||||
two
|
||||
```
|
||||
|
||||
You can also apply the specifiers to object and array instances to format fields and elements:
|
||||
```
|
||||
arr,x int[3]
|
||||
[0] 0x00000001
|
||||
[1] 0x00000002
|
||||
[1] 0x00000003
|
||||
```
|
||||
|
||||
|
||||
Note: Format specifiers for floating point values (`e`/`g`) and string encoding conversions (`s8`/`su`/`s32`) are not supported.
|
||||
|
||||
|
||||
## Powered by coffee
|
||||
|
||||
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you
|
||||
|
||||
@@ -79,7 +79,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
// number of call stack entries to display above the project source
|
||||
this.callStackDisplaySize = 1;
|
||||
|
||||
// the fifo queue of evaluations (watches, hover, etc)
|
||||
/**
|
||||
* the fifo queue of evaluations (watches, hover, etc)
|
||||
* @type {EvalQueueEntry[]}
|
||||
*/
|
||||
this._evals_queue = [];
|
||||
|
||||
// since we want to send breakpoint events, we will assign an id to every event
|
||||
@@ -1269,7 +1272,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
const stack_frame = thread.findStackFrame(args.variablesReference);
|
||||
// evaluate the expression
|
||||
const locals = await stack_frame.getLocals();
|
||||
const value = await evaluate(args.value, thread, locals, this.dbgr);
|
||||
const { value } = await evaluate(args.value, thread, locals, this.dbgr);
|
||||
// update the variable
|
||||
const vsvar = await stack_frame.setVariableValue(args.variablesReference, args.name, value);
|
||||
response.body = {
|
||||
@@ -1307,25 +1310,15 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendResponse(prev.response);
|
||||
}
|
||||
|
||||
const eval_info = {
|
||||
expression: args.expression,
|
||||
response,
|
||||
/** @type {DebuggerValue[]} */
|
||||
locals: null,
|
||||
/** @type {VariableManager} */
|
||||
var_manager: null,
|
||||
/** @type {AndroidThread} */
|
||||
thread: null,
|
||||
}
|
||||
let eval_info;
|
||||
if (args.frameId) {
|
||||
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
|
||||
const thread = this.getThread(threadId);
|
||||
if (!thread) return this.failRequestNoThread('Evaluate',threadId, response);
|
||||
if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response);
|
||||
eval_info.thread = thread;
|
||||
const stack_frame = thread.findStackFrame(args.frameId);
|
||||
eval_info.var_manager = stack_frame;
|
||||
eval_info.locals = await stack_frame.getLocals();
|
||||
const locals = await stack_frame.getLocals();
|
||||
eval_info = new EvalQueueEntry(args.expression, response, locals, stack_frame, thread);
|
||||
} else {
|
||||
// if there's no frameId, we are being asked to evaluate the value in the 'global' context.
|
||||
// This is a problem because there's no associated stack frame, so we include any locals in the evaluation.
|
||||
@@ -1334,9 +1327,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// would require primitive literals)
|
||||
const thread = this._threads.find(t => t && t.paused);
|
||||
if (!thread) return this.failRequest(`No threads are paused`, response);
|
||||
eval_info.thread = thread;
|
||||
eval_info.var_manager = thread.getGlobalVariableManager();
|
||||
eval_info.locals = [];
|
||||
eval_info = new EvalQueueEntry(args.expression, response, [], thread.getGlobalVariableManager(), thread);
|
||||
}
|
||||
|
||||
const queue_len = this._evals_queue.push(eval_info);
|
||||
@@ -1347,8 +1338,8 @@ class AndroidDebugSession extends DebugSession {
|
||||
while (this._evals_queue.length > 0) {
|
||||
const { expression, response, locals, var_manager, thread } = this._evals_queue[0];
|
||||
try {
|
||||
const value = await evaluate(expression, thread, locals, this.dbgr);
|
||||
const v = var_manager.makeVariableValue(value);
|
||||
const { value, display_format } = await evaluate(expression, thread, locals, this.dbgr, { allowFormatSpecifier:true });
|
||||
const v = var_manager.makeVariableValue(value, display_format);
|
||||
response.body = {
|
||||
result: v.value,
|
||||
variablesReference: v.variablesReference|0
|
||||
@@ -1363,6 +1354,23 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
class EvalQueueEntry {
|
||||
/**
|
||||
* @param {string} expression
|
||||
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateResponse} response
|
||||
* @param {DebuggerValue[]} locals
|
||||
* @param {VariableManager} var_manager
|
||||
* @param {AndroidThread} thread
|
||||
*/
|
||||
constructor(expression, response, locals, var_manager, thread) {
|
||||
this.expression = expression;
|
||||
this.response = response;
|
||||
this.locals = locals;
|
||||
this.var_manager = var_manager;
|
||||
this.thread = thread;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} p
|
||||
*/
|
||||
|
||||
@@ -471,7 +471,7 @@ async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operat
|
||||
* @param {DebuggerValue[]} locals
|
||||
* @param {AndroidThread} thread
|
||||
* @param {string} operator
|
||||
* @param {*} expr
|
||||
* @param {ParsedExpression} expr
|
||||
*/
|
||||
async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) {
|
||||
/** @type {DebuggerValue} */
|
||||
@@ -684,7 +684,7 @@ function evaluate_expression(dbgr, locals, thread, expr) {
|
||||
* @param {Debugger} dbgr
|
||||
* @param {DebuggerValue[]} locals
|
||||
* @param {AndroidThread} thread
|
||||
* @param {string} index_expr
|
||||
* @param {ParsedExpression} index_expr
|
||||
* @param {DebuggerValue} arr_local
|
||||
*/
|
||||
async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) {
|
||||
@@ -712,36 +712,36 @@ async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_loca
|
||||
/**
|
||||
* Build a regular expression which matches the possible parameter types for a value
|
||||
* @param {Debugger} dbgr
|
||||
* @param {DebuggerValue} v
|
||||
* @param {DebuggerValue} argument
|
||||
*/
|
||||
async function getParameterSignatureRegex(dbgr, v) {
|
||||
if (v.type.signature == 'Lnull;') {
|
||||
async function getParameterSignatureRegex(dbgr, argument) {
|
||||
if (argument.type.signature == 'Lnull;') {
|
||||
return /^[LT[]/; // null matches any reference type
|
||||
}
|
||||
if (/^L/.test(v.type.signature)) {
|
||||
if (/^L/.test(argument.type.signature)) {
|
||||
// for class reference types, retrieve a list of inherited classes
|
||||
// since subclass instances can be passed as arguments
|
||||
const sigs = await dbgr.getClassInheritanceList(v.type.signature);
|
||||
const sigs = await dbgr.getClassInheritanceList(argument.type.signature);
|
||||
const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$'));
|
||||
return new RegExp(`(^${re_sigs.join('$)|(^')}$)`);
|
||||
}
|
||||
if (/^\[/.test(v.type.signature)) {
|
||||
if (/^\[/.test(argument.type.signature)) {
|
||||
// for array types, only an exact array match or Object is allowed
|
||||
return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`);
|
||||
return new RegExp(`^(${argument.type.signature})|(${JavaType.Object.signature})$`);
|
||||
}
|
||||
switch(v.type.signature) {
|
||||
switch(argument.type.signature) {
|
||||
case 'I':
|
||||
// match bytes/shorts/ints/longs/floats/doubles literals within range
|
||||
if (v.value >= -128 && v.value <= 127)
|
||||
if (argument.value >= -128 && argument.value <= 127)
|
||||
return /^[BSIJFD]$/
|
||||
if (v.value >= -32768 && v.value <= 32767)
|
||||
if (argument.value >= -32768 && argument.value <= 32767)
|
||||
return /^[SIJFD]$/
|
||||
return /^[IJFD]$/;
|
||||
case 'F':
|
||||
return /^[FD]$/; // floats can be assigned to floats or doubles
|
||||
default:
|
||||
// anything else must be an exact match (no implicit cast is valid)
|
||||
return new RegExp(`^${v.type.signature}$`);
|
||||
return new RegExp(`^${argument.type.signature}$`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,8 +950,9 @@ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
|
||||
* @param {AndroidThread} thread
|
||||
* @param {DebuggerValue[]} locals
|
||||
* @param {Debugger} dbgr
|
||||
* @param {{allowFormatSpecifier:boolean}} [options]
|
||||
*/
|
||||
async function evaluate(expression, thread, locals, dbgr) {
|
||||
async function evaluate(expression, thread, locals, dbgr, options) {
|
||||
D('evaluate: ' + expression);
|
||||
await dbgr.ensureConnected();
|
||||
|
||||
@@ -967,6 +968,17 @@ async function evaluate(expression, thread, locals, dbgr) {
|
||||
}
|
||||
const parsed_expression = parse_expression(e);
|
||||
|
||||
let display_format = null;
|
||||
if (options && options.allowFormatSpecifier) {
|
||||
// look for formatting specifiers in the form of ',<x>'
|
||||
// ref: https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp
|
||||
const df_match = e.expr.match(/^,([doc!]|[xX]b?|bb?|sb?)/);
|
||||
if (df_match) {
|
||||
display_format = df_match[1];
|
||||
e.expr = e.expr.slice(df_match[0].length)
|
||||
}
|
||||
}
|
||||
|
||||
// if there's anything left, it's an error
|
||||
if (!parsed_expression || e.expr) {
|
||||
// the expression is not well-formed
|
||||
@@ -975,7 +987,11 @@ async function evaluate(expression, thread, locals, dbgr) {
|
||||
|
||||
// the expression is well-formed - start the (asynchronous) evaluation
|
||||
const value = await evaluate_expression(dbgr, locals, thread, parsed_expression);
|
||||
return value;
|
||||
|
||||
return {
|
||||
value,
|
||||
display_format,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -53,7 +53,7 @@ class DebuggerStackFrame extends VariableManager {
|
||||
}
|
||||
const fetch_locals = async () => {
|
||||
const values = await this.dbgr.getLocals(this.frame);
|
||||
// display the variables in (case-insensitive) alphabetical order, with 'this' first and all-caps last
|
||||
// display the variables in (case-insensitive) alphabetical order, with 'this' first
|
||||
return this.locals = sortVariables(values, true, false);
|
||||
}
|
||||
// @ts-ignore
|
||||
@@ -133,7 +133,7 @@ class DebuggerStackFrame extends VariableManager {
|
||||
values = [await this.getBigString(varinfo)];
|
||||
}
|
||||
|
||||
return (varinfo.cached = values).map(v => this.makeVariableValue(v));
|
||||
return (varinfo.cached = values).map(v => this.makeVariableValue(v, varinfo.display_format));
|
||||
}
|
||||
|
||||
async getObjectFields(varinfo) {
|
||||
|
||||
@@ -12,8 +12,6 @@ class VariableManager {
|
||||
* @param {VSCVariableReference} base_variable_reference The reference value for values stored by this manager
|
||||
*/
|
||||
constructor(base_variable_reference) {
|
||||
// expandable variables get allocated new variable references.
|
||||
this._expandable_prims = false;
|
||||
|
||||
/** @type {VSCVariableReference} */
|
||||
this.nextVariableRef = base_variable_reference + 10;
|
||||
@@ -40,10 +38,20 @@ class VariableManager {
|
||||
this.variableValues.set(variablesReference, value);
|
||||
}
|
||||
|
||||
_getObjectIdReference(type, objvalue) {
|
||||
/**
|
||||
* Retrieve or create a variable reference for a given object instance
|
||||
* @param {JavaType} type
|
||||
* @param {JavaObjectID} instance_id
|
||||
* @param {string} display_format
|
||||
*/
|
||||
_getObjectIdReference(type, instance_id, display_format) {
|
||||
// we need the type signature because we must have different id's for
|
||||
// an instance and it's supertype instance (which obviously have the same objvalue)
|
||||
const key = type.signature + objvalue;
|
||||
// an instance and it's supertype instance (which obviously have the same instance_id)
|
||||
//
|
||||
// display_format is also included to give unique variable references for each display type.
|
||||
// This is because VSCode caches expanded values, so once evaluated in one format, they can
|
||||
// never be changed.
|
||||
const key = `${type.signature}:${instance_id}:${display_format || ''}`;
|
||||
let value = this.objIdCache.get(key);
|
||||
if (!value) {
|
||||
this.objIdCache.set(key, value = this.nextVariableRef += 1);
|
||||
@@ -54,12 +62,12 @@ class VariableManager {
|
||||
/**
|
||||
* Convert to a VariableValue object used by VSCode
|
||||
* @param {DebuggerValue} v
|
||||
* @param {string} [display_format]
|
||||
*/
|
||||
makeVariableValue(v) {
|
||||
makeVariableValue(v, display_format) {
|
||||
let varref = 0;
|
||||
let value = '';
|
||||
const evaluateName = v.fqname || v.name;
|
||||
const formats = {};
|
||||
const full_typename = v.type.fullyQualifiedName();
|
||||
switch(true) {
|
||||
case v.hasnullvalue && JavaType.isReference(v.type):
|
||||
@@ -74,93 +82,161 @@ class VariableManager {
|
||||
value = v.type.typename;
|
||||
break;
|
||||
case v.type.signature === JavaType.String.signature:
|
||||
value = JSON.stringify(v.string);
|
||||
if (v.biglen) {
|
||||
// since this is a big string - make it viewable on expand
|
||||
varref = this._addVariable({
|
||||
bigstring: v,
|
||||
});
|
||||
value = `String (length:${v.biglen})`;
|
||||
}
|
||||
else if (this._expandable_prims) {
|
||||
// as a courtesy, allow strings to be expanded to see their length
|
||||
varref = this._addVariable({
|
||||
signature: v.type.signature,
|
||||
primitive: true,
|
||||
value: v.string.length
|
||||
});
|
||||
} else {
|
||||
value = formatString(v.string, display_format);
|
||||
}
|
||||
break;
|
||||
case JavaType.isArray(v.type):
|
||||
// non-null array type - if it's not zero-length add another variable reference so the user can expand
|
||||
if (v.arraylen) {
|
||||
varref = this._getObjectIdReference(v.type, v.value);
|
||||
varref = this._getObjectIdReference(v.type, v.value, display_format);
|
||||
this._setVariable(varref, {
|
||||
varref,
|
||||
arrvar: v,
|
||||
range:[0, v.arraylen],
|
||||
display_format,
|
||||
});
|
||||
}
|
||||
value = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
|
||||
break;
|
||||
case JavaType.isClass(v.type):
|
||||
// non-null object instance - add another variable reference so the user can expand
|
||||
varref = this._getObjectIdReference(v.type, v.value);
|
||||
varref = this._getObjectIdReference(v.type, v.value, display_format);
|
||||
this._setVariable(varref, {
|
||||
varref,
|
||||
objvar: v,
|
||||
display_format,
|
||||
});
|
||||
value = v.type.typename;
|
||||
break;
|
||||
case v.type.signature === JavaType.char.signature:
|
||||
// character types have a integer value
|
||||
const char = String.fromCodePoint(v.value);
|
||||
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\','\0':'0'};
|
||||
if (cmap[char]) {
|
||||
value = `'\\${cmap[char]}'`;
|
||||
} else if (v.value < 32) {
|
||||
value = `'\\u${v.value.toString(16).padStart(4,'0')}'`;
|
||||
} else value = `'${char}'`;
|
||||
value = formatChar(v.value, display_format);
|
||||
break;
|
||||
case v.type.signature === JavaType.long.signature:
|
||||
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||
const v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
||||
value = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
|
||||
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
|
||||
formats.oct = formats.bin = '';
|
||||
// 24 bit chunks...
|
||||
for (let s = v64hex; s; s = s.slice(0,-6)) {
|
||||
const uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
|
||||
formats.oct = uint.toString(8) + formats.oct;
|
||||
formats.bin = uint.toString(2) + formats.bin;
|
||||
}
|
||||
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
|
||||
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
|
||||
value = formatLong(v64hex, display_format);
|
||||
break;
|
||||
case JavaType.isInteger(v.type):
|
||||
value = formats.dec = v.value.toString();
|
||||
const uint = (v.value >>> 0);
|
||||
formats.hex = '0x' + uint.toString(16);
|
||||
formats.oct = '0c' + uint.toString(8);
|
||||
formats.bin = '0b' + uint.toString(2);
|
||||
value = formatInteger(v.value, v.type.signature, display_format);
|
||||
break;
|
||||
default:
|
||||
// other primitives: boolean, etc
|
||||
value = v.value.toString();
|
||||
break;
|
||||
}
|
||||
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
|
||||
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
|
||||
varref = this._addVariable({
|
||||
signature: v.type.signature,
|
||||
primitive: true,
|
||||
value: v.value,
|
||||
});
|
||||
}
|
||||
|
||||
return new VariableValue(v.name, value, full_typename, varref, evaluateName);
|
||||
}
|
||||
}
|
||||
|
||||
const cmap = {
|
||||
'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t',
|
||||
'\v':'v','\'':'\'','\\':'\\','\0':'0'
|
||||
};
|
||||
|
||||
function makeJavaChar(i) {
|
||||
let value;
|
||||
const char = String.fromCodePoint(i);
|
||||
if (cmap[char]) {
|
||||
value = `'\\${cmap[char]}'`;
|
||||
} else if (i < 32) {
|
||||
value = `'\\u${i.toString(16).padStart(4,'0')}'`;
|
||||
} else value = `'${char}'`;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} c
|
||||
* @param {string} df
|
||||
*/
|
||||
function formatChar(c, df) {
|
||||
if (/[xX]b|o|bb|d/.test(df)) {
|
||||
return formatInteger(c, 'C', df);
|
||||
}
|
||||
return makeJavaChar(c);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} s
|
||||
* @param {string} display_format
|
||||
*/
|
||||
function formatString(s, display_format) {
|
||||
if (display_format === '!') {
|
||||
return s;
|
||||
}
|
||||
let value = JSON.stringify(s);
|
||||
if (display_format === 'sb') {
|
||||
// remove quotes
|
||||
value = value.slice(1,-1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {hex64} hex64
|
||||
* @param {string} df
|
||||
*/
|
||||
function formatLong(hex64, df) {
|
||||
let minlength;
|
||||
if (/[xX]b?/.test(df)) {
|
||||
minlength = Math.ceil(64 / 4);
|
||||
let s = `${df[1]?'':'0x'}${hex64.padStart(minlength,'0')}`;
|
||||
return df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
|
||||
}
|
||||
if (/o/.test(df)) {
|
||||
minlength = Math.ceil(64 / 3);
|
||||
return `${df[1]?'':'0'}${NumberBaseConverter.convertBase(hex64,16,8).padStart(minlength, '0')}`;
|
||||
}
|
||||
if (/bb?/.test(df)) {
|
||||
minlength = 64;
|
||||
return `${df[1]?'':'0b'}${NumberBaseConverter.convertBase(hex64,16,2).padStart(minlength, '0')}`;
|
||||
}
|
||||
if (/c/.test(df)) {
|
||||
return makeJavaChar(parseInt(hex64.slice(-4), 16));
|
||||
}
|
||||
return NumberBaseConverter.convertBase(hex64, 16, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} i
|
||||
* @param {string} signature
|
||||
* @param {string} df
|
||||
*/
|
||||
function formatInteger(i, signature, df) {
|
||||
const bits = { B:8,S:16,I:32,C:16 }[signature];
|
||||
let u = (i & (-1 >>> (32 - bits))) >>> 0;
|
||||
let minlength;
|
||||
if (/[xX]b?/.test(df)) {
|
||||
minlength = Math.ceil(bits / 4);
|
||||
let s = u.toString(16).padStart(minlength,'0');
|
||||
s = df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
|
||||
return `${df[1]?'':'0x'}${s}`;
|
||||
}
|
||||
if (/o/.test(df)) {
|
||||
minlength = Math.ceil(bits / 3);
|
||||
return `${df[1]?'':'0'}${u.toString(8).padStart(minlength, '0')}`;
|
||||
}
|
||||
if (/bb?/.test(df)) {
|
||||
minlength = bits;
|
||||
return `${df[1]?'':'0b'}${u.toString(2).padStart(minlength, '0')}`;
|
||||
}
|
||||
if (/c/.test(df)) {
|
||||
minlength = bits;
|
||||
return makeJavaChar(u & 0xffff);
|
||||
}
|
||||
return i.toString();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
VariableManager,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user