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
This commit is contained in:
parent
877a6195ae
commit
c967ecc4e5
7 changed files with 31 additions and 28 deletions
|
@ -20,13 +20,19 @@ import React from 'react';
|
||||||
import type {Completion, SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp) {
|
constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) {
|
||||||
if (commandRegex) {
|
if (commandRegex) {
|
||||||
if (!commandRegex.global) {
|
if (!commandRegex.global) {
|
||||||
throw new Error('commandRegex must have global flag set');
|
throw new Error('commandRegex must have global flag set');
|
||||||
}
|
}
|
||||||
this.commandRegex = commandRegex;
|
this.commandRegex = commandRegex;
|
||||||
}
|
}
|
||||||
|
if (forcedCommandRegex) {
|
||||||
|
if (!forcedCommandRegex.global) {
|
||||||
|
throw new Error('forcedCommandRegex must have global flag set');
|
||||||
|
}
|
||||||
|
this.forcedCommandRegex = forcedCommandRegex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
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.
|
* 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;
|
let commandRegex = this.commandRegex;
|
||||||
|
|
||||||
if (force && this.shouldForceComplete()) {
|
if (force && this.shouldForceComplete()) {
|
||||||
commandRegex = /\S+/g;
|
commandRegex = this.forcedCommandRegex || /\S+/g;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandRegex == null) {
|
if (commandRegex == null) {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import NotifProvider from './NotifProvider';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
export type SelectionRange = {
|
export type SelectionRange = {
|
||||||
|
beginning: boolean,
|
||||||
start: number,
|
start: number,
|
||||||
end: number
|
end: number
|
||||||
};
|
};
|
||||||
|
@ -77,12 +78,12 @@ export default class Autocompleter {
|
||||||
// Array of inspections of promises that might timeout. Instead of allowing a
|
// 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
|
// single timeout to reject the Promise.all, reflect each one and once they've all
|
||||||
// settled, filter for the fulfilled ones
|
// settled, filter for the fulfilled ones
|
||||||
this.providers.map((provider) => {
|
this.providers.map(provider =>
|
||||||
return provider
|
provider
|
||||||
.getCompletions(query, selection, force)
|
.getCompletions(query, selection, force)
|
||||||
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||||
.reflect();
|
.reflect()
|
||||||
}),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return completionsList.filter(
|
return completionsList.filter(
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { _t, _td } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
import type {SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
// TODO merge this with the factory mechanics of SlashCommands?
|
// 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
|
// 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 = [];
|
let completions = [];
|
||||||
|
if (!selection.beginning) return completions;
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.matcher.match(command[0]).map((result) => {
|
completions = this.matcher.match(command[0]).map((result) => {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
import type {SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||||
const REFERRER = 'vector';
|
const REFERRER = 'vector';
|
||||||
|
@ -36,7 +37,7 @@ 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: {start: number, end: number}) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!query || !command) {
|
if (!query || !command) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { _t } from '../languageHandler';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
import type {SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
const AT_ROOM_REGEX = /@\S*/g;
|
const AT_ROOM_REGEX = /@\S*/g;
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
||||||
this.room = room;
|
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 RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeRoomPermalink} from "../matrix-to";
|
import {makeRoomPermalink} from "../matrix-to";
|
||||||
|
import type {SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
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');
|
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();
|
const client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
|
|
|
@ -28,18 +28,21 @@ import _sortBy from 'lodash/sortBy';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
import type {Room, RoomMember} from 'matrix-js-sdk';
|
import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||||
|
import type {SelectionRange} from './Autocompleter';
|
||||||
import {makeUserPermalink} from "../matrix-to";
|
import {makeUserPermalink} from "../matrix-to";
|
||||||
|
|
||||||
const USER_REGEX = /@\S*/g;
|
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 {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
users: Array<RoomMember> = null;
|
users: Array<RoomMember> = null;
|
||||||
room: Room = null;
|
room: Room = null;
|
||||||
|
|
||||||
constructor(room) {
|
constructor(room) {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, FORCED_USER_REGEX);
|
||||||
keys: ['name'],
|
|
||||||
});
|
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.matcher = new FuzzyMatcher([], {
|
this.matcher = new FuzzyMatcher([], {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name', 'userId'],
|
||||||
|
@ -87,15 +90,9 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
this.users = null;
|
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');
|
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
|
// lazy-load user list into matcher
|
||||||
if (this.users === null) this._makeUsers();
|
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.
|
// relies on the length of the entity === length of the text in the decoration.
|
||||||
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
||||||
completionId: user.userId,
|
completionId: user.userId,
|
||||||
suffix: range.start === 0 ? ': ' : ' ',
|
suffix: (selection.beginning && range.start === 0) ? ': ' : ' ',
|
||||||
href: makeUserPermalink(user.userId),
|
href: makeUserPermalink(user.userId),
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion
|
<PillCompletion
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue