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>
diff --git a/pw_web/webconsole/components/repl/autocomplete.ts b/pw_web/webconsole/components/repl/autocomplete.ts
index 8490905..24d7efb 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 @@
'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 @@
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 @@
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 @@
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 77cd836..eb5da8f 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 f5c3575..cb68c7f 100644
--- a/ts/device/index.ts
+++ b/ts/device/index.ts
@@ -28,6 +28,7 @@
private decoder: Decoder;
private encoder: Encoder;
private rpcAddress: number;
+ private nameToMethodArgumentsMap: any;
client: Client;
rpcs: any
@@ -40,6 +41,7 @@
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 @@
});
}
+ getMethodArguments(fullPath) {
+ return this.nameToMethodArgumentsMap[fullPath];
+ }
+
private setupRpcs() {
let rpcMap = {};
let channel = this.client.channel();
@@ -101,6 +107,10 @@
'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 6b31cc4..1218dbb 100644
--- a/ts/device/index_test.ts
+++ b/ts/device/index_test.ts
@@ -32,6 +32,11 @@
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,