Merge pull request #420 from koloml/suggestions-aliases-handling-in-local-ac

Fixed aliases still displaying for namespaced tags, fixing the actual cause of the issue inside local autocomplete
This commit is contained in:
liamwhite 2025-02-12 21:11:14 -05:00 committed by GitHub
commit eb164ab11f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 39 additions and 64 deletions

View file

@ -8,7 +8,7 @@ import store from './utils/store';
import { TermContext } from './query/lex'; import { TermContext } from './query/lex';
import { $$ } from './utils/dom'; import { $$ } from './utils/dom';
import { import {
createLocalAutocompleteResultFormatter, formatLocalAutocompleteResult,
fetchLocalAutocomplete, fetchLocalAutocomplete,
fetchSuggestions, fetchSuggestions,
SuggestionsPopup, SuggestionsPopup,
@ -196,11 +196,9 @@ function listenAutocomplete() {
originalTerm = `${inputField.value}`.toLowerCase(); originalTerm = `${inputField.value}`.toLowerCase();
} }
const matchedTerm = trimPrefixes(originalTerm);
const suggestions = localAc const suggestions = localAc
.matchPrefix(matchedTerm, suggestionsCount) .matchPrefix(trimPrefixes(originalTerm), suggestionsCount)
.map(createLocalAutocompleteResultFormatter(matchedTerm)); .map(formatLocalAutocompleteResult);
if (suggestions.length) { if (suggestions.length) {
popup.renderSuggestions(suggestions).showForField(targetedInput); popup.renderSuggestions(suggestions).showForField(targetedInput);

View file

@ -2,7 +2,7 @@ import { fetchMock } from '../../../test/fetch-mock.ts';
import { import {
fetchLocalAutocomplete, fetchLocalAutocomplete,
fetchSuggestions, fetchSuggestions,
createLocalAutocompleteResultFormatter, formatLocalAutocompleteResult,
purgeSuggestionsCache, purgeSuggestionsCache,
SuggestionsPopup, SuggestionsPopup,
TermSuggestion, TermSuggestion,
@ -334,13 +334,12 @@ describe('Suggestions', () => {
}); });
}); });
describe('createLocalAutocompleteResultFormatter', () => { describe('formatLocalAutocompleteResult', () => {
it('should format suggested tags as tag name and the count', () => { it('should format suggested tags as tag name and the count', () => {
const tagName = 'safe'; const tagName = 'safe';
const tagCount = getRandomIntBetween(5, 10); const tagCount = getRandomIntBetween(5, 10);
const formatter = createLocalAutocompleteResultFormatter(); const resultObject = formatLocalAutocompleteResult({
const resultObject = formatter({
name: tagName, name: tagName,
aliasName: tagName, aliasName: tagName,
imageCount: tagCount, imageCount: tagCount,
@ -355,8 +354,7 @@ describe('Suggestions', () => {
const tagAlias = 'rating:safe'; const tagAlias = 'rating:safe';
const tagCount = getRandomIntBetween(5, 10); const tagCount = getRandomIntBetween(5, 10);
const formatter = createLocalAutocompleteResultFormatter(); const resultObject = formatLocalAutocompleteResult({
const resultObject = formatter({
name: tagName, name: tagName,
aliasName: tagAlias, aliasName: tagAlias,
imageCount: tagCount, imageCount: tagCount,
@ -365,39 +363,5 @@ describe('Suggestions', () => {
expect(resultObject.label).toBe(`${tagAlias}${tagName} (${tagCount})`); expect(resultObject.label).toBe(`${tagAlias}${tagName} (${tagCount})`);
expect(resultObject.value).toBe(tagName); expect(resultObject.value).toBe(tagName);
}); });
it('should not display aliases when tag is starting with the same matched', () => {
const tagName = 'chest fluff';
const tagAlias = 'chest floof';
const tagCount = getRandomIntBetween(5, 10);
const prefix = 'ch';
const formatter = createLocalAutocompleteResultFormatter(prefix);
const resultObject = formatter({
name: tagName,
aliasName: tagAlias,
imageCount: tagCount,
});
expect(resultObject.label).toBe(`${tagName} (${tagCount})`);
});
it('should display aliases if matched prefix is different from the tag name', () => {
const tagName = 'queen chrysalis';
const tagAlias = 'chrysalis';
const tagCount = getRandomIntBetween(5, 10);
const prefix = 'ch';
const formatter = createLocalAutocompleteResultFormatter(prefix);
const resultObject = formatter({
name: tagName,
aliasName: tagAlias,
imageCount: tagCount,
});
expect(resultObject.label).toBe(`${tagAlias}${tagName} (${tagCount})`);
});
}); });
}); });

View file

@ -100,6 +100,13 @@ export class LocalAutocompleter {
return tagPointer; return tagPointer;
} }
/**
* Return whether the tag pointed to by the reference index is an alias.
*/
private tagReferenceIsAlias(i: TagReferenceIndex): boolean {
return this.view.getInt32(this.referenceStart + i * 8 + 4, true) < 0;
}
/** /**
* Get the images count for the given reference index. * Get the images count for the given reference index.
*/ */
@ -173,6 +180,7 @@ export class LocalAutocompleter {
getResult: (i: number) => TagReferenceIndex, getResult: (i: number) => TagReferenceIndex,
compare: (result: TagReferenceIndex) => number, compare: (result: TagReferenceIndex) => number,
hasFilteredAssociation: (result: TagReferenceIndex) => boolean, hasFilteredAssociation: (result: TagReferenceIndex) => boolean,
isAlias: (result: TagReferenceIndex) => boolean,
results: UniqueHeap<TagReferenceIndex>, results: UniqueHeap<TagReferenceIndex>,
) { ) {
const filter = !store.get('unfilter_tag_suggestions'); const filter = !store.get('unfilter_tag_suggestions');
@ -207,7 +215,7 @@ export class LocalAutocompleter {
} }
// Nothing was filtered, so add // Nothing was filtered, so add
results.append(referenceIndex); results.append(referenceIndex, !isAlias(referenceIndex));
} }
} }
@ -230,18 +238,19 @@ export class LocalAutocompleter {
// Set up filter context // Set up filter context
const hiddenTags = new Set(window.booru.hiddenTagList); const hiddenTags = new Set(window.booru.hiddenTagList);
const hasFilteredAssociation = this.isFilteredByReference.bind(this, hiddenTags); const hasFilteredAssociation = this.isFilteredByReference.bind(this, hiddenTags);
const isAlias = this.tagReferenceIsAlias.bind(this);
// Find tags ordered by full name // Find tags ordered by full name
const prefixMatch = (i: TagReferenceIndex) => const prefixMatch = (i: TagReferenceIndex) =>
strcmp(this.referenceToName(i, false).slice(0, prefix.length), prefix); strcmp(this.referenceToName(i, false).slice(0, prefix.length), prefix);
const referenceToNameIndex = (i: number) => i; const referenceToNameIndex = (i: number) => i;
this.scanResults(referenceToNameIndex, prefixMatch, hasFilteredAssociation, results); this.scanResults(referenceToNameIndex, prefixMatch, hasFilteredAssociation, isAlias, results);
// Find tags ordered by name in namespace // Find tags ordered by name in namespace
const namespaceMatch = (i: TagReferenceIndex) => const namespaceMatch = (i: TagReferenceIndex) =>
strcmp(nameInNamespace(this.referenceToName(i, false)).slice(0, prefix.length), prefix); strcmp(nameInNamespace(this.referenceToName(i, false)).slice(0, prefix.length), prefix);
const referenceToAliasIndex = this.getSecondaryResultAt.bind(this); const referenceToAliasIndex = this.getSecondaryResultAt.bind(this);
this.scanResults(referenceToAliasIndex, namespaceMatch, hasFilteredAssociation, results); this.scanResults(referenceToAliasIndex, namespaceMatch, hasFilteredAssociation, isAlias, results);
// Convert top K from heap into result array // Convert top K from heap into result array
return results.topK(k).map((i: TagReferenceIndex) => ({ return results.topK(k).map((i: TagReferenceIndex) => ({

View file

@ -176,17 +176,15 @@ export async function fetchLocalAutocomplete(): Promise<LocalAutocompleter> {
.then(buf => new LocalAutocompleter(buf)); .then(buf => new LocalAutocompleter(buf));
} }
export function createLocalAutocompleteResultFormatter(matchedPrefix?: string): (result: Result) => TermSuggestion { export function formatLocalAutocompleteResult(result: Result): TermSuggestion {
return result => { let tagName = result.name;
let tagName = result.name;
if (tagName !== result.aliasName && (!matchedPrefix || !tagName.startsWith(matchedPrefix))) { if (tagName !== result.aliasName) {
tagName = `${result.aliasName}${tagName}`; tagName = `${result.aliasName}${tagName}`;
} }
return { return {
value: result.name, value: result.name,
label: `${tagName} (${result.imageCount})`, label: `${tagName} (${result.imageCount})`,
};
}; };
} }

View file

@ -1,28 +1,34 @@
export type Compare<T> = (a: T, b: T) => number; export type Compare<T> = (a: T, b: T) => number;
export type Unique<T> = (a: T) => unknown; export type Unique<T> = (a: T) => unknown;
export type Collection<T> = { [index: number]: T; length: number }; export type Collection<T> = {
[index: number]: T;
length: number;
};
export class UniqueHeap<T> { export class UniqueHeap<T> {
private keys: Set<unknown>; private keys: Map<unknown, number>;
private values: Collection<T>; private values: Collection<T>;
private length: number; private length: number;
private compare: Compare<T>; private compare: Compare<T>;
private unique: Unique<T>; private unique: Unique<T>;
constructor(compare: Compare<T>, unique: Unique<T>, values: Collection<T>) { constructor(compare: Compare<T>, unique: Unique<T>, values: Collection<T>) {
this.keys = new Set(); this.keys = new Map();
this.values = values; this.values = values;
this.length = 0; this.length = 0;
this.compare = compare; this.compare = compare;
this.unique = unique; this.unique = unique;
} }
append(value: T) { append(value: T, forceReplace: boolean = false) {
const key = this.unique(value); const key = this.unique(value);
const prevIndex = this.keys.get(key);
if (!this.keys.has(key)) { if (typeof prevIndex === 'undefined') {
this.keys.add(key); this.keys.set(key, this.length);
this.values[this.length++] = value; this.values[this.length++] = value;
} else if (forceReplace) {
this.values[prevIndex] = value;
} }
} }