Skip to content

Commit

Permalink
Add crosshairs, destroy globals, and tweak updates for code editor (#…
Browse files Browse the repository at this point in the history
…17302)

* Add crosshairs, destroy globals, and tweak updates for code editor

* Define update listener as arrow function

* Ensure editor is recreated on reconnection

* Don't create code mirror multiple times

* Remove creation in update

* Leverage lit lifecycle for editor creation and destruction

* Bump @codemirror packages

---------

Co-authored-by: Paul Bottein <[email protected]>
  • Loading branch information
steverep and piitaya authored Aug 4, 2023
1 parent 0d630aa commit 716e68f
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 41 deletions.
77 changes: 44 additions & 33 deletions src/components/ha-code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import type {
CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state";
import type { Extension, TransactionSpec } from "@codemirror/state";
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 { stopPropagation } from "../common/dom/stop_propagation";
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types";
import "./ha-icon";

Expand Down Expand Up @@ -54,11 +54,11 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "autocomplete-icons" })
public autocompleteIcons = false;

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

@state() private _value = "";

private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _loadedCodeMirror?: CodeMirror;

private _iconList?: Completion[];

Expand All @@ -78,12 +78,19 @@ export class HaCodeEditor extends ReactiveElement {
this.codemirror.state,
[this._loadedCodeMirror.tags.comment]
);
return !!this.shadowRoot!.querySelector(`span.${className}`);
return !!this.renderRoot.querySelector(`span.${className}`);
}

public connectedCallback() {
super.connectedCallback();
// Force update on reconnection so editor is recreated
if (this.hasUpdated) {
this.requestUpdate();
}
this.addEventListener("keydown", stopPropagation);
// This is unreachable as editor will not exist yet,
// but focus should not behave like this for good a11y.
// (@steverep to fix in autofocus PR)
if (!this.codemirror) {
return;
}
Expand All @@ -95,78 +102,87 @@ export class HaCodeEditor extends ReactiveElement {
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.updateComplete.then(() => {
this.codemirror!.destroy();
delete this.codemirror;
});
}

// Ensure CodeMirror module is loaded before any update
protected override async scheduleUpdate() {
this._loadedCodeMirror ??= await loadCodeMirror();
super.scheduleUpdate();
}

protected update(changedProps: PropertyValues): void {
super.update(changedProps);

if (!this.codemirror) {
this._createCodeMirror();
return;
}

const transactions: TransactionSpec[] = [];
if (changedProps.has("mode")) {
this.codemirror.dispatch({
transactions.push({
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode
),
});
}
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
transactions.push({
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
});
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.dispatch({
transactions.push({
changes: {
from: 0,
to: this.codemirror.state.doc.length,
insert: this._value,
},
});
}
if (transactions.length > 0) {
this.codemirror.dispatch(...transactions);
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
}

protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._load();
}

private get _mode() {
return this._loadedCodeMirror!.langs[this.mode];
}

private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror();
private _createCodeMirror() {
if (!this._loadedCodeMirror) {
throw new Error("Cannot create editor before CodeMirror is loaded");
}
const extensions: Extension[] = [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.crosshairCursor(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
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.haTheme,
this._loadedCodeMirror.haSyntaxHighlighting,
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
];

if (!this.readOnly) {
Expand All @@ -192,8 +208,7 @@ export class HaCodeEditor extends ReactiveElement {
doc: this._value,
extensions,
}),
root: this.shadowRoot!,
parent: this.shadowRoot!,
parent: this.renderRoot,
});
}

Expand Down Expand Up @@ -277,17 +292,13 @@ export class HaCodeEditor extends ReactiveElement {
};
}

private _onUpdate(update: ViewUpdate): void {
private _onUpdate = (update: ViewUpdate): void => {
if (!update.docChanged) {
return;
}
const newValue = this.value;
if (newValue === this._value) {
return;
}
this._value = newValue;
this._value = update.state.doc.toString();
fireEvent(this, "value-changed", { value: this._value });
}
};

static get styles(): CSSResultGroup {
return css`
Expand Down
2 changes: 1 addition & 1 deletion src/components/ha-yaml-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class HaYamlEditor extends LitElement {

@property() public defaultValue?: any;

@property() public isValid = true;
@property({ type: Boolean }) public isValid = true;

@property() public label?: string;

Expand Down
12 changes: 5 additions & 7 deletions src/resources/codemirror.ondemand.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
let loaded: Promise<typeof import("./codemirror")>;
export type CodeMirror = typeof import("./codemirror");

export const loadCodeMirror = async (): Promise<
typeof import("./codemirror")
> => {
if (!loaded) {
loaded = import("./codemirror");
}
let loaded: CodeMirror;

export const loadCodeMirror = async () => {
loaded ??= await import("./codemirror");
return loaded;
};
1 change: 1 addition & 0 deletions src/resources/codemirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { highlightingFor } from "@codemirror/language";
export { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
export { EditorState } from "@codemirror/state";
export {
crosshairCursor,
drawSelection,
EditorView,
highlightActiveLine,
Expand Down

0 comments on commit 716e68f

Please sign in to comment.