xref: /aosp_15_r20/external/pigweed/pw_web/webconsole/components/repl/autocomplete.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2022 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15import { CompletionContext } from '@codemirror/autocomplete';
16import { syntaxTree } from '@codemirror/language';
17import { Device } from 'pigweedjs';
18
19const completePropertyAfter = ['PropertyName', '.', '?.'];
20const dontCompleteIn = [
21  'TemplateString',
22  'LineComment',
23  'BlockComment',
24  'VariableDefinition',
25  'PropertyDefinition',
26];
27// eslint-disable-next-line @typescript-eslint/no-var-requires
28const objectPath = require('object-path');
29
30export function completeFromGlobalScope(context: CompletionContext) {
31  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
32
33  if (
34    completePropertyAfter.includes(nodeBefore.name) &&
35    nodeBefore.parent?.name == 'MemberExpression'
36  ) {
37    const object = nodeBefore.parent.getChild('Expression');
38    if (object?.name == 'VariableName') {
39      const from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;
40      const variableName = context.state.sliceDoc(object.from, object.to);
41      // @ts-ignore
42      if (typeof window[variableName] == 'object') {
43        // @ts-ignore
44        return completeProperties(from, window[variableName]);
45      }
46    } else if (object?.name == 'MemberExpression') {
47      const from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;
48      const variableName = context.state.sliceDoc(object.from, object.to);
49      const variable = resolveWindowVariable(variableName);
50      // @ts-ignore
51      if (typeof variable == 'object') {
52        // @ts-ignore
53        return completeProperties(from, variable, variableName);
54      }
55    }
56  } else if (nodeBefore.name == 'VariableName') {
57    return completeProperties(nodeBefore.from, window);
58  } else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {
59    return completeProperties(context.pos, window);
60  }
61  return null;
62}
63
64function completeProperties(
65  from: number,
66  object: object,
67  variableName?: string,
68) {
69  const options = [];
70  for (const name in object) {
71    // @ts-ignore
72    if (object[name] instanceof Function && variableName) {
73      // eslint-disable-next-line no-debugger
74      debugger;
75      options.push({
76        label: name,
77        // @ts-ignore
78        detail: getFunctionDetailText(`${variableName}.${name}`),
79        type: 'function',
80      });
81    } else {
82      options.push({
83        label: name,
84        type: 'variable',
85      });
86    }
87  }
88  return {
89    from,
90    options,
91    validFor: /^[\w$]*$/,
92  };
93}
94
95function resolveWindowVariable(variableName: string) {
96  if (objectPath.has(window, variableName)) {
97    return objectPath.get(window, variableName);
98  }
99}
100
101function getFunctionDetailText(fullExpression: string): string {
102  if (fullExpression.startsWith('device.rpcs.')) {
103    fullExpression = fullExpression.replace('device.rpcs.', '');
104  }
105  const args = ((window as any).device as Device).getMethodArguments(
106    fullExpression,
107  );
108  if (args) {
109    return `(${args.join(', ')})`;
110  }
111  return '';
112}
113