Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Add a limit option for autocomplete results #6016

Merged
merged 5 commits into from
May 12, 2021
Merged
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
7 changes: 6 additions & 1 deletion src/autocomplete/AutocompleteProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
}

Expand Down
15 changes: 12 additions & 3 deletions src/autocomplete/Autocompleter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
otherwise, we run into a condition where new completions are displayed
while the user is interacting with the list, which makes it difficult
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
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(provider => {
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT);
const completionsList: ICompletion[][] = await Promise.all(this.providers.map(async provider => {
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
Expand Down
10 changes: 8 additions & 2 deletions src/autocomplete/CommandProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
if (!command) return [];

Expand All @@ -55,10 +60,11 @@ export default class CommandProvider extends AutocompleteProvider {
} else {
if (query === '/') {
// If they have just entered `/` show everything
// We exclude the limit on purpose to have a comprehensive list
matches = Commands;
} else {
// otherwise fuzzy match against all of the fields
matches = this.matcher.match(command[1]);
matches = this.matcher.match(command[1], limit);
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/autocomplete/CommunityProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

// Disable autocompletions when composing commands because of various issues
Expand Down Expand Up @@ -81,7 +86,7 @@ export default class CommunityProvider extends AutocompleteProvider {
this.matcher.setObjects(groups);

const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [
(c) => score(matchedString, c.groupId),
(c) => c.groupId.length,
Expand Down
10 changes: 8 additions & 2 deletions src/autocomplete/DuckDuckGoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
+ `&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);
if (!query || !command) {
return [];
Expand All @@ -46,7 +51,8 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
method: 'GET',
});
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 {
completion: result.Text,
component: (
Expand Down
9 changes: 7 additions & 2 deletions src/autocomplete/EmojiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
return []; // don't give any suggestions if the user doesn't want them
}
Expand All @@ -93,7 +98,7 @@ export default class EmojiProvider extends AutocompleteProvider {
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
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'
completions = completions.concat(this.nameMatcher.match(matchedString));
Expand Down
7 changes: 6 additions & 1 deletion src/autocomplete/NotifProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ export default class NotifProvider extends AutocompleteProvider {
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 client = MatrixClientPeg.get();
Expand Down
7 changes: 5 additions & 2 deletions src/autocomplete/QueryMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
if (this._options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, '');
Expand Down Expand Up @@ -129,7 +129,10 @@ export default class QueryMatcher<T extends Object> {
});

// 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 {
Expand Down
9 changes: 7 additions & 2 deletions src/autocomplete/RoomProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 client = MatrixClientPeg.get();
Expand Down Expand Up @@ -90,7 +95,7 @@ export default class RoomProvider extends AutocompleteProvider {

this.matcher.setObjects(matcherObjects);
const matchedString = command[0];
completions = this.matcher.match(matchedString);
completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [
(c) => score(matchedString, c.displayedAlias),
(c) => c.displayedAlias.length,
Expand Down
9 changes: 7 additions & 2 deletions src/autocomplete/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ export default class UserProvider extends AutocompleteProvider {
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');

// lazy-load user list into matcher
Expand All @@ -118,7 +123,7 @@ export default class UserProvider extends AutocompleteProvider {
if (fullMatch && fullMatch !== '@') {
// 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;
completions = this.matcher.match(query).map((user) => {
completions = this.matcher.match(query, limit).map((user) => {
const displayName = (user.name || user.userId || '');
return {
// Length of completion should equal length of text in decorator. draft-js
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/rooms/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
import {replaceableComponent} from "../../../utils/replaceableComponent";

const COMPOSER_SELECTED = 0;
const MAX_PROVIDER_MATCHES = 20;

export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;

Expand Down Expand Up @@ -136,7 +137,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {

processQuery(query: string, selection: ISelectionRange) {
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete,
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {
Expand Down