From 3aafa47f6d442482232b1e3ef14dfd400634e18c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Jun 2025 22:22:31 +0200 Subject: [PATCH] Improve Entity ID auto-complete in YAML mode (#25901) --- src/components/ha-code-editor.ts | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index 17003c8e0b..190b456fbd 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -257,6 +257,126 @@ export class HaCodeEditor extends ReactiveElement { private _entityCompletions( context: CompletionContext ): CompletionResult | null | Promise { + // Check for YAML mode and entity-related fields + if (this.mode === "yaml") { + const currentLine = context.state.doc.lineAt(context.pos); + const lineText = currentLine.text; + + // Properties that commonly contain entity IDs + const entityProperties = [ + "entity_id", + "entity", + "entities", + "badges", + "devices", + "lights", + "light", + "group_members", + "scene", + "zone", + "zones", + ]; + + // Create regex pattern for all entity properties + const propertyPattern = entityProperties.join("|"); + const entityFieldRegex = new RegExp( + `^\\s*(-\\s+)?(${propertyPattern}):\\s*` + ); + + // Check if we're in an entity field (single entity or list item) + const entityFieldMatch = lineText.match(entityFieldRegex); + const listItemMatch = lineText.match(/^\s*-\s+/); + + if (entityFieldMatch) { + // Calculate the position after the entity field + const afterField = currentLine.from + entityFieldMatch[0].length; + + // If cursor is after the entity field, show all entities + if (context.pos >= afterField) { + const states = this._getStates(this.hass!.states); + + if (!states || !states.length) { + return null; + } + + // Find what's already typed after the field + const typedText = context.state.sliceDoc(afterField, context.pos); + + // Filter states based on what's typed + const filteredStates = typedText + ? states.filter((entityState) => + entityState.label + .toLowerCase() + .startsWith(typedText.toLowerCase()) + ) + : states; + + return { + from: afterField, + options: filteredStates, + validFor: /^[a-z_]*\.?\w*$/, + }; + } + } else if (listItemMatch) { + // Check if this is a list item under an entity_id field + const lineNumber = currentLine.number; + + // Look at previous lines to check if we're under an entity_id field + for (let i = lineNumber - 1; i > 0 && i >= lineNumber - 10; i--) { + const prevLine = context.state.doc.line(i); + const prevText = prevLine.text; + + // Stop if we hit a non-indented line (new field) + if ( + prevText.trim() && + !prevText.startsWith(" ") && + !prevText.startsWith("\t") + ) { + break; + } + + // Check if we found an entity property field + const entityListFieldRegex = new RegExp( + `^\\s*(${propertyPattern}):\\s*$` + ); + if (prevText.match(entityListFieldRegex)) { + // We're in a list under an entity field + const afterListMarker = currentLine.from + listItemMatch[0].length; + + if (context.pos >= afterListMarker) { + const states = this._getStates(this.hass!.states); + + if (!states || !states.length) { + return null; + } + + // Find what's already typed after the list marker + const typedText = context.state.sliceDoc( + afterListMarker, + context.pos + ); + + // Filter states based on what's typed + const filteredStates = typedText + ? states.filter((entityState) => + entityState.label + .toLowerCase() + .startsWith(typedText.toLowerCase()) + ) + : states; + + return { + from: afterListMarker, + options: filteredStates, + validFor: /^[a-z_]*\.?\w*$/, + }; + } + } + } + } + } + + // Original entity completion logic for non-YAML or when not in entity_id field const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/); if (