Add toolbar to YAML/template editors (#26580)
parent
88ac56ac0b
commit
7f88d863e9
|
@ -5,9 +5,16 @@ import type {
|
|||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { undo, undoDepth, redo, redoDepth } from "@codemirror/commands";
|
||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||
import {
|
||||
mdiArrowExpand,
|
||||
mdiArrowCollapse,
|
||||
mdiContentCopy,
|
||||
mdiUndo,
|
||||
mdiRedo,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement, html, render } from "lit";
|
||||
|
@ -16,11 +23,14 @@ import memoizeOne from "memoize-one";
|
|||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { getEntityContext } from "../common/entity/context/get_entity_context";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-code-editor-completion-items";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-code-editor-completion-items";
|
||||
import "./ha-icon-button-toolbar";
|
||||
import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
|
@ -68,13 +78,24 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "has-toolbar" })
|
||||
public hasToolbar = true;
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
@state() private _isFullscreen = false;
|
||||
|
||||
@state() private _canUndo = false;
|
||||
|
||||
@state() private _canRedo = false;
|
||||
|
||||
@state() private _canCopy = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
private _editorToolbar?: HaIconButtonToolbar;
|
||||
|
||||
private _iconList?: Completion[];
|
||||
|
||||
public set value(value: string) {
|
||||
|
@ -119,9 +140,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
if (this._isFullscreen) {
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
this._updateFullscreenState(false);
|
||||
this.updateComplete.then(() => {
|
||||
this.codemirror!.destroy();
|
||||
delete this.codemirror;
|
||||
|
@ -157,6 +176,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
});
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
|
@ -177,14 +197,25 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
if (transactions.length > 0) {
|
||||
this.codemirror.dispatch(...transactions);
|
||||
}
|
||||
if (changedProps.has("hasToolbar")) {
|
||||
this._updateToolbar();
|
||||
}
|
||||
if (changedProps.has("error")) {
|
||||
this.classList.toggle("error-state", this.error);
|
||||
}
|
||||
if (changedProps.has("_isFullscreen")) {
|
||||
this.classList.toggle("fullscreen", this._isFullscreen);
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (
|
||||
changedProps.has("_canCopy") ||
|
||||
changedProps.has("_canUndo") ||
|
||||
changedProps.has("_canRedo")
|
||||
) {
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("disableFullscreen")) {
|
||||
this._updateFullscreenButton();
|
||||
this._updateFullscreenState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,6 +284,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
}
|
||||
}
|
||||
|
||||
// Create the code editor
|
||||
this.codemirror = new this._loadedCodeMirror.EditorView({
|
||||
state: this._loadedCodeMirror.EditorState.create({
|
||||
doc: this._value,
|
||||
|
@ -260,71 +292,160 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
}),
|
||||
parent: this.renderRoot,
|
||||
});
|
||||
this._canCopy = this._value?.length > 0;
|
||||
|
||||
this._updateFullscreenButton();
|
||||
// Update the toolbar. Creating it if required
|
||||
this._updateToolbar();
|
||||
}
|
||||
|
||||
private _updateFullscreenButton() {
|
||||
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
|
||||
private _fullscreenLabel(): string {
|
||||
if (this._isFullscreen)
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
|
||||
"Exit fullscreen"
|
||||
);
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
|
||||
"Enter fullscreen"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.disableFullscreen) {
|
||||
// Remove button if it exists and fullscreen is disabled
|
||||
if (existingButton) {
|
||||
existingButton.remove();
|
||||
}
|
||||
// Exit fullscreen if currently in fullscreen mode
|
||||
if (this._isFullscreen) {
|
||||
this._isFullscreen = false;
|
||||
}
|
||||
private _fullscreenIcon(): string {
|
||||
return this._isFullscreen ? mdiArrowCollapse : mdiArrowExpand;
|
||||
}
|
||||
|
||||
private _createEditorToolbar(): HaIconButtonToolbar {
|
||||
// Create the editor toolbar element
|
||||
const editorToolbar = document.createElement("ha-icon-button-toolbar");
|
||||
editorToolbar.classList.add("code-editor-toolbar");
|
||||
editorToolbar.items = [];
|
||||
return editorToolbar;
|
||||
}
|
||||
|
||||
private _updateToolbar() {
|
||||
// Show/Hide the toolbar if we have one.
|
||||
this.classList.toggle("hasToolbar", this.hasToolbar);
|
||||
|
||||
// Update fullscreen state. Handles toolbar and fullscreen mode being disabled.
|
||||
this._updateFullscreenState();
|
||||
|
||||
// If we don't have a toolbar, nothing to update
|
||||
if (!this.hasToolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create button if it doesn't exist
|
||||
if (!existingButton) {
|
||||
const button = document.createElement("ha-icon-button");
|
||||
(button as any).path = this._isFullscreen
|
||||
? mdiArrowCollapse
|
||||
: mdiArrowExpand;
|
||||
button.setAttribute(
|
||||
"label",
|
||||
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||
);
|
||||
button.classList.add("fullscreen-button");
|
||||
// Use bound method to ensure proper this context
|
||||
button.addEventListener("click", this._handleFullscreenClick);
|
||||
this.renderRoot.appendChild(button);
|
||||
} else {
|
||||
// Update existing button
|
||||
(existingButton as any).path = this._isFullscreen
|
||||
? mdiArrowCollapse
|
||||
: mdiArrowExpand;
|
||||
existingButton.setAttribute(
|
||||
"label",
|
||||
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
|
||||
);
|
||||
// If we don't yet have the toolbar, create it.
|
||||
if (!this._editorToolbar) {
|
||||
this._editorToolbar = this._createEditorToolbar();
|
||||
}
|
||||
|
||||
// Ensure all toolbar buttons are correctly configured.
|
||||
this._updateToolbarButtons();
|
||||
|
||||
// Render the toolbar. This must be placed as a child of the code
|
||||
// mirror element to ensure it doesn't affect the positioning and
|
||||
// size of codemirror.
|
||||
this.codemirror?.dom.appendChild(this._editorToolbar);
|
||||
}
|
||||
|
||||
private _updateToolbarButtons() {
|
||||
// Re-render all toolbar items.
|
||||
if (!this._editorToolbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editorToolbar.items = [
|
||||
{
|
||||
id: "undo",
|
||||
disabled: !this._canUndo,
|
||||
label: this.hass?.localize("ui.common.undo") || "Undo",
|
||||
path: mdiUndo,
|
||||
action: (e: Event) => this._handleUndoClick(e),
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
disabled: !this._canRedo,
|
||||
label: this.hass?.localize("ui.common.redo") || "Redo",
|
||||
path: mdiRedo,
|
||||
action: (e: Event) => this._handleRedoClick(e),
|
||||
},
|
||||
{
|
||||
id: "copy",
|
||||
disabled: !this._canCopy,
|
||||
label:
|
||||
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
|
||||
"Copy to Clipboard",
|
||||
path: mdiContentCopy,
|
||||
action: (e: Event) => this._handleClipboardClick(e),
|
||||
},
|
||||
{
|
||||
id: "fullscreen",
|
||||
disabled: this.disableFullscreen,
|
||||
label: this._fullscreenLabel(),
|
||||
path: this._fullscreenIcon(),
|
||||
action: (e: Event) => this._handleFullscreenClick(e),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _updateFullscreenState(
|
||||
fullscreen: boolean = this._isFullscreen
|
||||
): boolean {
|
||||
// Update the current fullscreen state based on selected value. If fullscreen
|
||||
// is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
|
||||
this._isFullscreen =
|
||||
fullscreen && !this.disableFullscreen && this.hasToolbar;
|
||||
// Return whether successfully in requested state
|
||||
return this._isFullscreen === fullscreen;
|
||||
}
|
||||
|
||||
private _handleClipboardClick = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.value) {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message:
|
||||
this.hass?.localize("ui.common.copied_clipboard") ||
|
||||
"Copied to clipboard",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _handleUndoClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
undo(this.codemirror);
|
||||
};
|
||||
|
||||
private _handleRedoClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
redo(this.codemirror);
|
||||
};
|
||||
|
||||
private _handleFullscreenClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
this._updateFullscreenState(!this._isFullscreen);
|
||||
};
|
||||
|
||||
private _toggleFullscreen() {
|
||||
this._isFullscreen = !this._isFullscreen;
|
||||
this._updateFullscreenButton();
|
||||
}
|
||||
|
||||
private _handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (this._isFullscreen && e.key === "Escape") {
|
||||
if (
|
||||
(e.key === "Escape" &&
|
||||
this._isFullscreen &&
|
||||
this._updateFullscreenState(false)) ||
|
||||
(e.key === "F11" && this._updateFullscreenState(true))
|
||||
) {
|
||||
// If we successfully performed the action, stop it propagating further.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
} else if (e.key === "F11" && !this.disableFullscreen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -592,10 +713,13 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
}
|
||||
|
||||
private _onUpdate = (update: ViewUpdate): void => {
|
||||
this._canUndo = !this.readOnly && undoDepth(update.state) > 0;
|
||||
this._canRedo = !this.readOnly && redoDepth(update.state) > 0;
|
||||
if (!update.docChanged) {
|
||||
return;
|
||||
}
|
||||
this._value = update.state.doc.toString();
|
||||
this._canCopy = this._value?.length > 0;
|
||||
fireEvent(this, "value-changed", { value: this._value });
|
||||
};
|
||||
|
||||
|
@ -614,39 +738,33 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
--code-editor-toolbar-height: 28px;
|
||||
}
|
||||
|
||||
:host(.error-state) .cm-gutters {
|
||||
border-color: var(--error-state-color, red);
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
.fullscreen-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
color: var(--secondary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
border-radius: 50%;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 18px;
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
:host(.hasToolbar) .cm-gutters {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.fullscreen-button:hover,
|
||||
.fullscreen-button:active {
|
||||
opacity: 1;
|
||||
:host(.hasToolbar) .cm-focused .cm-gutters {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.fullscreen-button {
|
||||
opacity: 0.8;
|
||||
}
|
||||
:host(.error-state) .cm-content {
|
||||
border-color: var(--error-state-color, var(--error-color)) !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-content {
|
||||
border: none;
|
||||
border-top: 1px solid var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-focused .cm-content {
|
||||
border-top: 2px solid var(--primary-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
:host(.fullscreen) {
|
||||
|
@ -655,7 +773,7 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
bottom: 8px !important;
|
||||
z-index: 9999 !important;
|
||||
z-index: 6;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
||||
overflow: hidden !important;
|
||||
|
@ -672,15 +790,28 @@ export class HaCodeEditor extends ReactiveElement {
|
|||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.hasToolbar) .cm-editor {
|
||||
padding-top: var(--code-editor-toolbar-height);
|
||||
}
|
||||
|
||||
:host(.fullscreen) .cm-editor {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
:host(.fullscreen) .fullscreen-button {
|
||||
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||
:host(:not(.hasToolbar)) .code-editor-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.code-editor-toolbar {
|
||||
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
|
||||
--icon-button-toolbar-color: var(
|
||||
--code-editor-gutter-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
import "./ha-icon-button-group";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface HaIconButtonToolbarItem {
|
||||
[key: string]: any;
|
||||
path: string;
|
||||
label: string;
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
action?: (e: Event) => any;
|
||||
}
|
||||
|
||||
@customElement("ha-icon-button-toolbar")
|
||||
export class HaIconButtonToolbar extends LitElement {
|
||||
@property({ type: Array, attribute: false })
|
||||
public items: (HaIconButtonToolbarItem | string)[] = [];
|
||||
|
||||
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];
|
||||
|
||||
// Returns all toolbar buttons, or undefined if there are none.
|
||||
// Optionally returns only those with matching selector.
|
||||
public findToolbarButtons(selector = ""): HaIconButton[] | undefined {
|
||||
// Search for all toolbar buttons
|
||||
const toolbarButtons = this._buttons?.filter((button) =>
|
||||
button.classList.contains("icon-toolbar-button")
|
||||
);
|
||||
if (!toolbarButtons || !toolbarButtons.length) return undefined;
|
||||
if (!selector.length) return toolbarButtons;
|
||||
// Filter by user class if provided
|
||||
const classButtons = toolbarButtons.filter((button) =>
|
||||
button.querySelector(selector)
|
||||
);
|
||||
return classButtons.length ? classButtons : undefined;
|
||||
}
|
||||
|
||||
// Returns a toolbar button based on the provided id.
|
||||
// Will return undefined if not found.
|
||||
public findToolbarButtonById(id): HaIconButton | undefined {
|
||||
// Find the specified id
|
||||
const element = this.shadowRoot?.getElementById(id);
|
||||
if (!element || element.localName !== "ha-icon-button") return undefined;
|
||||
return element as HaIconButton;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button-group class="icon-toolbar-buttongroup">
|
||||
${this.items.map((item) =>
|
||||
typeof item === "string"
|
||||
? html`<div class="icon-toolbar-divider" role="separator"></div>`
|
||||
: html`<ha-tooltip
|
||||
.disabled=${!item.tooltip}
|
||||
.for=${item.id ?? "icon-button-" + item.label}
|
||||
>${item.tooltip ?? ""}</ha-tooltip
|
||||
>
|
||||
<ha-icon-button
|
||||
class="icon-toolbar-button"
|
||||
.id=${item.id ?? "icon-button-" + item.label}
|
||||
@click=${item.action}
|
||||
.label=${item.label}
|
||||
.path=${item.path}
|
||||
.disabled=${item.disabled ?? false}
|
||||
></ha-icon-button>`
|
||||
)}
|
||||
</ha-icon-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
background-color: var(
|
||||
--icon-button-toolbar-color,
|
||||
var(--secondary-background-color, whitesmoke)
|
||||
);
|
||||
--icon-button-toolbar-height: 32px;
|
||||
--icon-button-toolbar-button: calc(
|
||||
var(--icon-button-toolbar-height) - 4px
|
||||
);
|
||||
--icon-button-toolbar-icon: calc(
|
||||
var(--icon-button-toolbar-height) - 10px
|
||||
);
|
||||
}
|
||||
|
||||
.icon-toolbar-divider {
|
||||
height: var(--icon-button-toolbar-icon);
|
||||
margin: 0px 4px;
|
||||
border: 0.5px solid
|
||||
var(--divider-color, var(--secondary-text-color, transparent));
|
||||
}
|
||||
|
||||
.icon-toolbar-buttongroup {
|
||||
background-color: transparent;
|
||||
padding-right: 4px;
|
||||
height: var(--icon-button-toolbar-height);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-toolbar-button {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-icon-button-size: var(--icon-button-toolbar-button);
|
||||
--mdc-icon-size: var(--icon-button-toolbar-icon);
|
||||
/* Ensure button is clickable on iOS */
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-button-toolbar": HaIconButtonToolbar;
|
||||
}
|
||||
}
|
|
@ -593,7 +593,6 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
|||
`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
copy-clipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
|
|
|
@ -500,7 +500,6 @@ export class HaScriptEditor extends UndoRedoMixin<
|
|||
`
|
||||
: this._mode === "yaml"
|
||||
? html`<ha-yaml-editor
|
||||
copy-clipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
|
|
|
@ -215,7 +215,6 @@ class HaPanelDevAction extends LitElement {
|
|||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
copy-clipboard
|
||||
read-only
|
||||
auto-update
|
||||
has-extra-actions
|
||||
|
|
|
@ -1219,7 +1219,9 @@
|
|||
"yaml-editor": {
|
||||
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
|
||||
"error": "Error in parsing YAML: {reason}",
|
||||
"error_location": "line: {line}, column: {column}"
|
||||
"error_location": "line: {line}, column: {column}",
|
||||
"enter_fullscreen": "Enter fullscreen",
|
||||
"exit_fullscreen": "Exit fullscreen"
|
||||
},
|
||||
"state-content-picker": {
|
||||
"state": "State",
|
||||
|
|
Loading…
Reference in New Issue