1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Fix code editor autocomplete using wa popup (#30081)

This commit is contained in:
Aidan Timson
2026-03-10 16:20:40 +00:00
committed by Bram Kragten
parent cfa8eb5370
commit ee77619da3
2 changed files with 214 additions and 13 deletions

View File

@@ -1,3 +1,5 @@
import "@home-assistant/webawesome/dist/components/popup/popup";
import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup";
import type {
Completion,
CompletionContext,
@@ -110,6 +112,18 @@ export class HaCodeEditor extends ReactiveElement {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _completionInfoPopover?: WaPopup;
private _completionInfoContainer?: HTMLDivElement;
private _completionInfoDestroy?: () => void;
private _completionInfoRequest = 0;
private _completionInfoKey?: string;
private _completionInfoFrame?: number;
private _editorToolbar?: HaIconButtonToolbar;
private _iconList?: Completion[];
@@ -155,6 +169,14 @@ export class HaCodeEditor extends ReactiveElement {
public disconnectedCallback() {
fireEvent(this, "dialog-set-fullscreen", false);
this._clearCompletionInfo();
if (this._completionInfoFrame !== undefined) {
cancelAnimationFrame(this._completionInfoFrame);
this._completionInfoFrame = undefined;
}
this._completionInfoPopover?.remove();
this._completionInfoPopover = undefined;
this._completionInfoContainer = undefined;
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.removeEventListener("keydown", this._handleKeyDown);
@@ -288,6 +310,9 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.foldingCompartment.of(
this._getFoldingExtensions()
),
this._loadedCodeMirror.tooltips({
position: "absolute",
}),
...(this.placeholder ? [placeholder(this.placeholder)] : []),
];
@@ -595,6 +620,157 @@ export class HaCodeEditor extends ReactiveElement {
return completionInfo;
};
private _getCompletionInfo = (
completion: Completion
): CompletionInfo | Promise<CompletionInfo> | null => {
if (this.hass && completion.label in this.hass.states) {
return this._renderInfo(completion);
}
if (completion.label.startsWith("mdi:")) {
return renderIcon(completion);
}
return null;
};
private _ensureCompletionInfoPopover(): WaPopup {
if (!this._completionInfoPopover) {
this._completionInfoPopover = document.createElement(
"wa-popup"
) as WaPopup;
this._completionInfoPopover.classList.add("completion-info-popover");
this._completionInfoPopover.placement = "right-start";
this._completionInfoPopover.distance = 4;
this._completionInfoPopover.flip = true;
this._completionInfoPopover.flipFallbackPlacements =
"left-start bottom-start top-start";
this._completionInfoPopover.shift = true;
this._completionInfoPopover.shiftPadding = 8;
this._completionInfoPopover.autoSize = "both";
this._completionInfoPopover.autoSizePadding = 8;
this._completionInfoContainer = document.createElement("div");
this._completionInfoPopover.appendChild(this._completionInfoContainer);
this.renderRoot.appendChild(this._completionInfoPopover);
}
return this._completionInfoPopover;
}
private _clearCompletionInfo() {
this._completionInfoRequest += 1;
this._completionInfoKey = undefined;
this._completionInfoDestroy?.();
this._completionInfoDestroy = undefined;
this._completionInfoContainer?.replaceChildren();
if (this._completionInfoPopover?.active) {
this._completionInfoPopover.active = false;
}
}
private _renderCompletionInfoContent(info: CompletionInfo) {
this._completionInfoDestroy?.();
this._completionInfoDestroy = undefined;
if (!this._completionInfoContainer) {
return;
}
if (info === null) {
this._completionInfoContainer.replaceChildren();
return;
}
if ("nodeType" in info) {
this._completionInfoContainer.replaceChildren(info);
return;
}
this._completionInfoContainer.replaceChildren(info.dom);
this._completionInfoDestroy = info.destroy;
}
private _syncCompletionInfoPopover = () => {
if (this._completionInfoFrame !== undefined) {
cancelAnimationFrame(this._completionInfoFrame);
}
this._completionInfoFrame = requestAnimationFrame(() => {
this._completionInfoFrame = undefined;
this._syncCompletionInfoPopoverNow();
});
};
private _syncCompletionInfoPopoverNow = () => {
if (!this.codemirror || !this._loadedCodeMirror) {
return;
}
if (window.matchMedia("(max-width: 600px)").matches) {
this._clearCompletionInfo();
return;
}
const completion = this._loadedCodeMirror.selectedCompletion(
this.codemirror.state
);
const selectedOption = this.codemirror.dom.querySelector(
".cm-tooltip-autocomplete li[aria-selected]"
) as HTMLElement | null;
if (!completion || !selectedOption) {
this._clearCompletionInfo();
return;
}
const infoResult = this._getCompletionInfo(completion);
if (!infoResult) {
this._clearCompletionInfo();
return;
}
const requestId = ++this._completionInfoRequest;
const infoKey = completion.label;
const popover = this._ensureCompletionInfoPopover();
popover.anchor = selectedOption;
const showPopover = async (info: CompletionInfo) => {
if (requestId !== this._completionInfoRequest) {
if (info && typeof info === "object" && "destroy" in info) {
info.destroy?.();
}
return;
}
if (infoKey !== this._completionInfoKey) {
this._renderCompletionInfoContent(info);
this._completionInfoKey = infoKey;
}
await popover.updateComplete;
popover.active = true;
popover.reposition();
};
if ("then" in infoResult) {
infoResult.then(showPopover).catch(() => {
if (requestId === this._completionInfoRequest) {
this._clearCompletionInfo();
}
});
return;
}
showPopover(infoResult).catch(() => {
if (requestId === this._completionInfoRequest) {
this._clearCompletionInfo();
}
});
};
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
@@ -604,7 +780,6 @@ export class HaCodeEditor extends ReactiveElement {
type: "variable",
label: key,
detail: states[key].attributes.friendly_name,
info: this._renderInfo,
}));
return options;
@@ -778,7 +953,6 @@ export class HaCodeEditor extends ReactiveElement {
type: "variable",
label: `mdi:${icon.name}`,
detail: icon.keywords.join(", "),
info: renderIcon,
}));
}
@@ -806,6 +980,7 @@ 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;
this._syncCompletionInfoPopover();
if (!update.docChanged) {
return;
}
@@ -925,9 +1100,31 @@ export class HaCodeEditor extends ReactiveElement {
padding: 8px;
}
wa-popup.completion-info-popover {
--auto-size-available-width: min(
420px,
calc(var(--safe-width) - var(--ha-space-8))
);
}
wa-popup.completion-info-popover::part(popup) {
padding: 0;
color: var(--primary-text-color);
background-color: var(
--code-editor-background-color,
var(--card-background-color)
);
border: 1px solid var(--divider-color);
border-radius: var(--mdc-shape-medium, 4px);
box-shadow:
0px 5px 5px -3px rgb(0 0 0 / 20%),
0px 8px 10px 1px rgb(0 0 0 / 14%),
0px 3px 14px 2px rgb(0 0 0 / 12%);
}
/* Hide completion info on narrow screens */
@media (max-width: 600px) {
.cm-completionInfo,
wa-popup.completion-info-popover,
.completion-info {
display: none;
}

View File

@@ -12,7 +12,7 @@ import type { KeyBinding } from "@codemirror/view";
import { EditorView } from "@codemirror/view";
import { tags } from "@lezer/highlight";
export { autocompletion } from "@codemirror/autocomplete";
export { autocompletion, selectedCompletion } from "@codemirror/autocomplete";
export { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
export { highlightingFor, foldGutter } from "@codemirror/language";
export {
@@ -32,6 +32,7 @@ export {
lineNumbers,
rectangularSelection,
dropCursor,
tooltips,
} from "@codemirror/view";
export { indentationMarkers } from "@replit/codemirror-indentation-markers";
export { tags } from "@lezer/highlight";
@@ -151,10 +152,22 @@ export const haTheme = EditorView.theme({
"var(--code-editor-background-color, var(--card-background-color))",
border: "1px solid var(--divider-color)",
borderRadius: "var(--mdc-shape-medium, 4px)",
maxWidth: "min(420px, calc(var(--safe-width) - var(--ha-space-8)))",
boxSizing: "border-box",
boxShadow:
"0px 5px 5px -3px rgb(0 0 0 / 20%), 0px 8px 10px 1px rgb(0 0 0 / 14%), 0px 3px 14px 2px rgb(0 0 0 / 12%)",
},
".cm-tooltip.cm-tooltip-autocomplete": {
maxWidth:
"min(420px, calc(var(--safe-width) - var(--ha-space-8)), calc(100% - var(--ha-space-2)))",
},
".cm-tooltip-autocomplete > ul": {
maxWidth: "100%",
boxSizing: "border-box",
},
"& .cm-tooltip.cm-tooltip-autocomplete > ul > li": {
padding: "4px 8px",
},
@@ -177,15 +190,6 @@ export const haTheme = EditorView.theme({
color: "var(--text-primary-color)",
},
"& .cm-completionInfo.cm-completionInfo-right": {
left: "calc(100% + 4px)",
},
"& .cm-tooltip.cm-completionInfo": {
padding: "4px 8px",
marginTop: "-5px",
},
".cm-selectionMatch": {
backgroundColor: "rgba(var(--rgb-primary-color), 0.1)",
},