Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/search/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('autocompleteTermSuggestions', () => {
['not(', 'Expected failure'],
['memento:', 'memento:any'],
['foo memento:', 'foo memento:any'],
['dupe:stats shot', 'dupe:stats is:shotgun'],
];

const plainStringCases: [query: string, mockCandidate: string][] = [['jotu', 'jötunn']];
Expand Down
36 changes: 28 additions & 8 deletions src/app/search/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Search } from '@destinyitemmanager/dim-api-types';
import { compact, filterMap, uniqBy } from 'app/utils/collections';
import { chainComparator, compareBy, reverseComparator } from 'app/utils/comparators';
import { partition } from 'es-toolkit';
import { ArmoryEntry, getArmorySuggestions } from './armory-search';
import { filterDescriptionText } from './filter-description';
import { canonicalFilterFormats } from './filter-types';
Expand Down Expand Up @@ -253,11 +254,15 @@
*
* @returns the start indexes of various points that could be incomplete filters
*/
function findLastFilter(queryUpToCaret: string): number[] | null {
function findLastFilter<I, FilterCtx, SuggestionsCtx>(
queryUpToCaret: string,
searchConfig: SearchConfig<I, FilterCtx, SuggestionsCtx>,
): number[] | null {
// Find the indexes where any incomplete filter starts. For example if the query is:
// name:"foo" bar baz
// then the open keywords are "bar baz" and "baz"
let incompleteFilterIndices: number[] = [];
let lastWasFreeform = false;
try {
// We can use the query lexer for this to scan through tokens in the query without parsing the whole AST.
for (const token of lexer(queryUpToCaret)) {
Expand All @@ -268,7 +273,14 @@
// Ignore complete quoted tokens, they're definitively finished.
!token.quoted
) {
// If the last filter wasn't freeform, close it off and start a new one.
if (!lastWasFreeform) {
incompleteFilterIndices = [];
}
incompleteFilterIndices.push(token.startIndex);
lastWasFreeform = canonicalFilterFormats(
searchConfig.filtersMap.kvFilters[token.keyword]?.format,
).includes('freeform');
} else {
incompleteFilterIndices = [];
}
Expand Down Expand Up @@ -313,7 +325,7 @@
caretIndex = (caretEndRegex.exec(query.slice(caretIndex))?.index || 0) + caretIndex;

const queryUpToCaret = query.slice(0, caretIndex);
const lastFilters = findLastFilter(queryUpToCaret);
const lastFilters = findLastFilter(queryUpToCaret, searchConfig);
if (!lastFilters) {
return [];
}
Expand Down Expand Up @@ -386,6 +398,7 @@
}
}
}
const searchCollator = cachedSearchCollator(searchConfig.language);

Check failure on line 401 in src/app/search/autocomplete.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe call of a type that could not be resolved

Check failure on line 401 in src/app/search/autocomplete.ts

View workflow job for this annotation

GitHub Actions / lint

'searchCollator' is assigned a value but never used. Allowed unused vars must match /^_./u

Check failure on line 401 in src/app/search/autocomplete.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an error typed value

// TODO: also search filter descriptions
return (typed: string): string[] => {
Expand Down Expand Up @@ -421,6 +434,7 @@
// and "stat" matches "stat:" and "basestat:"
const matchType = !mustStartWith && typedPlain.includes(':') ? 'startsWith' : 'includes';

// TODO: Instead of using the plaintext, use the intl cached search collator?
let suggestions = searchConfig.suggestions
.filter(
(word) => word.plainText.startsWith(mustStartWith) && word.plainText[matchType](typedPlain),
Expand All @@ -429,19 +443,25 @@

// TODO: sort this first?? it depends on term in one place

// TODO: We want to support is:gun autocompleting to is:shotgun

if (multiqueryTermsLookup[possibleKeyword] && filterNames.includes(possibleKeyword)) {
// For multiquery filters, if the user has typed a + (or hasn't typed
// anything) they're looking to add another query term, so offer to append
// one.
const existingTerms = new Set(
(typedSegments[1] || '')
.split('+')
.filter((t) => multiqueryTermsLookup[possibleKeyword]!.includes(t)),
const typedArgs = (typedSegments[1] || '').split('+');
// Existing, complete terms
const [existingTerms, possiblyIncompleteTerms] = partition(typedArgs, (t) =>
multiqueryTermsLookup[possibleKeyword]!.includes(t),
);
const stem = `${typedSegments[0]}:${[...existingTerms].join('+')}${existingTerms.size ? '+' : ''}`;
const stem = `${typedSegments[0]}:${[...existingTerms].join('+')}${existingTerms.length ? '+' : ''}`;
suggestions.push(
...filterMap(multiqueryTermsLookup[possibleKeyword], (t) => {
if (!existingTerms.has(t)) {
if (
!existingTerms.includes(t) &&
(possiblyIncompleteTerms.length === 0 ||
possiblyIncompleteTerms.some((arg) => t.includes(arg)))
) {
const newTerm = stem + t;
return {
rawText: newTerm,
Expand Down
1 change: 1 addition & 0 deletions src/app/search/search-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface FiltersMap<I, FilterCtx, SuggestionsCtx> {
kvFilters: Record<string, FilterDefinition<I, FilterCtx, SuggestionsCtx>>;
}

// TODO: Let's not compute both rawText and plainText until we need both. Maybe we can use an i18n comparison instead?
export interface Suggestion {
/** The original suggestion text. */
rawText: string;
Expand Down
Loading