commit 4440bfbe18f2f534b791c64266275b1d52867276 Author: WebFreak001 Date: Sun Jan 10 18:03:51 2016 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e5962e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out +node_modules \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..08869ac --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "out/src", + "preLaunchTask": "npm" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "out/test", + "preLaunchTask": "npm" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3f5aa9c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1992757 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +// A task runner that calls a custom npm script that compiles the extension. +{ + "version": "0.1.0", + + // we want to run npm + "command": "npm", + + // the command is a shell script + "isShellCommand": true, + + // show the output window only if unrecognized errors occur. + "showOutput": "silent", + + // we run the custom script "compile" as defined in package.json + "args": ["run", "compile", "--loglevel", "silent"], + + // The tsc compiler is started in watching mode + "isWatching": true, + + // use the standard tsc in watch mode problem matcher to find compile problems in the output. + "problemMatcher": "$tsc-watch" +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..795e714 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +typings/** +out/test/** +test/** +src/** +**/*.map +.gitignore +tsconfig.json +vsc-extension-quickstart.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..83072a6 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Debug + +Native VSCode debugger. Currently only using GDB. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..79d6b7a --- /dev/null +++ b/package.json @@ -0,0 +1,73 @@ +{ + "name": "debug", + "displayName": "Debug", + "description": "Native debugging for VSCode - Currently in GDB only beta", + "version": "0.1.0", + "publisher": "WebFreak", + "engines": { + "vscode": "^0.10.1" + }, + "categories": [ + "Debuggers" + ], + "contributes": { + "debuggers": [ + { + "type": "gdb", + "extensions": [], + "program": "./out/src/gdb.js", + "runtime": "node", + "label": "GDB", + "enableBreakpointsFor": { + "languageIds": [ + "c", + "cpp", + "d", + "objective-c", + "fortan", + "pascal", + "ada" + ] + }, + "configurationAttributes": { + "launch": { + "required": [ + "target" + ], + "properties": { + "target": { + "type": "string", + "description": "Path of executable" + }, + "cwd": { + "type": "string", + "description": "Path of project" + } + } + } + }, + "initialConfigurations": [ + { + "name": "Debug", + "type": "gdb", + "request": "launch", + "target": "./output", + "cwd": "${workspaceRoot}" + } + ] + } + ] + }, + "scripts": { + "vscode:prepublish": "node ./node_modules/vscode/bin/compile", + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" + }, + "dependencies": { + "vscode-debugadapter": "^1.0.1", + "vscode-debugprotocol": "^1.0.1" + }, + "devDependencies": { + "typescript": "^1.6.2", + "vscode": "0.10.x" + } +} \ No newline at end of file diff --git a/src/backend/backend.ts b/src/backend/backend.ts new file mode 100644 index 0000000..f262b6f --- /dev/null +++ b/src/backend/backend.ts @@ -0,0 +1,32 @@ +export interface Breakpoint { + file: string; + line: number; +} + +export interface Stack { + level: number; + address: string; + function: string; + fileName: string; + file: string; + line: number; +} + +export interface IBackend { + load(cwd: string, target: string): Thenable; + start(): Thenable; + stop(); + interrupt(): Thenable; + continue(): Thenable; + next(): Thenable; + step(): Thenable; + stepOut(): Thenable; + loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]>; + addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]>; + removeBreakPoint(breakpoint: Breakpoint): Thenable; + clearBreakPoints(): Thenable; + getStack(maxLevels: number): Thenable; + getStackVariables(thread: number, frame: number): Thenable<[string, string][]>; + evalExpression(name: string): Thenable; + isReady(): boolean; +} \ No newline at end of file diff --git a/src/backend/gdb_expansion.ts b/src/backend/gdb_expansion.ts new file mode 100644 index 0000000..3bc195e --- /dev/null +++ b/src/backend/gdb_expansion.ts @@ -0,0 +1,201 @@ +/* + { + quit = false, + _views = { + { + view = 0x7ffff7ece1e8, + renderer = 0x7ffff7eccc50, + world = 0x7ffff7ece480 + } + }, + deltaTimer = { + _flagStarted = false, + _timeStart = {length = 0}, + _timeMeasured = {length = 0} + }, + _start = {callbacks = 0x0}, + _stop = {callbacks = 0x0} + } +*/ + +const resultRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*)\s*=\s*/; +const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/; +const errorRegex = /^\<.+?\>/; +const referenceRegex = /^0x[0-9a-fA-F]+/; +const numberRegex = /^[0-9]+/; + +export function isExpandable(value: string): number { + let primitive: any; + let match; + value = value.trim(); + if (value.length == 0) return 0; + else if (value[0] == '{') return 1; // object + else if (value.startsWith("true")) return 0; + else if (value.startsWith("false")) return 0; + else if (value.startsWith("0x0")) return 0; + else if (match = referenceRegex.exec(value)) return 2; // reference + else if (match = numberRegex.exec(value)) return 0; + else if (match = variableRegex.exec(value)) return 0; + else if (match = errorRegex.exec(value)) return 0; + else return 0; +} + +export function expandValue(variableCreate: Function, value: string): any { + let parseCString = () => { + value = value.trim(); + if (value[0] != '"') + return ""; + let stringEnd = 1; + let inString = true; + let remaining = value.substr(1); + let escaped = false; + while (inString) { + if (escaped) + escaped = false; + else if (remaining[0] == '\\') + escaped = true; + else if (remaining[0] == '"') + inString = false; + + remaining = remaining.substr(1); + stringEnd++; + } + let str = value.substr(0, stringEnd).trim(); + value = value.substr(stringEnd).trim(); + return str; + }; + + let parseValue, parseCommaResult, parseCommaValue, parseResult; + + let parseTupleOrList = () => { + value = value.trim(); + if (value[0] != '{') + return undefined; + let oldContent = value; + value = value.substr(1).trim(); + if (value[0] == '}') + return []; + let eqPos = value.indexOf("="); + let newValPos1 = value.indexOf("{"); + let newValPos2 = value.indexOf(","); + let newValPos = newValPos1; + if (newValPos2 != -1 && newValPos2 < newValPos1) + newValPos = newValPos2; + if (newValPos != -1 && eqPos > newValPos || eqPos == -1) { // is value list + let values = []; + let val = parseValue(); + values.push(val); + let remaining = value; + while (val = parseCommaValue()) + values.push(val); + value = value.substr(1).trim(); // } + return values; + } + + let result = parseResult(); + if (result) { + let results = []; + results.push(result); + while (result = parseCommaResult()) + results.push(result); + value = value.substr(1).trim(); // } + return results; + } + + return undefined; + }; + + let parsePrimitive = () => { + let primitive: any; + let match; + value = value.trim(); + if (value.length == 0) + primitive = undefined; + else if (value.startsWith("true")) { + primitive = "true"; + value = value.substr(4).trim(); + } + else if (value.startsWith("false")) { + primitive = "false"; + value = value.substr(5).trim(); + } + else if (value.startsWith("0x0")) { + primitive = ""; + value = value.substr(3).trim(); + } + else if (match = referenceRegex.exec(value)) { + primitive = "*" + match[0]; + value = value.substr(match[0].length).trim(); + } + else if (match = numberRegex.exec(value)) { + primitive = match[0]; + value = value.substr(match[0].length).trim(); + } + else if (match = variableRegex.exec(value)) { + primitive = match[0]; + value = value.substr(match[0].length).trim(); + } + else if (match = errorRegex.exec(value)) { + primitive = match[0]; + value = value.substr(match[0].length).trim(); + } + else { + primitive = ""; + } + return primitive; + }; + + parseValue = () => { + value = value.trim(); + if (value[0] == '"') + return parseCString(); + else if (value[0] == '{') + return parseTupleOrList(); + else + return parsePrimitive(); + }; + + parseResult = () => { + value = value.trim(); + let variableMatch = resultRegex.exec(value); + if (!variableMatch) + return undefined; + value = value.substr(variableMatch[0].length).trim(); + let variable = variableMatch[1]; + let val = parseValue(); + let ref = 0; + if (typeof val == "object") { + ref = variableCreate(val); + val = "Object"; + } + if (typeof val == "string" && val.startsWith("*0x")) { + ref = variableCreate("*" + variable); + val = "Object@" + val; + } + return { + name: variable, + value: val, + variablesReference: ref + }; + }; + + parseCommaValue = () => { + value = value.trim(); + if (value[0] != ',') + return undefined; + value = value.substr(1).trim(); + return parseValue(); + }; + + parseCommaResult = () => { + value = value.trim(); + if (value[0] != ',') + return undefined; + value = value.substr(1).trim(); + return parseResult(); + }; + + + value = value.trim(); + return parseValue(); +} \ No newline at end of file diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts new file mode 100644 index 0000000..2d3fcd0 --- /dev/null +++ b/src/backend/mi2/mi2.ts @@ -0,0 +1,278 @@ +import { Breakpoint, IBackend, Stack } from "../backend.ts" +import * as ChildProcess from "child_process" +import { EventEmitter } from "events" +import { parseMI, MINode } from '../mi_parse'; + +export class MI2 extends EventEmitter implements IBackend { + constructor(public application: string, public preargs: string[]) { + super(); + } + + load(cwd: string, target: string): Thenable { + return new Promise((resolve, reject) => { + this.process = ChildProcess.spawn(this.application, this.preargs.concat([target]), { cwd: cwd }); + this.process.stdout.on("data", this.stdout.bind(this)); + this.process.on("exit", (() => { this.emit("quit"); }).bind(this)); + Promise.all([ + this.sendCommand("environment-directory \"" + cwd + "\"") + ]).then(resolve, reject); + }); + } + + stdout(data) { + this.buffer += data.toString("utf8"); + let end = this.buffer.lastIndexOf('\n'); + if (end != -1) { + this.onOutput(this.buffer.substr(0, end)); + this.buffer = this.buffer.substr(end + 1); + } + } + + onOutput(lines) { + lines = lines.split('\n'); + lines.forEach(line => { + let parsed = parseMI(line); + //this.log("log", JSON.stringify(parsed)); + let handled = false; + if (parsed.token !== undefined) { + if (this.handlers[parsed.token]) { + this.handlers[parsed.token](parsed); + delete this.handlers[parsed.token]; + handled = true; + } + } + if (parsed.resultRecords && parsed.resultRecords.resultClass == "error") { + this.log("log", "An error occured: " + parsed.result("msg")); + } + if (parsed.outOfBandRecord) { + parsed.outOfBandRecord.forEach(record => { + if (record.isStream) { + this.log(record.type, record.content); + } else { + if (record.type == "exec") { + this.emit("exec-async-output", parsed); + if (record.asyncClass == "running") + this.emit("running", parsed); + else if (record.asyncClass == "stopped") { + if (parsed.record("reason") == "breakpoint-hit") + this.emit("breakpoint", parsed); + else if (parsed.record("reason") == "end-stepping-range") + this.emit("step-end", parsed); + else if (parsed.record("reason") == "function-finished") + this.emit("step-out-end", parsed); + else + this.emit("stopped", parsed); + } else + this.log("log", JSON.stringify(parsed)); + } + } + }); + handled = true; + } + if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0) + handled = true; + if (!handled) + this.log("log", "Unhandled: " + JSON.stringify(parsed)); + }); + } + + start(): Thenable { + return new Promise((resolve, reject) => { + this.log("console", "Running executable"); + this.sendCommand("exec-run").then((info) => { + resolve(info.resultRecords.resultClass == "running"); + }, reject); + }); + } + + stop() { + let proc = this.process; + let to = setTimeout(() => { + proc.kill(); + }, 2222); + this.process.on("exit", function(code) { + clearTimeout(to); + }); + this.sendRaw("-gdb-exit"); + } + + interrupt(): Thenable { + return new Promise((resolve, reject) => { + this.sendCommand("exec-interrupt").then((info) => { + resolve(info.resultRecords.resultClass == "done"); + }, reject); + }); + } + + continue(): Thenable { + return new Promise((resolve, reject) => { + this.sendCommand("exec-continue").then((info) => { + resolve(info.resultRecords.resultClass == "running"); + }, reject); + }); + } + + next(): Thenable { + return new Promise((resolve, reject) => { + this.sendCommand("exec-next").then((info) => { + resolve(info.resultRecords.resultClass == "running"); + }, reject); + }); + } + + step(): Thenable { + return new Promise((resolve, reject) => { + this.sendCommand("exec-step").then((info) => { + resolve(info.resultRecords.resultClass == "running"); + }, reject); + }); + } + + stepOut(): Thenable { + return new Promise((resolve, reject) => { + this.sendCommand("exec-finish").then((info) => { + resolve(info.resultRecords.resultClass == "running"); + }, reject); + }); + } + + loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]> { + let promisses = []; + breakpoints.forEach(breakpoint => { + promisses.push(this.addBreakPoint(breakpoint)); + }); + return Promise.all(promisses); + } + + addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]> { + return new Promise((resolve, reject) => { + if (this.breakpoints.has(breakpoint)) + return resolve(false); + this.sendCommand("break-insert " + breakpoint.file + ":" + breakpoint.line).then((result) => { + if (result.resultRecords.resultClass == "done") { + let newBrk = { + file: result.result("bkpt.file"), + line: parseInt(result.result("bkpt.line")) + }; + this.breakpoints.set(newBrk, parseInt(result.result("bkpt.number"))); + resolve([true, newBrk]); + } + else { + resolve([false, null]); + } + }); + }); + } + + removeBreakPoint(breakpoint: Breakpoint): Thenable { + return new Promise((resolve, reject) => { + if (!this.breakpoints.has(breakpoint)) + return resolve(false); + this.sendCommand("break-delete " + this.breakpoints.get(breakpoint)).then((result) => { + if (result.resultRecords.resultClass == "done") { + this.breakpoints.delete(breakpoint); + resolve(true); + } + else resolve(false); + }); + }); + } + + clearBreakPoints(): Thenable { + let promisses = []; + let it = this.breakpoints.keys(); + let value; + while (!(value = it.next()).done) { + promisses.push(this.removeBreakPoint(value.value)); + } + return Promise.all(promisses); + } + + getStack(maxLevels: number): Thenable { + return new Promise((resolve, reject) => { + let command = "stack-list-frames"; + if (maxLevels) { + command += " 0 " + maxLevels; + } + this.sendCommand(command).then((result) => { + let stack = result.result("stack"); + let ret: Stack[] = []; + stack.forEach(element => { + let level = MINode.valueOf(element, "@frame.level"); + let addr = MINode.valueOf(element, "@frame.addr"); + let func = MINode.valueOf(element, "@frame.func"); + let filename = MINode.valueOf(element, "@frame.file"); + let file = MINode.valueOf(element, "@frame.fullname"); + let line = parseInt(MINode.valueOf(element, "@frame.line")); + let from = parseInt(MINode.valueOf(element, "@frame.from")); + ret.push({ + address: addr, + fileName: filename || "", + file: file || from || "", + function: func, + level: level, + line: line + }); + }); + resolve(ret); + }); + }); + } + + getStackVariables(thread: number, frame: number): Thenable<[string, string][]> { + return new Promise((resolve, reject) => { + this.sendCommand("stack-list-variables --thread " + thread + " --frame " + frame + " --simple-values").then((result) => { + let variables = result.result("variables"); + let ret: [string, string][] = []; + variables.forEach(element => { + const key = MINode.valueOf(element, "name"); + const value = MINode.valueOf(element, "value"); + ret.push([key, value]); + }); + resolve(ret); + }, reject); + }); + } + + evalExpression(name: string): Thenable { + return new Promise((resolve, reject) => { + this.sendCommand("data-evaluate-expression " + name).then((result) => { + resolve(result); + }, reject); + }); + } + + log(type: string, msg: string) { + this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n")); + } + + sendRaw(raw: string) { + this.process.stdin.write(raw + "\n"); + } + + sendCommand(command: string): Thenable { + return new Promise((resolve, reject) => { + this.handlers[this.currentToken] = (node: MINode) => { + if (node.resultRecords && node.resultRecords.resultClass == "error") { + let msg = node.result("msg") || "Internal error"; + this.log("stderr", "Failed to run command `" + command + "`: " + msg); + reject(msg); + } + else + resolve(node); + }; + this.process.stdin.write(this.currentToken + "-" + command + "\n"); + this.currentToken++; + }); + } + + isReady(): boolean { + return !!this.process; + } + + private currentToken: number = 1; + private handlers: { [index: number]: (info: MINode) => any } = {}; + private breakpoints: Map = new Map(); + private buffer: string; + private process: ChildProcess.ChildProcess; +} \ No newline at end of file diff --git a/src/backend/mi_parse.ts b/src/backend/mi_parse.ts new file mode 100644 index 0000000..7d41d6c --- /dev/null +++ b/src/backend/mi_parse.ts @@ -0,0 +1,262 @@ +export interface MIInfo { + token: number; + outOfBandRecord: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[]; + resultRecords: { resultClass: string, results: [string, any][] }; +} + +export class MINode implements MIInfo { + token: number; + outOfBandRecord: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[]; + resultRecords: { resultClass: string, results: [string, any][] }; + + constructor(token: number, info: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[], result: { resultClass: string, results: [string, any][] }) { + this.token = token; + this.outOfBandRecord = info; + this.resultRecords = result; + } + + record(path: string): any { + if (!this.outOfBandRecord) + return undefined; + return MINode.valueOf(this.outOfBandRecord[0].output, path); + } + + result(path: string): any { + if (!this.resultRecords) + return undefined; + return MINode.valueOf(this.resultRecords.results, path); + } + + static valueOf(start: any, path: string): any { + if (!start) + return undefined; + let pathRegex = /^\.?([a-zA-Z_\-][a-zA-Z0-9_\-]*)/; + let indexRegex = /^\[(\d+)\](?:$|\.)/; + path = path.trim(); + if (!path) + return start; + let current = start; + do { + let target = pathRegex.exec(path); + if (target) { + path = path.substr(target[0].length); + if (current.length && typeof current != "string") { + let found = []; + for (let i = 0; i < current.length; i++) { + let element = current[i]; + if (element[0] == target[1]) { + found.push(element[1]); + } + } + if (found.length > 1) { + current = found; + } else if (found.length == 1) { + current = found[0]; + } else return undefined; + } else return undefined; + } + else if (path[0] == '@') { + current = [current]; + path = path.substr(1); + } + else { + target = indexRegex.exec(path); + if (target) { + path = path.substr(target[0].length); + let i = parseInt(target[1]); + if (current.length && typeof current != "string" && i >= 0 && i < current.length) { + current = current[i]; + } else if (i == 0) { + } else return undefined; + } + else return undefined; + } + path = path.trim(); + } while (path); + return current; + } +} + +const tokenRegex = /^[0-9]+/; +const outOfBandRecordRegex = /^(?:([0-9]*)([\*\+\=])|([\~\@\&]))/; +const resultRecordRegex = /^([0-9]*)\^(done|running|connected|error|exit)/; +const newlineRegex = /^\r\n?/; +const endRegex = /^\(gdb\)\r\n?/; +const variableRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*)/; +const asyncClassRegex = /^(.*?),/; + +export function parseMI(output: string): MINode { + /* + output ==> + ( + exec-async-output = [ token ] "*" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n + status-async-output = [ token ] "+" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n + notify-async-output = [ token ] "=" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n + console-stream-output = "~" c-string \n + target-stream-output = "@" c-string \n + log-stream-output = "&" c-string \n + )* + [ + [ token ] "^" ("done" | "running" | "connected" | "error" | "exit") ( "," variable "=" (const | tuple | list) )* \n + ] + "(gdb)" \n + */ + + let token = undefined; + let outOfBandRecord = []; + let resultRecords = undefined; + + let asyncRecordType = { + "*": "exec", + "+": "status", + "=": "notify" + }; + let streamRecordType = { + "~": "console", + "@": "target", + "&": "log" + }; + + let parseCString = () => { + if (output[0] != '"') + return ""; + let stringEnd = 1; + let inString = true; + let remaining = output.substr(1); + let escaped = false; + while (inString) { + if (escaped) + escaped = false; + else if (remaining[0] == '\\') + escaped = true; + else if (remaining[0] == '"') + inString = false; + + remaining = remaining.substr(1); + stringEnd++; + } + // hax + let str = JSON.parse(output.substr(0, stringEnd)); + output = output.substr(stringEnd); + return str; + }; + + let parseValue, parseCommaResult, parseCommaValue, parseResult; + + let parseTupleOrList = () => { + if (output[0] != '{' && output[0] != '[') + return undefined; + let oldContent = output; + let canBeValueList = output[0] == '['; + output = output.substr(1); + if (output[0] == '}' || output[0] == ']') + return []; + if (canBeValueList) { + let value = parseValue(); + if (value) { // is value list + let values = []; + values.push(value); + let remaining = output; + while (value = parseCommaValue()) + values.push(value); + output = output.substr(1); // ] + return values; + } + } + let result = parseResult(); + if (result) { + let results = []; + results.push(result); + while (result = parseCommaResult()) + results.push(result); + output = output.substr(1); // } + return results; + } + output = (canBeValueList ? '[' : '{') + output; + return undefined; + }; + + parseValue = () => { + if (output[0] == '"') + return parseCString(); + else if (output[0] == '{' || output[0] == '[') + return parseTupleOrList(); + else + return undefined; + }; + + parseResult = () => { + let variableMatch = variableRegex.exec(output); + if (!variableMatch) + return undefined; + output = output.substr(variableMatch[0].length + 1); + let variable = variableMatch[1]; + return [variable, parseValue()]; + }; + + parseCommaValue = () => { + if (output[0] != ',') + return undefined; + output = output.substr(1); + return parseValue(); + }; + + parseCommaResult = () => { + if (output[0] != ',') + return undefined; + output = output.substr(1); + return parseResult(); + }; + + let match = undefined; + + while (match = outOfBandRecordRegex.exec(output)) { + output = output.substr(match[0].length); + if (match[1] && token === undefined) { + token = parseInt(match[1]); + } + + if (match[2]) { + let classMatch = asyncClassRegex.exec(output); + output = output.substr(classMatch[1].length); + let asyncRecord = { + isStream: false, + type: asyncRecordType[match[2]], + asyncClass: classMatch[1], + output: [] + }; + let result; + while (result = parseCommaResult()) + asyncRecord.output.push(result); + outOfBandRecord.push(asyncRecord); + } + else if (match[3]) { + let streamRecord = { + isStream: true, + type: streamRecordType[match[3]], + content: parseCString() + }; + outOfBandRecord.push(streamRecord); + } + + output = output.replace(newlineRegex, ""); + } + + if (match = resultRecordRegex.exec(output)) { + output = output.substr(match[0].length); + if (match[1] && token === undefined) { + token = parseInt(match[1]); + } + resultRecords = { + resultClass: match[2], + results: [] + }; + let result; + while (result = parseCommaResult()) + resultRecords.results.push(result); + + output = output.replace(newlineRegex, ""); + } + + return new MINode(token, outOfBandRecord || [], resultRecords); +} \ No newline at end of file diff --git a/src/gdb.ts b/src/gdb.ts new file mode 100644 index 0000000..91d560c --- /dev/null +++ b/src/gdb.ts @@ -0,0 +1,255 @@ +import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { Breakpoint, IBackend } from './backend/backend' +import { MINode } from './backend/mi_parse' +import { expandValue, isExpandable } from './backend/gdb_expansion' +import { MI2 } from './backend/mi2/mi2' + +export interface LaunchRequestArguments { + cwd: string; + target: string; +} + +class MI2DebugSession extends DebugSession { + private static THREAD_ID = 1; + private gdbDebugger: MI2; + private variableHandles = new Handles(); + private quit: boolean; + + public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { + super(debuggerLinesStartAt1, isServer); + } + + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + this.sendResponse(response); + this.gdbDebugger = new MI2("gdb", ["-q", "--interpreter=mi2"]); + this.gdbDebugger.on("quit", this.quitEvent.bind(this)); + this.gdbDebugger.on("stopped", this.stopEvent.bind(this)); + this.gdbDebugger.on("msg", this.handleMsg.bind(this)); + this.gdbDebugger.on("breakpoint", this.handleBreak.bind(this)); + this.gdbDebugger.on("step-end", this.handleBreak.bind(this)); + this.gdbDebugger.on("step-out-end", this.handleBreak.bind(this)); + this.sendEvent(new InitializedEvent()); + } + + private handleMsg(type: string, msg: string) { + if (type == "target") + type = "stdout"; + if (type == "log") + type = "stderr"; + this.sendEvent(new OutputEvent(msg, type)); + } + + private handleBreak(info: MINode) { + this.sendEvent(new StoppedEvent("step", MI2DebugSession.THREAD_ID)); + } + + private stopEvent(info: MINode) { + if(!this.quit) + this.sendEvent(new StoppedEvent("exception", MI2DebugSession.THREAD_ID)); + } + + private quitEvent() { + this.quit = true; + this.sendEvent(new TerminatedEvent()); + } + + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + this.gdbDebugger.load(args.cwd, args.target).then(() => { + this.gdbDebugger.start().then(() => { + this.sendResponse(response); + }); + }); + } + + protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { + this.gdbDebugger.stop(); + this.sendResponse(response); + } + + protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { + this.gdbDebugger.clearBreakPoints().then(() => { + let path = args.source.path; + let lines = args.lines; + let all = []; + lines.forEach(line => { + all.push(this.gdbDebugger.addBreakPoint({ file: path, line: line })); + }); + Promise.all(all).then(brkpoints => { + response.body.breakpoints = brkpoints; + this.sendResponse(response); + }); + }); + } + + protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { + response.body = { + threads: [ + new Thread(MI2DebugSession.THREAD_ID, "Thread 1") + ] + }; + this.sendResponse(response); + } + + protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + this.gdbDebugger.getStack(args.levels).then(stack => { + let ret: StackFrame[] = []; + stack.forEach(element => { + ret.push(new StackFrame(element.level, element.function + "@" + element.address, new Source(element.fileName, element.file), element.line, 0)); + }); + response.body = { + stackFrames: ret + }; + this.sendResponse(response); + }); + } + + protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + const scopes = new Array(); + scopes.push(new Scope("Local", this.variableHandles.create("@frame:" + args.frameId), false)); + + response.body = { + scopes: scopes + }; + this.sendResponse(response); + } + + protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { + const variables = []; + const id = this.variableHandles.get(args.variablesReference); + + let createVariable = (arg) => { + return this.variableHandles.create(arg); + }; + + if (typeof id == "string") { + if (id.startsWith("@frame:")) { + this.gdbDebugger.getStackVariables(1, 0).then(stack => { + stack.forEach(variable => { + if (variable[1] !== undefined) { + let expanded = expandValue(createVariable, `{${variable[0]} = ${variable[1]}}`); + if (!expanded) + new OutputEvent("Could not expand " + variable[1] + "\n", "stderr"); + else if (typeof expanded[0] == "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + variables.push(expanded[0]); + } else + variables.push({ + name: variable[0], + value: "", + variablesReference: createVariable(variable[0]) + }); + }); + response.body = { + variables: variables + }; + this.sendResponse(response); + }); + } + else { + // Variable members + this.gdbDebugger.evalExpression(JSON.stringify(id)).then(variable => { + let expanded = expandValue(createVariable, variable.result("value")); + if (!expanded) + this.sendEvent(new OutputEvent("Could not expand " + variable.result("value") + "\n", "stderr")); + else if (typeof expanded[0] == "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + response.body = { + variables: expanded + }; + this.sendResponse(response); + }); + } + } + else if (typeof id == "object") { + response.body = { + variables: id + }; + this.sendResponse(response); + } + else { + response.body = { + variables: variables + }; + this.sendResponse(response); + } + } + + protected pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + this.gdbDebugger.interrupt().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not pause: ${msg}\n`, 'stderr')); + }); + } + + protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + this.gdbDebugger.continue().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not continue: ${msg}\n`, 'stderr')); + }); + } + + protected stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.gdbDebugger.step().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not step in: ${msg}\n`, 'stderr')); + }); + } + + protected stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.gdbDebugger.stepOut().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not step out: ${msg}\n`, 'stderr')); + }); + } + + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this.gdbDebugger.next().then(done => { + this.sendResponse(response); + }, msg => { + this.sendResponse(response); + this.sendEvent(new OutputEvent(`Could not step over: ${msg}\n`, 'stderr')); + }); + } + + protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { + if (args.context == "watch") + this.gdbDebugger.evalExpression(args.expression).then((res) => { + response.body = { + variablesReference: 0, + result: res.result("value") + } + this.sendResponse(response); + }); + else { + this.gdbDebugger.sendRaw(args.expression); + this.sendResponse(response); + } + } +} + +function prettyStringArray(strings: string[]) { + return strings.join(", "); +} + +DebugSession.run(MI2DebugSession); \ No newline at end of file diff --git a/test/gdb_expansion.test.ts b/test/gdb_expansion.test.ts new file mode 100644 index 0000000..0553933 --- /dev/null +++ b/test/gdb_expansion.test.ts @@ -0,0 +1,237 @@ +import * as assert from 'assert'; +import { expandValue, isExpandable } from '../src/backend/gdb_expansion'; + +suite("GDB Value Expansion", () => { + let variableCreate = (variable) => { return { expanded: variable }; }; + test("Various values", () => { + assert.strictEqual(isExpandable(`false`), 0); + assert.equal(expandValue(variableCreate, `false`), "false"); + assert.strictEqual(isExpandable(`5`), 0); + assert.equal(expandValue(variableCreate, `5`), "5"); + assert.strictEqual(isExpandable(`"hello world!"`), 0); + assert.equal(expandValue(variableCreate, `"hello world!"`), `"hello world!"`); + assert.strictEqual(isExpandable(`0x0`), 0); + assert.equal(expandValue(variableCreate, `0x0`), ""); + assert.strictEqual(isExpandable(`0xabc`), 2); + assert.equal(expandValue(variableCreate, `0x7ffff7ecb480`), "*0x7ffff7ecb480"); + assert.strictEqual(isExpandable(`{a = b, c = d}`), 1); + assert.deepEqual(expandValue(variableCreate, `{a = b, c = d}`), [ + { + name: "a", + value: "b", + variablesReference: 0 + }, { + name: "c", + value: "d", + variablesReference: 0 + }]); + assert.strictEqual(isExpandable(`{{a = b}}`), 1); + assert.deepEqual(expandValue(variableCreate, `{{a = b}}`), [ + [ + { + name: "a", + value: "b", + variablesReference: 0 + } + ] + ]); + assert.deepEqual(expandValue(variableCreate, `{1, 2, 3, 4}`), [1, 2, 3, 4]); + }); + test("Error values", () => { + assert.strictEqual(isExpandable(``), 0); + assert.equal(expandValue(variableCreate, ``), ""); + }); + test("Nested values", () => { + assert.strictEqual(isExpandable(`{a = {b = e}, c = d}`), 1); + assert.deepEqual(expandValue(variableCreate, `{a = {b = e}, c = d}`), [ + { + name: "a", + value: "Object", + variablesReference: { + expanded: [ + { + name: "b", + value: "e", + variablesReference: 0 + } + ] + } + }, { + name: "c", + value: "d", + variablesReference: 0 + }]); + }); + test("Simple node", () => { + assert.strictEqual(isExpandable(`{a = false, b = 5, c = 0x0, d = "foobar"}`), 1); + let variables = expandValue(variableCreate, `{a = false, b = 5, c = 0x0, d = "foobar"}`); + assert.equal(variables.length, 4); + assert.equal(variables[0].name, "a"); + assert.equal(variables[0].value, "false"); + assert.equal(variables[1].name, "b"); + assert.equal(variables[1].value, "5"); + assert.equal(variables[2].name, "c"); + assert.equal(variables[2].value, ""); + assert.equal(variables[3].name, "d"); + assert.equal(variables[3].value, `"foobar"`); + }); + test("Complex node", () => { + let node = `{quit = false, _views = {{view = 0x7ffff7ece1e8, renderer = 0x7ffff7eccc50, world = 0x7ffff7ece480}}, deltaTimer = {_flagStarted = false, _timeStart = {length = 0}, _timeMeasured = {length = 0}}, _start = {callbacks = 0x0}, _stop = {callbacks = 0x0}}`; + assert.strictEqual(isExpandable(node), 1); + let variables = expandValue(variableCreate, node); + assert.deepEqual(variables, [ + { + name: "quit", + value: "false", + variablesReference: 0 + }, + { + name: "_views", + value: "Object", + variablesReference: { + expanded: [ + [ + { + name: "view", + value: "Object@*0x7ffff7ece1e8", + variablesReference: { expanded: "*view" } + }, + { + name: "renderer", + value: "Object@*0x7ffff7eccc50", + variablesReference: { expanded: "*renderer" } + }, + { + name: "world", + value: "Object@*0x7ffff7ece480", + variablesReference: { expanded: "*world" } + } + ] + ] + } + }, + { + name: "deltaTimer", + value: "Object", + variablesReference: { + expanded: [ + { + name: "_flagStarted", + value: "false", + variablesReference: 0 + }, + { + name: "_timeStart", + value: "Object", + variablesReference: { + expanded: [ + { + name: "length", + value: "0", + variablesReference: 0 + } + ] + } + }, + { + name: "_timeMeasured", + value: "Object", + variablesReference: { + expanded: [ + { + name: "length", + value: "0", + variablesReference: 0 + } + ] + } + } + ] + } + }, + { + name: "_start", + value: "Object", + variablesReference: { + expanded: [ + { + name: "callbacks", + value: "", + variablesReference: 0 + } + ] + } + }, + { + name: "_stop", + value: "Object", + variablesReference: { + expanded: [ + { + name: "callbacks", + value: "", + variablesReference: 0 + } + ] + } + } + ]); + }); + test("Simple node with errors", () => { + let node = `{_enableMipMaps = false, _minFilter = , _magFilter = , _wrapX = , _wrapY = , _inMode = 6408, _mode = 6408, _id = 1, _width = 1024, _height = 1024}`; + assert.strictEqual(isExpandable(node), 1); + let variables = expandValue(variableCreate, node); + assert.deepEqual(variables, [ + { + name: "_enableMipMaps", + value: "false", + variablesReference: 0 + }, + { + name: "_minFilter", + value: "", + variablesReference: 0 + }, + { + name: "_magFilter", + value: "", + variablesReference: 0 + }, + { + name: "_wrapX", + value: "", + variablesReference: 0 + }, + { + name: "_wrapY", + value: "", + variablesReference: 0 + }, + { + name: "_inMode", + value: "6408", + variablesReference: 0 + }, + { + name: "_mode", + value: "6408", + variablesReference: 0 + }, + { + name: "_id", + value: "1", + variablesReference: 0 + }, + { + name: "_width", + value: "1024", + variablesReference: 0 + }, + { + name: "_height", + value: "1024", + variablesReference: 0 + } + ]); + }) +}); \ No newline at end of file diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..e3cebd0 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,22 @@ +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +var testRunner = require('vscode/lib/testrunner'); + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner; \ No newline at end of file diff --git a/test/mi_parse.test.ts b/test/mi_parse.test.ts new file mode 100644 index 0000000..dd75df7 --- /dev/null +++ b/test/mi_parse.test.ts @@ -0,0 +1,160 @@ +import * as assert from 'assert'; +import { parseMI, MINode } from '../src/backend/mi_parse'; + +suite("MI Parse", () => { + test("Simple out of band record", () => { + let parsed = parseMI(`4=thread-exited,id="3",group-id="i1"`); + assert.ok(parsed); + assert.equal(parsed.token, 4); + assert.equal(parsed.outOfBandRecord.length, 1); + assert.equal(parsed.outOfBandRecord[0].isStream, false); + assert.equal(parsed.outOfBandRecord[0].asyncClass, "thread-exited"); + assert.equal(parsed.outOfBandRecord[0].output.length, 2); + assert.deepEqual(parsed.outOfBandRecord[0].output[0], ["id", "3"]); + assert.deepEqual(parsed.outOfBandRecord[0].output[1], ["group-id", "i1"]); + assert.equal(parsed.resultRecords, undefined); + }); + test("Console stream output with new line", () => { + let parsed = parseMI(`~"[Thread 0x7fffe993a700 (LWP 11002) exited]\\n"`); + assert.ok(parsed); + assert.equal(parsed.token, undefined); + assert.equal(parsed.outOfBandRecord.length, 1); + assert.equal(parsed.outOfBandRecord[0].isStream, true); + assert.equal(parsed.outOfBandRecord[0].content, "[Thread 0x7fffe993a700 (LWP 11002) exited]\n"); + assert.equal(parsed.resultRecords, undefined); + }); + test("Empty line", () => { + let parsed = parseMI(``); + assert.ok(parsed); + assert.equal(parsed.token, undefined); + assert.equal(parsed.outOfBandRecord.length, 0); + assert.equal(parsed.resultRecords, undefined); + }); + test("'(gdb)' line", () => { + let parsed = parseMI(`(gdb)`); + assert.ok(parsed); + assert.equal(parsed.token, undefined); + assert.equal(parsed.outOfBandRecord.length, 0); + assert.equal(parsed.resultRecords, undefined); + }); + test("Simple result record", () => { + let parsed = parseMI(`1^running`); + assert.ok(parsed); + assert.equal(parsed.token, 1); + assert.equal(parsed.outOfBandRecord.length, 0); + assert.notEqual(parsed.resultRecords, undefined); + assert.equal(parsed.resultRecords.resultClass, "running"); + assert.equal(parsed.resultRecords.results.length, 0); + }); + test("Advanced out of band record (Breakpoint hit)", () => { + let parsed = parseMI(`*stopped,reason="breakpoint-hit",disp="keep",bkptno="1",frame={addr="0x00000000004e807f",func="D main",args=[{name="args",value="..."}],file="source/app.d",fullname="/path/to/source/app.d",line="157"},thread-id="1",stopped-threads="all",core="0"`); + assert.ok(parsed); + assert.equal(parsed.token, undefined); + assert.equal(parsed.outOfBandRecord.length, 1); + assert.equal(parsed.outOfBandRecord[0].isStream, false); + assert.equal(parsed.outOfBandRecord[0].asyncClass, "stopped"); + assert.equal(parsed.outOfBandRecord[0].output.length, 7); + assert.deepEqual(parsed.outOfBandRecord[0].output[0], ["reason", "breakpoint-hit"]); + assert.deepEqual(parsed.outOfBandRecord[0].output[1], ["disp", "keep"]); + assert.deepEqual(parsed.outOfBandRecord[0].output[2], ["bkptno", "1"]); + let frame = [ + ["addr", "0x00000000004e807f"], + ["func", "D main"], + ["args", [[["name", "args"], ["value", "..."]]]], + ["file", "source/app.d"], + ["fullname", "/path/to/source/app.d"], + ["line", "157"] + ]; + assert.deepEqual(parsed.outOfBandRecord[0].output[3], ["frame", frame]); + assert.deepEqual(parsed.outOfBandRecord[0].output[4], ["thread-id", "1"]); + assert.deepEqual(parsed.outOfBandRecord[0].output[5], ["stopped-threads", "all"]); + assert.deepEqual(parsed.outOfBandRecord[0].output[6], ["core", "0"]); + assert.equal(parsed.resultRecords, undefined); + }); + test("Advanced result record", () => { + let parsed = parseMI(`2^done,asm_insns=[src_and_asm_line={line="134",file="source/app.d",fullname="/path/to/source/app.d",line_asm_insn=[{address="0x00000000004e7da4",func-name="_Dmain",offset="0",inst="push %rbp"},{address="0x00000000004e7da5",func-name="_Dmain",offset="1",inst="mov %rsp,%rbp"}]}]`); + assert.ok(parsed); + assert.equal(parsed.token, 2); + assert.equal(parsed.outOfBandRecord.length, 0); + assert.notEqual(parsed.resultRecords, undefined); + assert.equal(parsed.resultRecords.resultClass, "done"); + assert.equal(parsed.resultRecords.results.length, 1); + let asm_insns = [ + "asm_insns", + [ + [ + "src_and_asm_line", + [ + ["line", "134"], + ["file", "source/app.d"], + ["fullname", "/path/to/source/app.d"], + [ + "line_asm_insn", + [ + [ + ["address", "0x00000000004e7da4"], + ["func-name", "_Dmain"], + ["offset", "0"], + ["inst", "push %rbp"] + ], + [ + ["address", "0x00000000004e7da5"], + ["func-name", "_Dmain"], + ["offset", "1"], + ["inst", "mov %rsp,%rbp"] + ] + ] + ] + ] + ] + ] + ]; + assert.deepEqual(parsed.resultRecords.results[0], asm_insns); + assert.equal(parsed.result("asm_insns.src_and_asm_line.line_asm_insn[1].address"), "0x00000000004e7da5"); + }); + test("valueof children", () => { + let obj = [ + [ + "frame", + [ + ["level", "0"], + ["addr", "0x0000000000435f70"], + ["func", "D main"], + ["file", "source/app.d"], + ["fullname", "/path/to/source/app.d"], + ["line", "5"] + ] + ], + [ + "frame", + [ + ["level", "1"], + ["addr", "0x00000000004372d3"], + ["func", "rt.dmain2._d_run_main()"] + ] + ], + [ + "frame", + [ + ["level", "2"], + ["addr", "0x0000000000437229"], + ["func", "rt.dmain2._d_run_main()"] + ] + ] + ]; + + assert.equal(MINode.valueOf(obj[0], "@frame.level"), "0"); + assert.equal(MINode.valueOf(obj[0], "@frame.addr"), "0x0000000000435f70"); + assert.equal(MINode.valueOf(obj[0], "@frame.func"), "D main"); + assert.equal(MINode.valueOf(obj[0], "@frame.file"), "source/app.d"); + assert.equal(MINode.valueOf(obj[0], "@frame.fullname"), "/path/to/source/app.d"); + assert.equal(MINode.valueOf(obj[0], "@frame.line"), "5"); + + assert.equal(MINode.valueOf(obj[1], "@frame.level"), "1"); + assert.equal(MINode.valueOf(obj[1], "@frame.addr"), "0x00000000004372d3"); + assert.equal(MINode.valueOf(obj[1], "@frame.func"), "rt.dmain2._d_run_main()"); + assert.equal(MINode.valueOf(obj[1], "@frame.file"), undefined); + assert.equal(MINode.valueOf(obj[1], "@frame.fullname"), undefined); + assert.equal(MINode.valueOf(obj[1], "@frame.line"), undefined); + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..69905df --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES5", + "outDir": "out", + "noLib": true, + "sourceMap": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/typings/node.d.ts b/typings/node.d.ts new file mode 100644 index 0000000..5ed7730 --- /dev/null +++ b/typings/node.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts new file mode 100644 index 0000000..61430b1 --- /dev/null +++ b/typings/vscode-typings.d.ts @@ -0,0 +1 @@ +///