From f2227d77859c58d6dc301366b125c049f1ce7a71 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Sun, 10 May 2026 19:47:54 +0100 Subject: [PATCH] feat(autocomplete): apply keep-underscores setting to insertion Rename `autocomplete_replace_underscores` to `autocomplete_keep_underscores` (default false) so both autocomplete toggles read as "keep" semantics. `insertTag` now honors the setting for both tag and artist modes, matching the dropdown display. Embedding insertion always preserves underscores because the names are filesystem identifiers. - ui_definitions: rename option, flip default, update label - scripts/autocomplete: rename handler, checkbox, and config-bridge keys - javascript/autocomplete: invert render polarity; insertTag takes a `kind` parameter; drop the artist-mode hardcoded underscore replacement so the setting controls tags and artists uniformly Closes #4827 --- javascript/autocomplete.js | 24 +++++++++++++----------- modules/ui_definitions.py | 2 +- scripts/autocomplete.py | 20 ++++++++++---------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/javascript/autocomplete.js b/javascript/autocomplete.js index 8e8e91d33..b020490c2 100644 --- a/javascript/autocomplete.js +++ b/javascript/autocomplete.js @@ -358,7 +358,7 @@ function insertExtraNetwork(textarea, item, kind) { } /** Insert a tag at the current word position, replacing the typed prefix. */ -function insertTag(textarea, tagName) { +function insertTag(textarea, tagName, kind = 'tag') { const info = getCurrentWord(textarea); if (!info || (info.mode !== 'tag' && info.mode !== 'artist')) return; const { value } = textarea; @@ -371,13 +371,13 @@ function insertTag(textarea, tagName) { const prefix = needsSepBefore ? `${sep} ` : ''; let suffix = `${sep} `; if (after.length > 0 && after.trimStart().startsWith(',')) suffix = ' '; - // Artist mode: optionally keep the `@` prefix (Anima syntax); always convert underscores to spaces - // since Anima requires space-separated artist names. The `@` is consumed for non-Anima models. + // Embedding names are file-system identifiers, so underscores must be preserved regardless of the user setting. + // Tags and artists honor `autocomplete_keep_underscores`; default is to swap `_` for space. + const keepUnderscores = window.opts?.autocomplete_keep_underscores ?? false; let body = tagName; - if (info.mode === 'artist') { - body = body.replace(/_/g, ' '); - if (window.opts?.autocomplete_at_prefix_artist) body = `@${body}`; - } + if (kind !== 'embed' && !keepUnderscores) body = body.replace(/_/g, ' '); + // Artist mode optionally keeps the `@` prefix (Anima syntax). The `@` is consumed for non-Anima models. + if (info.mode === 'artist' && window.opts?.autocomplete_at_prefix_artist) body = `@${body}`; const insertion = `${prefix}${escapeParensForPrompt(body)}${suffix}`; textarea.value = before.trimEnd() + (before.trimEnd().length > 0 ? ' ' : '') + insertion + after.trimStart(); // Position cursor after the inserted tag + separator @@ -447,7 +447,7 @@ const dropdown = { }, render() { - const replaceUnderscores = window.opts?.autocomplete_replace_underscores ?? true; + const keepUnderscores = window.opts?.autocomplete_keep_underscores ?? false; const queryNorm = this.query.toLowerCase().replace(/ /g, '_'); this.listEl.replaceChildren(); this.results.forEach((tag, i) => { @@ -462,7 +462,9 @@ const dropdown = { dot.title = kind === 'tag' ? (engine.categoryNames[tag.category] || '') : kind; const name = document.createElement('span'); name.className = 'autocomplete-tag'; - const tagText = replaceUnderscores ? tag.display.replace(/_/g, ' ') : tag.display; + // Embeddings are file-name identifiers, so they always render as-is to match how they get inserted. + const swapForKind = kind !== 'embed'; + const tagText = (swapForKind && !keepUnderscores) ? tag.display.replace(/_/g, ' ') : tag.display; const canonicalMatch = tag.name.indexOf(queryNorm); if (canonicalMatch >= 0 && queryNorm.length > 0) { const mark = document.createElement('mark'); @@ -480,7 +482,7 @@ const dropdown = { if (tag.matchedVia === 'alias') annotationTerm = tag.matchedAlias; else if (tag.matchedVia === 'translation') annotationTerm = tag.matchedTerm; if (annotationTerm) { - const annotationDisplay = replaceUnderscores ? annotationTerm.replace(/_/g, ' ') : annotationTerm; + const annotationDisplay = (swapForKind && !keepUnderscores) ? annotationTerm.replace(/_/g, ' ') : annotationTerm; const annotationLower = annotationTerm.toLowerCase(); const annotationMatch = annotationLower.indexOf(queryNorm); const prefix = tag.matchedVia === 'translation' ? ' \u{1F310} ' : ' ('; @@ -561,7 +563,7 @@ const dropdown = { insertExtraNetwork(this.textarea, result, result.kind); } else { // 'embed' kind and untagged tag results both go through insertTag (comma-aware, paren-escaped). - insertTag(this.textarea, result.display ?? result.name); + insertTag(this.textarea, result.display ?? result.name, result.kind); } } this.hide(); diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index 24a6881c0..f4a8f9010 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -684,7 +684,7 @@ def create_settings(cmd_opts): "autocomplete_active": OptionInfo(False, "Enable Autocomplete", gr.Checkbox, {"visible": False}), "autocomplete_enabled": OptionInfo([], "Enabled tag autocomplete files", gr.Dropdown, {"multiselect": True, "choices": [], "visible": False}), "autocomplete_min_chars": OptionInfo(3, "Min autocomplete chars", gr.Slider, {"minimum": 2, "maximum": 6, "step": 1, "visible": False}), - "autocomplete_replace_underscores": OptionInfo(True, "Replace underscores in autocomplete", gr.Checkbox, {"visible": False}), + "autocomplete_keep_underscores": OptionInfo(False, "Keep underscores in autocomplete", gr.Checkbox, {"visible": False}), "autocomplete_append_comma": OptionInfo(True, "Append comma after autocomplete", gr.Checkbox, {"visible": False}), # Caption settings (controlled via Caption Tab UI) diff --git a/scripts/autocomplete.py b/scripts/autocomplete.py index 0665ace6a..664619afd 100644 --- a/scripts/autocomplete.py +++ b/scripts/autocomplete.py @@ -27,7 +27,7 @@ def get_config_json(): "autocomplete_active": bool(shared.opts.data.get('autocomplete_active', False)), "autocomplete_enabled": enabled, "autocomplete_min_chars": shared.opts.data.get('autocomplete_min_chars', 3), - "autocomplete_replace_underscores": shared.opts.data.get('autocomplete_replace_underscores', True), + "autocomplete_keep_underscores": shared.opts.data.get('autocomplete_keep_underscores', False), "autocomplete_append_comma": shared.opts.data.get('autocomplete_append_comma', True), "autocomplete_at_prefix_artist": shared.opts.data.get('autocomplete_at_prefix_artist', False), "autocomplete_translations": bool(shared.opts.data.get('autocomplete_translations', False)), @@ -52,8 +52,8 @@ def on_min_chars_change(value): return get_config_json() -def on_replace_underscores_change(value): - shared.opts.data['autocomplete_replace_underscores'] = bool(value) +def on_keep_underscores_change(value): + shared.opts.data['autocomplete_keep_underscores'] = bool(value) shared.opts.save(silent=True) return get_config_json() @@ -180,10 +180,10 @@ class AutocompleteScript(scripts_manager.Script): refresh_btn = ToolButton(value=symbols.refresh, elem_id=self.elem_id("refresh")) update_btn = ToolButton(value=symbols.save, elem_id=self.elem_id("update")) with gr.Row(): - replace_underscores = gr.Checkbox( - label="Replace underscores", - value=shared.opts.data.get('autocomplete_replace_underscores', True), - elem_id=self.elem_id("replace_underscores"), + keep_underscores = gr.Checkbox( + label="Keep underscores", + value=shared.opts.data.get('autocomplete_keep_underscores', False), + elem_id=self.elem_id("keep_underscores"), ) append_comma = gr.Checkbox( label="Comma separator", @@ -217,14 +217,14 @@ class AutocompleteScript(scripts_manager.Script): active_cb.change(fn=on_active_change, inputs=[active_cb], outputs=[config_json, status]) enabled_dd.change(fn=on_enabled_change, inputs=[enabled_dd], outputs=[config_json, status]) min_chars.change(fn=on_min_chars_change, inputs=[min_chars], outputs=[config_json]) - replace_underscores.change(fn=on_replace_underscores_change, inputs=[replace_underscores], outputs=[config_json]) + keep_underscores.change(fn=on_keep_underscores_change, inputs=[keep_underscores], outputs=[config_json]) append_comma.change(fn=on_append_comma_change, inputs=[append_comma], outputs=[config_json]) at_prefix_artist.change(fn=on_at_prefix_artist_change, inputs=[at_prefix_artist], outputs=[config_json]) translations_cb.change(fn=on_translations_change, inputs=[translations_cb], outputs=[config_json]) refresh_btn.click(fn=on_refresh, inputs=[], outputs=[enabled_dd, status]) update_btn.click(fn=on_update, inputs=[enabled_dd], outputs=[status]) - for comp in [enabled_dd, min_chars, replace_underscores, append_comma, at_prefix_artist, translations_cb, config_json, status]: + for comp in [enabled_dd, min_chars, keep_underscores, append_comma, at_prefix_artist, translations_cb, config_json, status]: comp.do_not_save_to_config = True - return [active_cb, enabled_dd, min_chars, replace_underscores, append_comma, at_prefix_artist, translations_cb, config_json] + return [active_cb, enabled_dd, min_chars, keep_underscores, append_comma, at_prefix_artist, translations_cb, config_json]