UNPKG

chrome-devtools-frontend

Version:
775 lines (709 loc) • 29.6 kB
// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as ProtocolClient from '../../core/protocol_client/protocol_client.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as DataGrid from '../../ui/components/data_grid/data_grid.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as LitHtml from '../../ui/lit-html/lit-html.js'; import {JSONPromptEditor} from './JSONPromptEditor.js'; import protocolMonitorStyles from './protocolMonitor.css.js'; const UIStrings = { /** *@description Text for one or a group of functions */ method: 'Method', /** * @description Text in Protocol Monitor. Title for a table column which shows in which direction * the particular protocol message was travelling. Values in this column will either be 'sent' or * 'received'. */ type: 'Type', /** * @description Text in Protocol Monitor of the Protocol Monitor tab. Noun relating to a network request. */ request: 'Request', /** *@description Title of a cell content in protocol monitor. A Network response refers to the act of acknowledging a network request. Should not be confused with answer. */ response: 'Response', /** *@description Text for timestamps of items */ timestamp: 'Timestamp', /** *@description Title of a cell content in protocol monitor. It describes the time between sending a request and receiving a response. */ elapsedTime: 'Elapsed time', /** *@description Text in Protocol Monitor of the Protocol Monitor tab */ target: 'Target', /** *@description Text to record a series of actions for analysis */ record: 'Record', /** *@description Text to clear everything */ clearAll: 'Clear all', /** *@description Text to filter result items */ filter: 'Filter', /** *@description Text for the documentation of something */ documentation: 'Documentation', /** *@description Cell text content in Protocol Monitor of the Protocol Monitor tab *@example {30} PH1 */ sMs: '{PH1} ms', /** *@description Text in Protocol Monitor of the Protocol Monitor tab */ noMessageSelected: 'No message selected', /** *@description Text in Protocol Monitor for the save button */ save: 'Save', /** *@description Text in Protocol Monitor to describe the sessions column */ session: 'Session', /** *@description A placeholder for an input in Protocol Monitor. The input accepts commands that are sent to the backend on Enter. CDP stands for Chrome DevTools Protocol. */ sendRawCDPCommand: 'Send a raw `CDP` command', /** * @description A tooltip text for the input in the Protocol Monitor panel. The tooltip describes what format is expected. */ sendRawCDPCommandExplanation: 'Format: `\'Domain.commandName\'` for a command without parameters, or `\'{"command":"Domain.commandName", "parameters": {...}}\'` as a JSON object for a command with parameters. `\'cmd\'`/`\'method\'` and `\'args\'`/`\'params\'`/`\'arguments\'` are also supported as alternative keys for the `JSON` object.', /** * @description A label for a select input that allows selecting a CDP target to send the commands to. */ selectTarget: 'Select a target', /** * @description Tooltip for the the console sidebar toggle in the Console panel. Command to * open/show the sidebar. */ showCDPCommandEditor: 'Show CDP command editor', /** * @description Tooltip for the the console sidebar toggle in the Console panel. Command to * open/show the sidebar. */ hideCDPCommandEditor: 'Hide CDP command editor', /** * @description Screen reader announcement when the sidebar is shown in the Console panel. */ CDPCommandEditorShown: 'CDP command editor shown', /** * @description Screen reader announcement when the sidebar is hidden in the Console panel. */ CDPCommandEditorHidden: 'CDP command editor hidden', }; const str_ = i18n.i18n.registerUIStrings('panels/protocol_monitor/ProtocolMonitor.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const timeRenderer = (value: DataGrid.DataGridUtils.CellValue): LitHtml.TemplateResult => { return LitHtml.html`${i18nString(UIStrings.sMs, {PH1: String(value)})}`; }; export interface Message { id?: number; method: string; error: Object; result: Object; params: Object; sessionId?: string; } export interface LogMessage { id?: number; domain: string; method: string; params: Object; type: 'send'|'recv'; } interface CommandParameter { name: string; type: string; optional: boolean; } export interface ProtocolDomain { readonly domain: string; readonly commandParameters: { [x: string]: CommandParameter[], }; } let protocolMonitorImplInstance: ProtocolMonitorImpl; export class ProtocolMonitorImpl extends UI.Widget.VBox { private started: boolean; private startTime: number; private readonly requestTimeForId: Map<number, number>; private readonly dataGridRowForId: Map<number, DataGrid.DataGridUtils.Row>; private readonly infoWidget: InfoWidget; private readonly dataGridIntegrator: DataGrid.DataGridControllerIntegrator.DataGridControllerIntegrator; private readonly filterParser: TextUtils.TextUtils.FilterParser; private readonly suggestionBuilder: UI.FilterSuggestionBuilder.FilterSuggestionBuilder; private readonly textFilterUI: UI.Toolbar.ToolbarInput; private messages: LogMessage[] = []; private isRecording: boolean = false; #commandAutocompleteSuggestionProvider = new CommandAutocompleteSuggestionProvider(); #editorWidget = new EditorWidget(this.#commandAutocompleteSuggestionProvider); #selectedTargetId?: string; constructor() { super(true); this.started = false; this.startTime = 0; this.dataGridRowForId = new Map(); this.requestTimeForId = new Map(); const topToolbar = new UI.Toolbar.Toolbar('protocol-monitor-toolbar', this.contentElement); this.contentElement.classList.add('protocol-monitor'); const recordButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.record), 'record-start', 'record-stop'); recordButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => { recordButton.setToggled(!recordButton.toggled()); this.setRecording(recordButton.toggled()); }); recordButton.setToggleWithRedColor(true); topToolbar.appendToolbarItem(recordButton); recordButton.setToggled(true); const clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'clear'); clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => { this.messages = []; this.dataGridIntegrator.update({...this.dataGridIntegrator.data(), rows: []}); this.infoWidget.render(null); }); topToolbar.appendToolbarItem(clearButton); const saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.save), 'download'); saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => { void this.saveAsFile(); }); topToolbar.appendToolbarItem(saveButton); const split = new UI.SplitWidget.SplitWidget(true, true, 'protocol-monitor-panel-split', 250); const splitTextAreaEditor = new UI.SplitWidget.SplitWidget(true, false, 'protocol-monitor-panel-split-container', 400); splitTextAreaEditor.show(this.contentElement); this.infoWidget = new InfoWidget(); this.#editorWidget.addEventListener(Events.CommandSent, event => { this.#onCommandSend(JSON.stringify(event.data)); }); const dataGridInitialData: DataGrid.DataGridController.DataGridControllerData = { paddingRowsCount: 100, showScrollbar: true, columns: [ { id: 'type', title: i18nString(UIStrings.type), sortable: true, widthWeighting: 1, visible: true, hideable: true, styles: { 'text-align': 'center', }, }, { id: 'method', title: i18nString(UIStrings.method), sortable: false, widthWeighting: 5, visible: true, hideable: false, }, { id: 'request', title: i18nString(UIStrings.request), sortable: false, widthWeighting: 5, visible: true, hideable: true, }, { id: 'response', title: i18nString(UIStrings.response), sortable: false, widthWeighting: 5, visible: true, hideable: true, }, { id: 'elapsedTime', title: i18nString(UIStrings.elapsedTime), sortable: true, widthWeighting: 2, visible: true, hideable: true, }, { id: 'timestamp', title: i18nString(UIStrings.timestamp), sortable: true, widthWeighting: 5, visible: false, hideable: true, }, { id: 'target', title: i18nString(UIStrings.target), sortable: true, widthWeighting: 5, visible: false, hideable: true, }, { id: 'session', title: i18nString(UIStrings.session), sortable: true, widthWeighting: 5, visible: false, hideable: true, }, ], rows: [], contextMenus: { bodyRow: (menu: UI.ContextMenu.ContextMenu, columns: readonly DataGrid.DataGridUtils.Column[], row: Readonly<DataGrid.DataGridUtils.Row>): void => { const methodColumn = DataGrid.DataGridUtils.getRowEntryForColumnId(row, 'method'); const typeColumn = DataGrid.DataGridUtils.getRowEntryForColumnId(row, 'type'); /** * You can click the "Filter" item in the context menu to filter the * protocol monitor entries to those that match the method of the * current row. */ menu.defaultSection().appendItem(i18nString(UIStrings.filter), () => { const methodColumn = DataGrid.DataGridUtils.getRowEntryForColumnId(row, 'method'); this.textFilterUI.setValue(`method:${methodColumn.value}`, true); }); /** * You can click the "Documentation" item in the context menu to be * taken to the CDP Documentation site entry for the given method. */ menu.defaultSection().appendItem(i18nString(UIStrings.documentation), () => { if (!methodColumn.value) { return; } const [domain, method] = String(methodColumn.value).split('.'); const type = typeColumn.value === 'sent' ? 'method' : 'event'; Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab( `https://chromedevtools.github.io/devtools-protocol/tot/${domain}#${type}-${method}` as Platform.DevToolsPath.UrlString); }); }, }, }; this.dataGridIntegrator = new DataGrid.DataGridControllerIntegrator.DataGridControllerIntegrator(dataGridInitialData); this.dataGridIntegrator.dataGrid.addEventListener('cellfocused', event => { const focusedRow = event.data.row; const infoWidgetData = { request: DataGrid.DataGridUtils.getRowEntryForColumnId(focusedRow, 'request'), response: DataGrid.DataGridUtils.getRowEntryForColumnId(focusedRow, 'response'), type: DataGrid.DataGridUtils.getRowEntryForColumnId(focusedRow, 'type').title as 'sent' | 'received' | undefined, }; this.infoWidget.render(infoWidgetData); }); this.dataGridIntegrator.dataGrid.addEventListener('newuserfiltertext', event => { this.textFilterUI.setValue(event.data.filterText, /* notify listeners */ true); }); split.setMainWidget(this.dataGridIntegrator); split.setSidebarWidget(this.infoWidget); splitTextAreaEditor.setMainWidget(split); splitTextAreaEditor.setSidebarWidget(this.#editorWidget); splitTextAreaEditor.hideSidebar(); splitTextAreaEditor.enableShowModeSaving(); const keys = ['method', 'request', 'response', 'type', 'target', 'session']; this.filterParser = new TextUtils.TextUtils.FilterParser(keys); this.suggestionBuilder = new UI.FilterSuggestionBuilder.FilterSuggestionBuilder(keys); this.textFilterUI = new UI.Toolbar.ToolbarInput( i18nString(UIStrings.filter), '', 1, .2, '', this.suggestionBuilder.completions.bind(this.suggestionBuilder), true); this.textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, event => { const query = event.data as string; const filters = this.filterParser.parse(query); this.dataGridIntegrator.update({...this.dataGridIntegrator.data(), filters}); }); topToolbar.appendToolbarItem(this.textFilterUI); const bottomToolbar = new UI.Toolbar.Toolbar('protocol-monitor-bottom-toolbar', this.contentElement); bottomToolbar.appendToolbarItem(splitTextAreaEditor.createShowHideSidebarButton( i18nString(UIStrings.showCDPCommandEditor), i18nString(UIStrings.hideCDPCommandEditor), i18nString(UIStrings.CDPCommandEditorShown), i18nString(UIStrings.CDPCommandEditorHidden))); bottomToolbar.appendToolbarItem(this.#createCommandInput()); bottomToolbar.appendToolbarItem(this.#createTargetSelector()); } #createCommandInput(): UI.Toolbar.ToolbarInput { const placeholder = i18nString(UIStrings.sendRawCDPCommand); const accessiblePlaceholder = placeholder; const growFactor = 1; const shrinkFactor = 0.2; const tooltip = i18nString(UIStrings.sendRawCDPCommandExplanation); const input = new UI.Toolbar.ToolbarInput( placeholder, accessiblePlaceholder, growFactor, shrinkFactor, tooltip, this.#commandAutocompleteSuggestionProvider.buildTextPromptCompletions, false); input.addEventListener(UI.Toolbar.ToolbarInput.Event.EnterPressed, () => this.#onCommandSend(input.value())); input.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, () => this.#onCommandChange(input)); return input; } #createTargetSelector(): UI.Toolbar.ToolbarComboBox { const selector = new UI.Toolbar.ToolbarComboBox(() => { this.#selectedTargetId = selector.selectedOption()?.value; }, i18nString(UIStrings.selectTarget)); selector.setMaxWidth(120); const targetManager = SDK.TargetManager.TargetManager.instance(); const syncTargets = (): void => { selector.removeOptions(); for (const target of targetManager.targets()) { selector.createOption(`${target.name()} (${target.inspectedURL()})`, target.id()); } }; targetManager.addEventListener(SDK.TargetManager.Events.AvailableTargetsChanged, syncTargets); syncTargets(); return selector; } #onCommandSend(input: string): void { const {command, parameters} = parseCommandInput(input); const test = ProtocolClient.InspectorBackend.test; const targetManager = SDK.TargetManager.TargetManager.instance(); const selectedTarget = this.#selectedTargetId ? targetManager.targetById(this.#selectedTargetId) : null; const sessionId = selectedTarget ? selectedTarget.sessionId : ''; // TS thinks that properties are read-only because // in TS test is defined as a namespace. // @ts-ignore test.sendRawMessage(command, parameters, () => {}, sessionId); this.#commandAutocompleteSuggestionProvider.addEntry(input); } #onCommandChange(input: UI.Toolbar.ToolbarInput): void { const value = input.valueWithoutSuggestion(); const {command, parameters} = parseCommandInput(value); this.#editorWidget.setCommand(command, parameters); } static instance(opts: {forceNew: null|boolean} = {forceNew: null}): ProtocolMonitorImpl { const {forceNew} = opts; if (!protocolMonitorImplInstance || forceNew) { protocolMonitorImplInstance = new ProtocolMonitorImpl(); } return protocolMonitorImplInstance; } override wasShown(): void { if (this.started) { return; } this.registerCSSFiles([protocolMonitorStyles]); this.started = true; this.startTime = Date.now(); this.setRecording(true); } private setRecording(recording: boolean): void { this.isRecording = recording; const test = ProtocolClient.InspectorBackend.test; if (recording) { // TODO: TS thinks that properties are read-only because // in TS test is defined as a namespace. // @ts-ignore test.onMessageSent = this.messageSent.bind(this); // @ts-ignore test.onMessageReceived = this.messageReceived.bind(this); } else { // @ts-ignore test.onMessageSent = null; // @ts-ignore test.onMessageReceived = null; } } private targetToString(target: SDK.Target.Target|null): string { if (!target) { return ''; } return target.decorateLabel( `${target.name()} ${target === SDK.TargetManager.TargetManager.instance().rootTarget() ? '' : target.id()}`); } // eslint-disable private messageReceived(message: Message, target: ProtocolClient.InspectorBackend.TargetBase|null): void { if (this.isRecording) { this.messages.push({...message, type: 'recv', domain: '-'}); } if ('id' in message && message.id) { const existingRow = this.dataGridRowForId.get(message.id); if (!existingRow) { return; } const allExistingRows = this.dataGridIntegrator.data().rows; const matchingExistingRowIndex = allExistingRows.findIndex(r => existingRow === r); const newRowWithUpdate = { ...existingRow, cells: existingRow.cells.map(cell => { if (cell.columnId === 'response') { return { ...cell, value: JSON.stringify(message.result || message.error), }; } if (cell.columnId === 'elapsedTime') { const requestTime = this.requestTimeForId.get(message.id as number); if (requestTime) { return { ...cell, value: Date.now() - requestTime, renderer: timeRenderer, }; } } return cell; }), }; const newRowsArray = [...this.dataGridIntegrator.data().rows]; newRowsArray[matchingExistingRowIndex] = newRowWithUpdate; // Now we've updated the message, it won't be updated again, so we can delete it from the tracking map. this.dataGridRowForId.delete(message.id); this.dataGridIntegrator.update({ ...this.dataGridIntegrator.data(), rows: newRowsArray, }); return; } const sdkTarget = target as SDK.Target.Target | null; const responseIcon = new IconButton.Icon.Icon(); responseIcon.data = {iconName: 'arrow-down', color: 'var(--icon-request)', width: '20px', height: '20px'}; const newRow: DataGrid.DataGridUtils.Row = { cells: [ {columnId: 'method', value: message.method, title: message.method}, {columnId: 'request', value: '', renderer: DataGrid.DataGridRenderers.codeBlockRenderer}, { columnId: 'response', value: JSON.stringify(message.params), renderer: DataGrid.DataGridRenderers.codeBlockRenderer, }, { columnId: 'timestamp', value: Date.now() - this.startTime, renderer: timeRenderer, }, {columnId: 'elapsedTime', value: ''}, {columnId: 'type', value: responseIcon, title: 'received'}, {columnId: 'target', value: this.targetToString(sdkTarget)}, {columnId: 'session', value: message.sessionId || ''}, ], hidden: false, }; this.dataGridIntegrator.update({ ...this.dataGridIntegrator.data(), rows: this.dataGridIntegrator.data().rows.concat([newRow]), }); } private messageSent( message: {domain: string, method: string, params: Object, id: number, sessionId?: string}, target: ProtocolClient.InspectorBackend.TargetBase|null): void { if (this.isRecording) { this.messages.push({...message, type: 'send'}); } const sdkTarget = target as SDK.Target.Target | null; const requestResponseIcon = new IconButton.Icon.Icon(); requestResponseIcon .data = {iconName: 'arrow-up-down', color: 'var(--icon-request-response)', width: '20px', height: '20px'}; const newRow: DataGrid.DataGridUtils.Row = { styles: { '--override-data-grid-row-background-color': 'var(--override-data-grid-sent-message-row-background-color)', }, cells: [ {columnId: 'method', value: message.method, title: message.method}, { columnId: 'request', value: JSON.stringify(message.params), renderer: DataGrid.DataGridRenderers.codeBlockRenderer, }, {columnId: 'response', value: '(pending)', renderer: DataGrid.DataGridRenderers.codeBlockRenderer}, { columnId: 'timestamp', value: Date.now() - this.startTime, renderer: timeRenderer, }, {columnId: 'elapsedTime', value: '(pending)'}, {columnId: 'type', value: requestResponseIcon, title: 'sent'}, {columnId: 'target', value: this.targetToString(sdkTarget)}, {columnId: 'session', value: message.sessionId || ''}, ], hidden: false, }; this.requestTimeForId.set(message.id, Date.now()); this.dataGridRowForId.set(message.id, newRow); this.dataGridIntegrator.update({ ...this.dataGridIntegrator.data(), rows: this.dataGridIntegrator.data().rows.concat([newRow]), }); } private async saveAsFile(): Promise<void> { const now = new Date(); const fileName = 'ProtocolMonitor-' + Platform.DateUtilities.toISO8601Compact(now) + '.json' as Platform.DevToolsPath.RawPathString; const stream = new Bindings.FileUtils.FileOutputStream(); const accepted = await stream.open(fileName); if (!accepted) { return; } void stream.write(JSON.stringify(this.messages, null, ' ')); void stream.close(); } } export class CommandAutocompleteSuggestionProvider { #maxHistorySize = 200; #commandHistory = new Set<string>(); #protocolMethods = this.buildProtocolCommands(ProtocolClient.InspectorBackend.inspectorBackend.agentPrototypes.values()); constructor(maxHistorySize?: number) { if (maxHistorySize !== undefined) { this.#maxHistorySize = maxHistorySize; } } buildTextPromptCompletions = async(expression: string, prefix: string, force?: boolean): Promise<UI.SuggestBox.Suggestions> => { if (!prefix && !force && expression) { return []; } const newestToOldest = [...this.#commandHistory].reverse(); newestToOldest.push(...this.#protocolMethods); return newestToOldest.filter(cmd => cmd.startsWith(prefix)).map(text => ({ text, })); }; buildProtocolCommands(domains: Iterable<ProtocolDomain>): Set<string> { const commands: Set<string> = new Set(); for (const domain of domains) { for (const command of Object.keys(domain.commandParameters)) { commands.add(command); } } return commands; } addEntry(value: string): void { if (this.#commandHistory.has(value)) { this.#commandHistory.delete(value); } this.#commandHistory.add(value); if (this.#commandHistory.size > this.#maxHistorySize) { const earliestEntry = this.#commandHistory.values().next().value; this.#commandHistory.delete(earliestEntry); } } } export class InfoWidget extends UI.Widget.VBox { private readonly tabbedPane: UI.TabbedPane.TabbedPane; constructor() { super(); this.tabbedPane = new UI.TabbedPane.TabbedPane(); this.tabbedPane.appendTab('request', i18nString(UIStrings.request), new UI.Widget.Widget()); this.tabbedPane.appendTab('response', i18nString(UIStrings.response), new UI.Widget.Widget()); this.tabbedPane.show(this.contentElement); this.tabbedPane.selectTab('response'); this.render(null); } render(data: { request: DataGrid.DataGridUtils.Cell|undefined, response: DataGrid.DataGridUtils.Cell|undefined, type: 'sent'|'received'|undefined, }|null): void { if (!data || !data.request || !data.response) { this.tabbedPane.changeTabView('request', new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMessageSelected))); this.tabbedPane.changeTabView( 'response', new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMessageSelected))); return; } const requestEnabled = data && data.type && data.type === 'sent'; this.tabbedPane.setTabEnabled('request', Boolean(requestEnabled)); if (!requestEnabled) { this.tabbedPane.selectTab('response'); } const requestParsed = JSON.parse(String(data.request.value) || 'null'); this.tabbedPane.changeTabView('request', SourceFrame.JSONView.JSONView.createViewSync(requestParsed)); const responseParsed = data.response.value === '(pending)' ? null : JSON.parse(String(data.response.value) || 'null'); this.tabbedPane.changeTabView('response', SourceFrame.JSONView.JSONView.createViewSync(responseParsed)); } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { CommandSent = 'CommandSent', } export type EventTypes = { [Events.CommandSent]: Command, }; export interface Command { command: string; parameters: object; } export class EditorWidget extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) { private readonly promptContainer: HTMLElement; private readonly promptElement: HTMLElement; readonly promptList: HTMLElement; private readonly promptInner: HTMLElement; #commandAutocompleteSuggestionProvider: CommandAutocompleteSuggestionProvider; private jsonPromptEditors: JSONPromptEditor[] = []; private commandPromptEditor: JSONPromptEditor; constructor(commandAutocompleteSuggestionProvider: CommandAutocompleteSuggestionProvider) { super(); // TODO: fix ad hoc section property in a separate CL to be safe this.promptContainer = this.element.createChild('div', 'cdp-command-prompt-container'); this.promptElement = this.promptContainer.createChild('div'); this.promptList = this.promptElement.createChild('ul'); this.promptList.style.paddingLeft = '0px'; this.promptInner = this.promptList.createChild('div'); this.promptContainer.addEventListener('keydown', (event: Event) => { if ((event as KeyboardEvent).key === 'Enter') { this.dispatchEventToListeners(Events.CommandSent, this.getCommand()); } }); this.#commandAutocompleteSuggestionProvider = commandAutocompleteSuggestionProvider; this.commandPromptEditor = new JSONPromptEditor('command', '', this.#commandAutocompleteSuggestionProvider); const output = this.commandPromptEditor.render(); LitHtml.render(output, this.promptInner, {host: this}); } getCommand(): Command { return { command: this.commandPromptEditor.getText(), parameters: this.jsonPromptEditors.reduce<{[key: string]: string}>( (parameters, editor) => { parameters[editor.getKey()] = editor.getText(); return parameters; }, {}), }; } setCommand(command: string, parameters: { [x: string]: unknown, }): void { this.commandPromptEditor.setText(command); this.jsonPromptEditors = []; if (parameters) { for (const key of Object.keys(parameters)) { const value = JSON.stringify(parameters[key]); const jsonPromptEditor = new JSONPromptEditor(key, value, this.#commandAutocompleteSuggestionProvider); this.jsonPromptEditors.push(jsonPromptEditor); } const output = this.jsonPromptEditors.map(editor => editor.render()); LitHtml.render(output, this.promptList, {host: this}); } } } export function parseCommandInput(input: string): {command: string, parameters: {}} { // If input cannot be parsed as json, we assume it's the command name // for a command without parameters. Otherwise, we expect an object // with "command"/"method"/"cmd" and "parameters"/"params"/"args"/"arguments" attributes. let json = null; try { json = JSON.parse(input); } catch (err) { } const command = json ? json.command || json.method || json.cmd : input; const parameters = json ? json.parameters || json.params || json.args || json.arguments : {}; return {command, parameters}; }