Add a limit option for autocomplete results (#6016)

This commit is contained in:
Germain 2021-05-12 12:18:56 +01:00 committed by GitHub
parent eea6ba6825
commit bd2917aa69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 75 additions and 20 deletions

View file

@ -93,7 +93,12 @@ export default class AutocompleteProvider {
}; };
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
return []; return [];
} }

View file

@ -82,15 +82,24 @@ export default class Autocompleter {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<IProviderCompletions[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<IProviderCompletions[]> {
/* Note: This intentionally waits for all providers to return, /* Note: This intentionally waits for all providers to return,
otherwise, we run into a condition where new completions are displayed otherwise, we run into a condition where new completions are displayed
while the user is interacting with the list, which makes it difficult while the user is interacting with the list, which makes it difficult
to predict whether an action will actually do what is intended to predict whether an action will actually do what is intended
*/ */
// list of results from each provider, each being a list of completions or null if it times out // list of results from each provider, each being a list of completions or null if it times out
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(provider => { const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); return await timeout(
provider.getCompletions(query, selection, force, limit),
null,
PROVIDER_COMPLETION_TIMEOUT,
);
})); }));
// map then filter to maintain the index for the map-operation, for this.providers to line up // map then filter to maintain the index for the map-operation, for this.providers to line up

View file

@ -38,7 +38,12 @@ export default class CommandProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force?: boolean,
limit = -1,
): Promise<ICompletion[]> {
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (!command) return []; if (!command) return [];
@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider {
} else { } else {
if (query === '/') { if (query === '/') {
// If they have just entered `/` show everything // If they have just entered `/` show everything
// We exclude the limit on purpose to have a comprehensive list
matches = Commands; matches = Commands;
} else { } else {
// otherwise fuzzy match against all of the fields // otherwise fuzzy match against all of the fields
matches = this.matcher.match(command[1]); matches = this.matcher.match(command[1], limit);
} }
} }

View file

@ -50,7 +50,12 @@ export default class CommunityProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
// Disable autocompletions when composing commands because of various issues // Disable autocompletions when composing commands because of various issues
@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider {
this.matcher.setObjects(groups); this.matcher.setObjects(groups);
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString); completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [ completions = sortBy(completions, [
(c) => score(matchedString, c.groupId), (c) => score(matchedString, c.groupId),
(c) => c.groupId.length, (c) => c.groupId.length,

View file

@ -36,7 +36,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
} }
async getCompletions(query: string, selection: ISelectionRange, force= false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (!query || !command) { if (!query || !command) {
return []; return [];
@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
method: 'GET', method: 'GET',
}); });
const json = await response.json(); const json = await response.json();
const results = json.Results.map((result) => { const maxLength = limit > -1 ? limit : json.Results.length;
const results = json.Results.slice(0, maxLength).map((result) => {
return { return {
completion: result.Text, completion: result.Text,
component: ( component: (

View file

@ -84,7 +84,12 @@ export default class EmojiProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force?: boolean): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force?: boolean,
limit = -1,
): Promise<ICompletion[]> {
if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) {
return []; // don't give any suggestions if the user doesn't want them return []; // don't give any suggestions if the user doesn't want them
} }
@ -93,7 +98,7 @@ export default class EmojiProvider extends AutocompleteProvider {
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (command) { if (command) {
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString); completions = this.matcher.match(matchedString, limit);
// Do second match with shouldMatchWordsOnly in order to match against 'name' // Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString)); completions = completions.concat(this.nameMatcher.match(matchedString));

View file

@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider {
this.room = room; this.room = room;
} }
async getCompletions(query: string, selection: ISelectionRange, force= false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();

View file

@ -87,7 +87,7 @@ export default class QueryMatcher<T extends Object> {
} }
} }
match(query: string): T[] { match(query: string, limit = -1): T[] {
query = this.processQuery(query); query = this.processQuery(query);
if (this._options.shouldMatchWordsOnly) { if (this._options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, ''); query = query.replace(/[^\w]/g, '');
@ -129,7 +129,10 @@ export default class QueryMatcher<T extends Object> {
}); });
// Now map the keys to the result objects. Also remove any duplicates. // Now map the keys to the result objects. Also remove any duplicates.
return uniq(matches.map((match) => match.object)); const dedupped = uniq(matches.map((match) => match.object));
const maxLength = limit === -1 ? dedupped.length : limit;
return dedupped.slice(0, maxLength);
} }
private processQuery(query: string): string { private processQuery(query: string): string {

View file

@ -58,7 +58,12 @@ export default class RoomProvider extends AutocompleteProvider {
}); });
} }
async getCompletions(query: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
@ -90,7 +95,7 @@ export default class RoomProvider extends AutocompleteProvider {
this.matcher.setObjects(matcherObjects); this.matcher.setObjects(matcherObjects);
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString); completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [ completions = sortBy(completions, [
(c) => score(matchedString, c.displayedAlias), (c) => score(matchedString, c.displayedAlias),
(c) => c.displayedAlias.length, (c) => c.displayedAlias.length,

View file

@ -102,7 +102,12 @@ export default class UserProvider extends AutocompleteProvider {
this.users = null; this.users = null;
}; };
async getCompletions(rawQuery: string, selection: ISelectionRange, force = false): Promise<ICompletion[]> { async getCompletions(
rawQuery: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
// lazy-load user list into matcher // lazy-load user list into matcher
@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider {
if (fullMatch && fullMatch !== '@') { if (fullMatch && fullMatch !== '@') {
// Don't include the '@' in our search query - it's only used as a way to trigger completion // Don't include the '@' in our search query - it's only used as a way to trigger completion
const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch; const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch;
completions = this.matcher.match(query).map((user) => { completions = this.matcher.match(query, limit).map((user) => {
const displayName = (user.name || user.userId || ''); const displayName = (user.name || user.userId || '');
return { return {
// Length of completion should equal length of text in decorator. draft-js // Length of completion should equal length of text in decorator. draft-js

View file

@ -26,6 +26,7 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
const COMPOSER_SELECTED = 0; const COMPOSER_SELECTED = 0;
const MAX_PROVIDER_MATCHES = 20;
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`; export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
@ -136,7 +137,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
processQuery(query: string, selection: ISelectionRange) { processQuery(query: string, selection: ISelectionRange) {
return this.autocompleter.getCompletions( return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete, query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
).then((completions) => { ).then((completions) => {
// Only ever process the completions for the most recent query being processed // Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) { if (query !== this.queryRequested) {