chrome-devtools-frontend
Version:
Chrome DevTools UI
159 lines (136 loc) • 6.18 kB
text/typescript
// Copyright 2014 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 type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as FormatterModule from '../../models/formatter/formatter.js';
import * as Persistence from '../../models/persistence/persistence.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import type {EditorAction, EditorClosedEvent, SourcesView} from './SourcesView.js';
import {Events, registerEditorAction} from './SourcesView.js';
const UIStrings = {
/**
*@description Title of the pretty print button in the Sources panel
*@example {file name} PH1
*/
prettyPrintS: 'Pretty print {PH1}',
/**
*@description Text to pretty print a file
*/
prettyPrint: 'Pretty print',
};
const str_ = i18n.i18n.registerUIStrings('panels/sources/ScriptFormatterEditorAction.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let scriptFormatterEditorActionInstance: ScriptFormatterEditorAction;
export class ScriptFormatterEditorAction implements EditorAction {
private readonly pathsToFormatOnLoad: Set<Platform.DevToolsPath.UrlString>;
private sourcesView!: SourcesView;
private button!: UI.Toolbar.ToolbarButton;
private constructor() {
this.pathsToFormatOnLoad = new Set();
}
static instance(opts: {
forceNew: boolean|null,
} = {forceNew: null}): ScriptFormatterEditorAction {
const {forceNew} = opts;
if (!scriptFormatterEditorActionInstance || forceNew) {
scriptFormatterEditorActionInstance = new ScriptFormatterEditorAction();
}
return scriptFormatterEditorActionInstance;
}
private editorSelected(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
this.updateButton(uiSourceCode);
if (this.isFormattableScript(uiSourceCode) && this.pathsToFormatOnLoad.has(uiSourceCode.url()) &&
!FormatterModule.SourceFormatter.SourceFormatter.instance().hasFormatted(uiSourceCode)) {
void this.showFormatted(uiSourceCode);
}
}
private async editorClosed(event: Common.EventTarget.EventTargetEvent<EditorClosedEvent>): Promise<void> {
const {uiSourceCode, wasSelected} = event.data;
if (wasSelected) {
this.updateButton(null);
}
const original =
await FormatterModule.SourceFormatter.SourceFormatter.instance().discardFormattedUISourceCode(uiSourceCode);
if (original) {
this.pathsToFormatOnLoad.delete(original.url());
}
}
private updateButton(uiSourceCode: Workspace.UISourceCode.UISourceCode|null): void {
const isFormattable = this.isFormattableScript(uiSourceCode);
this.button.element.classList.toggle('hidden', !isFormattable);
if (uiSourceCode) {
// We always update the title of the button, even if the {uiSourceCode} is
// not formattable, since we use the title (the aria-label actually) as a
// signal for the E2E tests that the source code loading is done.
this.button.setTitle(i18nString(UIStrings.prettyPrintS, {PH1: uiSourceCode.name()}));
}
}
getOrCreateButton(sourcesView: SourcesView): UI.Toolbar.ToolbarButton {
if (this.button) {
return this.button;
}
this.sourcesView = sourcesView;
this.sourcesView.addEventListener(Events.EditorSelected, event => {
this.editorSelected(event);
});
this.sourcesView.addEventListener(Events.EditorClosed, event => {
void this.editorClosed(event);
});
this.button = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.prettyPrint), 'largeicon-pretty-print');
this.button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.onFormatScriptButtonClicked, this);
this.updateButton(sourcesView.currentUISourceCode());
return this.button;
}
private isFormattableScript(uiSourceCode: Workspace.UISourceCode.UISourceCode|null): boolean {
if (!uiSourceCode) {
return false;
}
if (uiSourceCode.project().canSetFileContent()) {
return false;
}
if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Formatter) {
return false;
}
if (Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode)) {
return false;
}
if (uiSourceCode.mimeType() === 'application/wasm') {
return false;
}
return uiSourceCode.contentType().hasScripts();
}
isCurrentUISourceCodeFormattable(): boolean {
const uiSourceCode = this.sourcesView.currentUISourceCode();
return this.isFormattableScript(uiSourceCode);
}
private onFormatScriptButtonClicked(): void {
this.toggleFormatScriptSource();
}
toggleFormatScriptSource(): void {
const uiSourceCode = this.sourcesView.currentUISourceCode();
if (!uiSourceCode || !this.isFormattableScript(uiSourceCode)) {
return;
}
this.pathsToFormatOnLoad.add(uiSourceCode.url());
void this.showFormatted(uiSourceCode);
}
private async showFormatted(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
const formatData = await FormatterModule.SourceFormatter.SourceFormatter.instance().format(uiSourceCode);
if (uiSourceCode !== this.sourcesView.currentUISourceCode()) {
return;
}
const sourceFrame = this.sourcesView.viewForFile(uiSourceCode);
let start: number[]|number[] = [0, 0];
if (sourceFrame instanceof SourceFrame.SourceFrame.SourceFrameImpl) {
const selection = sourceFrame.textEditor.toLineColumn(sourceFrame.textEditor.state.selection.main.head);
start = formatData.mapping.originalToFormatted(selection.lineNumber, selection.columnNumber);
}
this.sourcesView.showSourceLocation(formatData.formattedSourceCode, {lineNumber: start[0], columnNumber: start[1]});
}
}
registerEditorAction(ScriptFormatterEditorAction.instance);