From 20fd5528207d0028ecb6227bf153e7001c6b91bc Mon Sep 17 00:00:00 2001 From: Asad Memon Date: Wed, 7 Sep 2022 21:06:51 +0000 Subject: [PATCH] pw_web: Better autocomplete and method arguments for RPC methods Change-Id: I4b96029685631f942386fcb177bbfa90fe2ae0ec Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/108812 Commit-Queue: Asad Memon Reviewed-by: Anthony DiGirolamo --- .../components/repl/autocomplete.ts | 53 ++++++++++++++++--- pw_web/webconsole/package.json | 1 + ts/device/index.ts | 10 ++++ ts/device/index_test.ts | 5 ++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/pw_web/webconsole/components/repl/autocomplete.ts b/pw_web/webconsole/components/repl/autocomplete.ts index 8490905f4..24d7efb3a 100644 --- a/pw_web/webconsole/components/repl/autocomplete.ts +++ b/pw_web/webconsole/components/repl/autocomplete.ts @@ -14,6 +14,7 @@ import {CompletionContext} from '@codemirror/autocomplete' import {syntaxTree} from '@codemirror/language' +import {Device} from "pigweedjs"; const completePropertyAfter = ['PropertyName', '.', '?.'] const dontCompleteIn = [ @@ -23,6 +24,7 @@ const dontCompleteIn = [ 'VariableDefinition', 'PropertyDefinition' ] +var objectPath = require("object-path"); export function completeFromGlobalScope(context: CompletionContext) { let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1) @@ -41,6 +43,16 @@ export function completeFromGlobalScope(context: CompletionContext) { return completeProperties(from, window[variableName]) } } + else if (object?.name == 'MemberExpression') { + let from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from + let variableName = context.state.sliceDoc(object.from, object.to) + let variable = resolveWindowVariable(variableName); + // @ts-ignore + if (typeof variable == 'object') { + // @ts-ignore + return completeProperties(from, variable, variableName) + } + } } else if (nodeBefore.name == 'VariableName') { return completeProperties(nodeBefore.from, window) } else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) { @@ -49,14 +61,26 @@ export function completeFromGlobalScope(context: CompletionContext) { return null } -function completeProperties(from: number, object: Object) { +function completeProperties(from: number, object: Object, variableName?: string) { let options = [] for (let name in object) { - options.push({ - label: name, - // @ts-ignore - type: typeof object[name] == 'function' ? 'function' : 'variable' - }) + // @ts-ignore + if (object[name] instanceof Function && variableName) { + debugger; + options.push({ + label: name, + // @ts-ignore + detail: getFunctionDetailText(`${variableName}.${name}`), + type: 'function' + }) + } + else { + options.push({ + label: name, + type: 'variable' + }) + } + } return { from, @@ -64,3 +88,20 @@ function completeProperties(from: number, object: Object) { validFor: /^[\w$]*$/ } } + +function resolveWindowVariable(variableName: string) { + if (objectPath.has(window, variableName)) { + return objectPath.get(window, variableName); + } +} + +function getFunctionDetailText(fullExpression: string): string { + if (fullExpression.startsWith("device.rpcs.")) { + fullExpression = fullExpression.replace("device.rpcs.", ""); + } + const args = ((window as any).device as Device).getMethodArguments(fullExpression); + if (args) { + return `(${args.join(", ")})`; + } + return ""; +} diff --git a/pw_web/webconsole/package.json b/pw_web/webconsole/package.json index 77cd83641..eb5da8f21 100644 --- a/pw_web/webconsole/package.json +++ b/pw_web/webconsole/package.json @@ -18,6 +18,7 @@ "@mui/material": "^5.9.3", "codemirror": "^6.0.1", "next": "12.2.3", + "object-path": "^0.11.8", "pigweedjs": "file:../../", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/ts/device/index.ts b/ts/device/index.ts index f5c357572..cb68c7f71 100644 --- a/ts/device/index.ts +++ b/ts/device/index.ts @@ -28,6 +28,7 @@ export class Device { private decoder: Decoder; private encoder: Encoder; private rpcAddress: number; + private nameToMethodArgumentsMap: any; client: Client; rpcs: any @@ -40,6 +41,7 @@ export class Device { this.protoCollection = protoCollection; this.decoder = new Decoder(); this.encoder = new Encoder(); + this.nameToMethodArgumentsMap = {}; const channels = [ new Channel(1, (bytes) => { const hdlcBytes = this.encoder.uiFrame(this.rpcAddress, bytes); @@ -63,6 +65,10 @@ export class Device { }); } + getMethodArguments(fullPath) { + return this.nameToMethodArgumentsMap[fullPath]; + } + private setupRpcs() { let rpcMap = {}; let channel = this.client.channel(); @@ -101,6 +107,10 @@ export class Device { 'return this(arguments);' ); + // We store field names so REPL can show hints in autocomplete using these. + this.nameToMethodArgumentsMap[fullMethodPath] = requestFields + .map(field => field.getName()); + // We create a new JS function dynamically here that takes // proto message fields as arguments and calls the actual RPC method. let fn = new Function(...functionArguments).bind((args) => { diff --git a/ts/device/index_test.ts b/ts/device/index_test.ts index 6b31cc450..1218dbbc9 100644 --- a/ts/device/index_test.ts +++ b/ts/device/index_test.ts @@ -32,6 +32,11 @@ describe('WebSerialTransport', () => { expect(device.rpcs.pw.rpc.EchoService.Echo).toBeDefined(); }); + it('has method arguments data', () => { + expect(device.getMethodArguments("pw.rpc.EchoService.Echo")).toStrictEqual(["msg"]); + expect(device.getMethodArguments("pw.test2.Alpha.Unary")).toStrictEqual(['magic_number']); + }); + it('unary rpc sends request to serial', async () => { const helloResponse = new Uint8Array([ 126, 165, 3, 42, 7, 10, 5, 104,