From c967ecc4e59cc475feaed9f36a6f447fa327b139 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 13 May 2018 03:04:40 +0100 Subject: [PATCH] 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 --- src/autocomplete/AutocompleteProvider.js | 12 +++++++++--- src/autocomplete/Autocompleter.js | 9 +++++---- src/autocomplete/CommandProvider.js | 4 +++- src/autocomplete/DuckDuckGoProvider.js | 3 ++- src/autocomplete/NotifProvider.js | 3 ++- src/autocomplete/RoomProvider.js | 9 ++------- src/autocomplete/UserProvider.js | 19 ++++++++----------- 7 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index c93ae4fb2a..2d3bad14c5 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -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) { diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 3d30363d9f..db3ba5a7e1 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -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( diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index e33fa7861f..0545095cf0 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -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) => { diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 68d4915f56..236ab49b62 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -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 []; diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js index 5d2f05ecb9..a426528567 100644 --- a/src/autocomplete/NotifProvider.js +++ b/src/autocomplete/NotifProvider.js @@ -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(); diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b9346e75cc..139ac87041 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -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); diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index a6dd13051b..c25dad8877 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -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 = 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: (