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
This commit is contained in:
CalamitousFelicitousness
2026-05-10 19:47:54 +01:00
parent df15857a61
commit f2227d7785
3 changed files with 24 additions and 22 deletions
+13 -11
View File
@@ -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();