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 <asadmemon@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
This commit is contained in:
Asad Memon 2022-09-07 21:06:51 +00:00 committed by CQ Bot Account
parent 84884eb9b0
commit 20fd552820
4 changed files with 63 additions and 6 deletions

View File

@ -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 "";
}

View File

@ -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",

View File

@ -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) => {

View File

@ -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,