autocomplete polishing

* suppress autocomplete when navigating through history
* only search for slashcommands if in the first block of the editor
* handle suffix returns from providers correctly
* fix SelectionRange typing in the providers
* fix bugs when pressing ctrl-a, typing and then tab to complete a replacement by collapsing selection to anchor when inserting a completion in the editor
* fix https://github.com/vector-im/riot-web/issues/4762
pull/21833/head
Matthew Hodgson 2018-05-13 03:04:40 +01:00
parent 877a6195ae
commit c967ecc4e5
7 changed files with 31 additions and 28 deletions

View File

@ -20,13 +20,19 @@ import React from 'react';
import type {Completion, SelectionRange} from './Autocompleter';
export default class AutocompleteProvider {
constructor(commandRegex?: RegExp) {
constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
if (commandRegex) {
if (!commandRegex.global) {
throw new Error('commandRegex must have global flag set');
}
this.commandRegex = commandRegex;
}
if (forcedCommandRegex) {
if (!forcedCommandRegex.global) {
throw new Error('forcedCommandRegex must have global flag set');
}
this.forcedCommandRegex = forcedCommandRegex;
}
}
destroy() {
@ -36,11 +42,11 @@ export default class AutocompleteProvider {
/**
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
*/
getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string {
getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false): ?string {
let commandRegex = this.commandRegex;
if (force && this.shouldForceComplete()) {
commandRegex = /\S+/g;
commandRegex = this.forcedCommandRegex || /\S+/g;
}
if (commandRegex == null) {

View File

@ -27,6 +27,7 @@ import NotifProvider from './NotifProvider';
import Promise from 'bluebird';
export type SelectionRange = {
beginning: boolean,
start: number,
end: number
};
@ -77,12 +78,12 @@ export default class Autocompleter {
// Array of inspections of promises that might timeout. Instead of allowing a
// single timeout to reject the Promise.all, reflect each one and once they've all
// settled, filter for the fulfilled ones
this.providers.map((provider) => {
return provider
this.providers.map(provider =>
provider
.getCompletions(query, selection, force)
.timeout(PROVIDER_COMPLETION_TIMEOUT)
.reflect();
}),
.reflect()
),
);
return completionsList.filter(

View File

@ -21,6 +21,7 @@ import { _t, _td } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components';
import type {SelectionRange} from './Autocompleter';
// TODO merge this with the factory mechanics of SlashCommands?
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
@ -123,8 +124,9 @@ export default class CommandProvider extends AutocompleteProvider {
});
}
async getCompletions(query: string, selection: {start: number, end: number}) {
async getCompletions(query: string, selection: SelectionRange) {
let completions = [];
if (!selection.beginning) return completions;
const {command, range} = this.getCurrentCommand(query, selection);
if (command) {
completions = this.matcher.match(command[0]).map((result) => {

View File

@ -22,6 +22,7 @@ import AutocompleteProvider from './AutocompleteProvider';
import 'whatwg-fetch';
import {TextualCompletion} from './Components';
import type {SelectionRange} from './Autocompleter';
const DDG_REGEX = /\/ddg\s+(.+)$/g;
const REFERRER = 'vector';
@ -36,7 +37,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
}
async getCompletions(query: string, selection: {start: number, end: number}) {
async getCompletions(query: string, selection: SelectionRange) {
const {command, range} = this.getCurrentCommand(query, selection);
if (!query || !command) {
return [];

View File

@ -20,6 +20,7 @@ import { _t } from '../languageHandler';
import MatrixClientPeg from '../MatrixClientPeg';
import {PillCompletion} from './Components';
import sdk from '../index';
import type {SelectionRange} from './Autocompleter';
const AT_ROOM_REGEX = /@\S*/g;
@ -29,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider {
this.room = room;
}
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
async getCompletions(query: string, selection: SelectionRange, force = false) {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get();

View File

@ -26,6 +26,7 @@ import {getDisplayAliasForRoom} from '../Rooms';
import sdk from '../index';
import _sortBy from 'lodash/sortBy';
import {makeRoomPermalink} from "../matrix-to";
import type {SelectionRange} from './Autocompleter';
const ROOM_REGEX = /(?=#)(\S*)/g;
@ -46,15 +47,9 @@ export default class RoomProvider extends AutocompleteProvider {
});
}
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
async getCompletions(query: string, selection: SelectionRange, force = false) {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
// Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/riot-web/issues/4762)
if (/^(\/join|\/leave)/.test(query)) {
return [];
}
const client = MatrixClientPeg.get();
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection, force);

View File

@ -28,18 +28,21 @@ import _sortBy from 'lodash/sortBy';
import MatrixClientPeg from '../MatrixClientPeg';
import type {Room, RoomMember} from 'matrix-js-sdk';
import type {SelectionRange} from './Autocompleter';
import {makeUserPermalink} from "../matrix-to";
const USER_REGEX = /@\S*/g;
// used when you hit 'tab' - we allow some separator chars at the beginning
// to allow you to tab-complete /mat into /(matthew)
const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g;
export default class UserProvider extends AutocompleteProvider {
users: Array<RoomMember> = null;
room: Room = null;
constructor(room) {
super(USER_REGEX, {
keys: ['name'],
});
super(USER_REGEX, FORCED_USER_REGEX);
this.room = room;
this.matcher = new FuzzyMatcher([], {
keys: ['name', 'userId'],
@ -87,15 +90,9 @@ export default class UserProvider extends AutocompleteProvider {
this.users = null;
}
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
async getCompletions(query: string, selection: SelectionRange, force = false) {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
// Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/riot-web/issues/4762)
if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) {
return [];
}
// lazy-load user list into matcher
if (this.users === null) this._makeUsers();
@ -114,7 +111,7 @@ export default class UserProvider extends AutocompleteProvider {
// relies on the length of the entity === length of the text in the decoration.
completion: user.rawDisplayName.replace(' (IRC)', ''),
completionId: user.userId,
suffix: range.start === 0 ? ': ' : ' ',
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
href: makeUserPermalink(user.userId),
component: (
<PillCompletion