Skip to content
Merged
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.5",
"@codemirror/gutter": "^0.19.4",
"@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0",
"@codemirror/autocomplete": "^0.19.12",
"@codemirror/commands": "^0.19.8",
"@codemirror/gutter": "^0.19.9",
"@codemirror/highlight": "^0.19.7",
"@codemirror/history": "^0.19.2",
"@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.4",
"@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.5",
"@codemirror/view": "^0.19.15",
"@codemirror/search": "^0.19.6",
"@codemirror/state": "^0.19.6",
"@codemirror/stream-parser": "^0.19.5",
"@codemirror/text": "^0.19.6",
"@codemirror/view": "^0.19.40",
"@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.40",
Expand Down
119 changes: 90 additions & 29 deletions src/components/ha-code-editor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import type {
Completion,
CompletionContext,
CompletionResult,
} from "@codemirror/autocomplete";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types";

declare global {
interface HASSDomEvents {
Expand All @@ -24,10 +32,15 @@ export class HaCodeEditor extends ReactiveElement {

@property() public mode = "yaml";

public hass?: HomeAssistant;

@property({ type: Boolean }) public autofocus = false;

@property({ type: Boolean }) public readOnly = false;

@property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false;

@property() public error = false;

@state() private _value = "";
Expand Down Expand Up @@ -110,43 +123,92 @@ export class HaCodeEditor extends ReactiveElement {

private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror();
const extensions = [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
];

if (!this.readOnly && this.autocompleteEntities && this.hass) {
extensions.push(
this._loadedCodeMirror.autocompletion({
override: [this._entityCompletions.bind(this)],
maxRenderedOptions: 10,
})
);
}

this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({
doc: this._value,
extensions: [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],
extensions,
}),
root: this.shadowRoot!,
parent: this.shadowRoot!,
});
}

private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
}
const options = Object.keys(states).map((key) => ({
type: "variable",
label: key,
detail: states[key].attributes.friendly_name,
info: `State: ${states[key].state}`,
}));

return options;
});

private _entityCompletions(
context: CompletionContext
): CompletionResult | null | Promise<CompletionResult | null> {
const entityWord = context.matchBefore(/[a-z_]{3,}\./);

if (
!entityWord ||
(entityWord.from === entityWord.to && !context.explicit)
) {
return null;
}

const states = this._getStates(this.hass!.states);

if (!states || !states.length) {
return null;
}

return {
from: Number(entityWord.from),
options: states,
span: /^\w*.\w*$/,
};
}

private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
Expand All @@ -163,10 +225,9 @@ export class HaCodeEditor extends ReactiveElement {
fireEvent(this, "value-changed", { value: this._value });
}

// Only Lit 2.0 will use this
static get styles(): CSSResultGroup {
return css`
:host(.error-state) div.cm-wrap .cm-gutters {
:host(.error-state) .cm-gutters {
border-color: var(--error-state-color, red);
}
`;
Expand Down
1 change: 1 addition & 0 deletions src/components/ha-selector/ha-selector-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class HaObjectSelector extends LitElement {

protected render() {
return html`<ha-yaml-editor
.hass=${this.hass}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
Expand Down
1 change: 1 addition & 0 deletions src/components/ha-service-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export class HaServiceControl extends LitElement {
: ""}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.service_data"
)}
Expand Down
5 changes: 5 additions & 0 deletions src/components/ha-yaml-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-code-editor";

const isEmpty = (obj: Record<string, unknown>): boolean => {
Expand All @@ -18,6 +19,8 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {

@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;

@property() public value?: any;

@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
Expand Down Expand Up @@ -56,8 +59,10 @@ export class HaYamlEditor extends LitElement {
return html`
${this.label ? html`<p>${this.label}</p>` : ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
mode="yaml"
autocomplete-entities
.error=${this.isValid === false}
@value-changed=${this._onChange}
dir="ltr"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export default class HaAutomationActionRow extends LitElement {
)}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class HaEventAction extends LitElement implements ActionElement {
@value-changed=${this._eventChanged}
></paper-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.service_data"
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default class HaAutomationConditionEditor extends LitElement {
)}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
Expand Down
1 change: 1 addition & 0 deletions src/panels/config/automation/ha-automation-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
`
: ``}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
@value-changed=${this._valueChanged}
></paper-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_data"
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class HaPanelDevMqtt extends LitElement {
<p>${this.hass.localize("ui.panel.config.mqtt.payload")}</p>
<ha-code-editor
mode="jinja2"
autocomplete-entities
.hass=${this.hass}
.value=${this.payload}
@value-changed=${this._handlePayload}
dir="ltr"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class DialogZHADeviceZigbeeInfo extends LitElement {
>
<ha-code-editor
mode="yaml"
readonly
readOnly
.value=${this._signature}
dir="ltr"
>
Expand Down
1 change: 1 addition & 0 deletions src/panels/config/script/ha-script-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
`
: ``}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class HaPanelDevService extends LitElement {
@value-changed=${this._serviceChanged}
></ha-service-picker>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._serviceData}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ class HaPanelDevTemplate extends LitElement {
</p>
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this._template}
.error=${this._error}
autofocus
autocomplete-entities
@value-changed=${this._templateChanged}
dir="ltr"
></ha-code-editor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class HuiDialogSuggestCard extends LitElement {
? html`
<div class="editor">
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._cardConfig}
></ha-yaml-editor>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/editor/hui-dialog-save-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
)}
</p>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._params!.lovelace.config}
></ha-yaml-editor>
`}
Expand Down
2 changes: 2 additions & 0 deletions src/panels/lovelace/editor/hui-element-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ export abstract class HuiElementEditor<T> extends LitElement {
<ha-code-editor
mode="yaml"
autofocus
autocomplete-entities
.hass=${this.hass}
.value=${this.yaml}
.error=${Boolean(this._errors)}
@value-changed=${this._handleYAMLChanged}
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/hui-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class LovelaceFullConfigEditor extends LitElement {
<ha-code-editor
mode="yaml"
autofocus
autocomplete-entities
.hass=${this.hass}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSave}
Expand Down
Loading