| // Copyright 2023 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. |
| |
| /* eslint-disable rulesdir/no-underscored-properties, @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars */ |
| |
| import {html, nothing, render} from 'lit-html'; |
| |
| import { |
| DEFAULT_MODULE_CONFIGURATIONS, |
| type ModuleConfiguration, |
| type ModuleConfigurations, |
| type PathSubstitution, |
| } from './ModuleConfiguration.js'; |
| |
| export interface ModuleConfigurationListData { |
| moduleConfigurations: ModuleConfigurations; |
| } |
| |
| export class ModuleConfigurationsChangedEvent extends CustomEvent<ModuleConfigurationListData> { |
| constructor(detail: ModuleConfigurationListData) { |
| super('module-configurations-changed', {detail}); |
| } |
| } |
| |
| export class ModuleConfigurationList extends HTMLElement { |
| private readonly _shadow = this.attachShadow({mode: 'open'}); |
| private _moduleConfigurations: ModuleConfigurations = DEFAULT_MODULE_CONFIGURATIONS; |
| |
| constructor() { |
| super(); |
| this._render(); |
| } |
| |
| get data(): ModuleConfigurationListData { |
| return {moduleConfigurations: this._moduleConfigurations}; |
| } |
| |
| set data({moduleConfigurations}: ModuleConfigurationListData) { |
| this._moduleConfigurations = moduleConfigurations; |
| this._render(); |
| } |
| |
| private _setModuleConfigurations(moduleConfigurations: ModuleConfigurations) { |
| this._moduleConfigurations = moduleConfigurations; |
| this._render(); |
| this.dispatchEvent(new ModuleConfigurationsChangedEvent({moduleConfigurations})); |
| } |
| |
| private _addModuleConfiguration(event: Event) { |
| this._setModuleConfigurations(this._moduleConfigurations.concat([{name: '', pathSubstitutions: []}])); |
| const {target} = event; |
| if (target instanceof HTMLButtonElement) { |
| const moduleInputs = target.parentElement?.querySelectorAll('input'); |
| const element = moduleInputs ? moduleInputs[moduleInputs.length - 1] : undefined; |
| if (element instanceof HTMLInputElement) { |
| element.focus(); |
| } |
| } |
| } |
| |
| private _removeModuleConfiguration(moduleConfiguration: ModuleConfiguration, event: Event) { |
| this._setModuleConfigurations(this._moduleConfigurations.filter(m => m !== moduleConfiguration)); |
| } |
| |
| private _updateModuleConfiguration(mc: ModuleConfiguration, fn: (mc: ModuleConfiguration) => ModuleConfiguration) { |
| this._setModuleConfigurations(this._moduleConfigurations.map( |
| moduleConfiguration => moduleConfiguration === mc ? fn(moduleConfiguration) : moduleConfiguration)); |
| } |
| |
| private _updateModuleConfigurationName(mc: ModuleConfiguration, event: Event) { |
| const name = (event.target as HTMLInputElement).value; |
| this._updateModuleConfiguration(mc, mc => ({...mc, name})); |
| } |
| |
| private _addPathSubstitution(mc: ModuleConfiguration, event: Event) { |
| this._updateModuleConfiguration( |
| mc, mc => ({...mc, pathSubstitutions: mc.pathSubstitutions.concat([{from: '', to: ''}])})); |
| const {target} = event; |
| if (target instanceof HTMLButtonElement) { |
| const prev = target.parentElement?.previousElementSibling?.firstElementChild; |
| if (prev instanceof HTMLInputElement) { |
| prev.focus(); |
| } |
| } |
| } |
| |
| private _removePathSubstitution(mc: ModuleConfiguration, sm: PathSubstitution, event: Event) { |
| this._updateModuleConfiguration(mc, mc => ({...mc, pathSubstitutions: mc.pathSubstitutions.filter(s => s !== sm)})); |
| } |
| |
| private _updatePathSubstitutionFrom(mc: ModuleConfiguration, sm: PathSubstitution, event: Event) { |
| const from = (event.target as HTMLInputElement).value; |
| this._updateModuleConfiguration( |
| mc, mc => ({...mc, pathSubstitutions: mc.pathSubstitutions.map(s => s === sm ? {...sm, from} : s)})); |
| } |
| |
| private _updatePathSubstitutionTo(mc: ModuleConfiguration, sm: PathSubstitution, event: Event) { |
| const to = (event.target as HTMLInputElement).value; |
| this._updateModuleConfiguration( |
| mc, mc => ({...mc, pathSubstitutions: mc.pathSubstitutions.map(s => s === sm ? {...sm, to} : s)})); |
| } |
| |
| private _render() { |
| const output = html` |
| <style> |
| .mc-list { |
| display: grid; |
| grid-template-columns: 2fr 6fr auto; |
| gap: 10px; |
| padding: 10px; |
| border-radius: 4px; |
| border: 1px solid lightgrey; |
| } |
| .mc-list label { |
| font-weight: 600; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .mc-separator { |
| margin: 0px; |
| grid-column-end: span 3; |
| border-bottom: 1px solid lightgrey; |
| } |
| .sm { |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .sm-item { |
| display: flex; |
| flex-direction: row; |
| gap: 10px; |
| } |
| .icon-button { |
| border: none; |
| border-radius: 0; |
| cursor: pointer; |
| display: inline-block; |
| mask-size: contain; |
| width: 20px; |
| height: 20px; |
| background-color: rgb(110 110 110); |
| color: #5a5a5a; |
| } |
| .icon-button:hover { |
| background-color: #333; |
| } |
| .icon-button:focus { |
| background-color: #333; |
| } |
| .mc-delete { |
| mask-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' height='24px' viewBox='0 0 24 24' width='24px' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M15 4V3H9v1H4v2h1v13c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V6h1V4h-5zm2 15H7V6h10v13zM9 8h2v9H9zm4 0h2v9h-2z'/%3E%3C/svg%3E"); |
| width: 32px; |
| height: 32px; |
| } |
| .sm-delete { |
| mask-image: url('data:image/svg+xml,%0A%3Csvg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="%23000000"%3E%3Cpath d="M0 0h24v24H0V0z" fill="none"/%3E%3Cpath d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/%3E%3C/svg%3E'); |
| } |
| </style> |
| <p> |
| The debug information encoded in the <code>.wasm</code> binaries references source files via |
| paths that usually only make sense on the build machine, but you might need to debug them on |
| different machine (i.e. build was created by CI bot). If that's the case, you can specify |
| path substitutions below that replace certain parts of the paths and tell Chrome DevTools to |
| look somewhere else for the source file in question. This is the equivalent to GDB's |
| <a href="https://sourceware.org/gdb/current/onlinedocs/gdb/Source-Path.html#set-substitute_002dpath"><code>set substitute-path</code></a> |
| and LLDB's <code>set target.source-map <i>old</i> <i>new</i></code> feature, which you might |
| already be familiar with. |
| </p> |
| <p> |
| Below you can specify path substitutions on a per <code>.wasm</code> module level (or just |
| specify default settings that will be used when no module matches the name). The following |
| wildcard patterns are supported for the module name: |
| <ul> |
| <li>A <code>*</code> in the pattern matches any sequence of characters, except for slashes (<code>/</code>).</li> |
| <li>A <code>**/</code> in the pattern matches any sequence of characters, including slashes (<code>/</code>).</li> |
| </ul> |
| If the pattern contains a slash (<code>/</code>) the full URL will be tested against the pattern, |
| while if the pattern doesn't contain any slashes, only the basename of the URL's path will be tested. |
| For example, say you specify <code>foo*.wasm</code> as pattern, then it will successfully match |
| <code>http://localhost/foo1.wasm</code>, but not <code>http://foo.wasm/file.wasm</code>. |
| </p> |
| <div class=mc-list> |
| ${ |
| this._moduleConfigurations.map( |
| mc => html` |
| <label> |
| ${ |
| mc.name !== undefined ? html`Module |
| <input |
| placeholder="filename.wasm" |
| .value=${mc.name} |
| @input=${this._updateModuleConfigurationName.bind(this, mc)}> |
| ` : |
| html`Default settings`} |
| </label> |
| <div class=sm> |
| <label>Path substitutions</label> |
| ${ |
| mc.pathSubstitutions.map( |
| sm => html` |
| <div class=sm-item> |
| <input |
| placeholder="/old/path" |
| value=${sm.from} |
| @input=${this._updatePathSubstitutionFrom.bind(this, mc, sm)}> |
| <input |
| placeholder="/new/path" |
| value=${sm.to} |
| @input=${this._updatePathSubstitutionTo.bind(this, mc, sm)}> |
| <button class="icon-button sm-delete" @click=${ |
| this._removePathSubstitution.bind(this, mc, sm)} title="Remove path substitution"></button> |
| </div> |
| `)} |
| <div><button @click=${this._addPathSubstitution.bind(this, mc)}>Add path substitution</button></div> |
| </div> |
| <div>${ |
| mc.name === undefined ? |
| nothing : |
| html`<button class="icon-button mc-delete" @click=${ |
| this._removeModuleConfiguration.bind(this, mc)} title="Remove module"></button>`}</div> |
| <div class=mc-separator></div> |
| `)} |
| <button style="margin: 10px" @click=${this._addModuleConfiguration}>Add module settings</button> |
| </div> |
| `; |
| render(output, this._shadow, {eventContext: this}); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'devtools-cxx-debugging-module-configuration-list': ModuleConfigurationList; |
| } |
| } |
| |
| customElements.define('devtools-cxx-debugging-module-configuration-list', ModuleConfigurationList); |