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

Commit

Permalink
Merge pull request #466 from aviraldg/feature-autocomplete-behaviour
Browse files Browse the repository at this point in the history
Improve autocomplete behaviour
  • Loading branch information
ara4n authored Sep 13, 2016
2 parents c8def54 + 79e5e6f commit 8bb9422
Show file tree
Hide file tree
Showing 16 changed files with 449 additions and 220 deletions.
16 changes: 12 additions & 4 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,26 @@
/** react **/

// bind or arrow function in props causes performance issues
"react/jsx-no-bind": ["error"],
"react/jsx-no-bind": ["error", {
"ignoreRefs": true
}],
"react/jsx-key": ["error"],
"react/prefer-stateless-function": ["warn"],
"react/sort-comp": ["warn"],

/** flowtype **/
"flowtype/require-parameter-type": 1,
"flowtype/require-parameter-type": [
1,
{
"excludeArrowFunctions": true
}
],
"flowtype/define-flow-type": 1,
"flowtype/require-return-type": [
1,
"always",
{
"annotateUndefined": "never"
"annotateUndefined": "never",
"excludeArrowFunctions": true
}
],
"flowtype/space-after-type-colon": [
Expand Down
6 changes: 6 additions & 0 deletions code_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,11 @@ React
<Foo onClick={this.doStuff}> // Better
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
```
Not doing so is acceptable in a single case; in function-refs:
```jsx
<Foo ref={(self) => this.component = self}>
```
- Think about whether your component really needs state: are you duplicating
information in component state that could be derived from the model?
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"babel-loader": "^5.4.0",
"babel-polyfill": "^6.5.0",
"eslint": "^2.13.1",
"eslint-plugin-flowtype": "^2.3.0",
"eslint-plugin-react": "^5.2.2",
"eslint-plugin-flowtype": "^2.17.0",
"eslint-plugin-react": "^6.2.1",
"expect": "^1.16.0",
"json-loader": "^0.5.3",
"karma": "^0.13.22",
Expand Down
3 changes: 2 additions & 1 deletion src/RichText.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import * as sdk from './index';
import * as emojione from 'emojione';
import {stateToHTML} from 'draft-js-export-html';
import {SelectionRange} from "./autocomplete/Autocompleter";

const MARKDOWN_REGEX = {
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
Expand Down Expand Up @@ -203,7 +204,7 @@ export function selectionStateToTextOffsets(selectionState: SelectionState,
};
}

export function textOffsetsToSelectionState({start, end}: {start: number, end: number},
export function textOffsetsToSelectionState({start, end}: SelectionRange,
contentBlocks: Array<ContentBlock>): SelectionState {
let selectionState = SelectionState.createEmpty();

Expand Down
12 changes: 12 additions & 0 deletions src/SlashCommands.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
var MatrixClientPeg = require("./MatrixClientPeg");
var dis = require("./dispatcher");
var Tinter = require("./Tinter");
import sdk from './index';
import Modal from './Modal';


class Command {
Expand Down Expand Up @@ -56,6 +58,16 @@ var success = function(promise) {
};

var commands = {
ddg: new Command("ddg", "<query>", function(roomId, args) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here.
Modal.createDialog(ErrorDialog, {
title: "/ddg is not a command",
description: "To use it, just wait for autocomplete results to load and tab through them.",
});
return success();
}),

// Change your nickname
nick: new Command("nick", "<display_name>", function(room_id, args) {
if (args) {
Expand Down
30 changes: 20 additions & 10 deletions src/autocomplete/AutocompleteProvider.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Q from 'q';
import React from 'react';
import type {Completion, SelectionRange} from './Autocompleter';

export default class AutocompleteProvider {
constructor(commandRegex?: RegExp, fuseOpts?: any) {
if(commandRegex) {
if(!commandRegex.global) {
if (commandRegex) {
if (!commandRegex.global) {
throw new Error('commandRegex must have global flag set');
}
this.commandRegex = commandRegex;
Expand All @@ -14,18 +14,23 @@ 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}): ?Array<string> {
if (this.commandRegex == null) {
getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string {
let commandRegex = this.commandRegex;

if (force && this.shouldForceComplete()) {
commandRegex = /[^\W]+/g;
}

if (commandRegex == null) {
return null;
}

this.commandRegex.lastIndex = 0;
commandRegex.lastIndex = 0;

let match;
while ((match = this.commandRegex.exec(query)) != null) {
while ((match = commandRegex.exec(query)) != null) {
let matchStart = match.index,
matchEnd = matchStart + match[0].length;

if (selection.start <= matchEnd && selection.end >= matchStart) {
return {
command: match,
Expand All @@ -45,8 +50,8 @@ export default class AutocompleteProvider {
};
}

getCompletions(query: string, selection: {start: number, end: number}) {
return Q.when([]);
async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
return [];
}

getName(): string {
Expand All @@ -57,4 +62,9 @@ export default class AutocompleteProvider {
console.error('stub; should be implemented in subclasses');
return null;
}

// Whether we should provide completions even if triggered forcefully, without a sigil.
shouldForceComplete(): boolean {
return false;
}
}
59 changes: 50 additions & 9 deletions src/autocomplete/Autocompleter.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,63 @@
// @flow

import type {Component} from 'react';
import CommandProvider from './CommandProvider';
import DuckDuckGoProvider from './DuckDuckGoProvider';
import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider';
import Q from 'q';

export type SelectionRange = {
start: number,
end: number
};

export type Completion = {
completion: string,
component: ?Component,
range: SelectionRange,
command: ?string,
};

const PROVIDERS = [
UserProvider,
CommandProvider,
DuckDuckGoProvider,
RoomProvider,
EmojiProvider,
CommandProvider,
DuckDuckGoProvider,
].map(completer => completer.getInstance());

export function getCompletions(query: string, selection: {start: number, end: number}) {
return PROVIDERS.map(provider => {
return {
completions: provider.getCompletions(query, selection),
provider,
};
});
// Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000;

export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
/* Note: That this waits for all providers to return is *intentional*
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
It ends up containing a list of Q promise states, which are objects with
state (== "fulfilled" || "rejected") and value. */
const completionsList = await Q.allSettled(
PROVIDERS.map(provider => {
return Q(provider.getCompletions(query, selection, force))
.timeout(PROVIDER_COMPLETION_TIMEOUT);
})
);

return completionsList
.filter(completion => completion.state === "fulfilled")
.map((completionsState, i) => {
return {
completions: completionsState.value,
provider: PROVIDERS[i],

/* the currently matched "command" the completer tried to complete
* we pass this through so that Autocomplete can figure out when to
* re-show itself once hidden.
*/
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
};
});
}
12 changes: 8 additions & 4 deletions src/autocomplete/CommandProvider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import Fuse from 'fuse.js';
import {TextualCompletion} from './Components';

Expand All @@ -23,7 +22,7 @@ const COMMANDS = [
{
command: '/invite',
args: '<user-id>',
description: 'Invites user with given id to current room'
description: 'Invites user with given id to current room',
},
{
command: '/join',
Expand All @@ -40,6 +39,11 @@ const COMMANDS = [
args: '<display-name>',
description: 'Changes your display nickname',
},
{
command: '/ddg',
args: '<query>',
description: 'Searches DuckDuckGo for results',
}
];

let COMMAND_RE = /(^\/\w*)/g;
Expand All @@ -54,7 +58,7 @@ export default class CommandProvider extends AutocompleteProvider {
});
}

getCompletions(query: string, selection: {start: number, end: number}) {
async getCompletions(query: string, selection: {start: number, end: number}) {
let completions = [];
let {command, range} = this.getCurrentCommand(query, selection);
if (command) {
Expand All @@ -70,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider {
};
});
}
return Q.when(completions);
return completions;
}

getName() {
Expand Down
97 changes: 47 additions & 50 deletions src/autocomplete/DuckDuckGoProvider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import 'whatwg-fetch';

import {TextualCompletion} from './Components';
Expand All @@ -20,61 +19,59 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
}

getCompletions(query: string, selection: {start: number, end: number}) {
async getCompletions(query: string, selection: {start: number, end: number}) {
let {command, range} = this.getCurrentCommand(query, selection);
if (!query || !command) {
return Q.when([]);
return [];
}

return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
const response = await fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
method: 'GET',
})
.then(response => response.json())
.then(json => {
let results = json.Results.map(result => {
return {
completion: result.Text,
component: (
<TextualCompletion
title={result.Text}
description={result.Result} />
),
range,
};
});
if (json.Answer) {
results.unshift({
completion: json.Answer,
component: (
<TextualCompletion
title={json.Answer}
description={json.AnswerType} />
),
range,
});
}
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
results.unshift({
completion: json.RelatedTopics[0].Text,
component: (
<TextualCompletion
title={json.RelatedTopics[0].Text} />
),
range,
});
}
if (json.AbstractText) {
results.unshift({
completion: json.AbstractText,
component: (
<TextualCompletion
title={json.AbstractText} />
),
range,
});
}
return results;
});
const json = await response.json();
let results = json.Results.map(result => {
return {
completion: result.Text,
component: (
<TextualCompletion
title={result.Text}
description={result.Result} />
),
range,
};
});
if (json.Answer) {
results.unshift({
completion: json.Answer,
component: (
<TextualCompletion
title={json.Answer}
description={json.AnswerType} />
),
range,
});
}
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
results.unshift({
completion: json.RelatedTopics[0].Text,
component: (
<TextualCompletion
title={json.RelatedTopics[0].Text} />
),
range,
});
}
if (json.AbstractText) {
results.unshift({
completion: json.AbstractText,
component: (
<TextualCompletion
title={json.AbstractText} />
),
range,
});
}
return results;
}

getName() {
Expand Down
Loading

0 comments on commit 8bb9422

Please sign in to comment.