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

Better autocomplete #296

Merged
merged 16 commits into from
Jul 4, 2016
Merged
Show file tree
Hide file tree
Changes from 3 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 .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@
"no-new-wrappers": ["error"],
"no-invalid-regexp": ["error"],
"no-extra-bind": ["error"],
"no-magic-numbers": ["error"],
"no-magic-numbers": ["error", {
"ignore": [-1, 0, 1], // usually used in array/string indexing
"ignoreArrayIndexes": true,
"enforceConst": true,
"detectObjects": true
}],
"consistent-return": ["error"],
"valid-jsdoc": ["error"],
"no-use-before-define": ["error"],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"glob": "^5.0.14",
"highlight.js": "^8.9.1",
"linkifyjs": "^2.0.0-beta.4",
"lodash": "^4.13.1",
"marked": "^0.3.5",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
Expand Down
63 changes: 55 additions & 8 deletions src/RichText.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';
import {
Editor,
Modifier,
ContentState,
ContentBlock,
convertFromHTML,
DefaultDraftBlockRenderMap,
DefaultDraftInlineStyle,
CompositeDecorator,
SelectionState
SelectionState,
} from 'draft-js';
import * as sdk from './index';
import * as emojione from 'emojione';

const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
element: 'span'
Expand All @@ -24,7 +27,7 @@ const STYLES = {
CODE: 'code',
ITALIC: 'em',
STRIKETHROUGH: 's',
UNDERLINE: 'u'
UNDERLINE: 'u',
};

const MARKDOWN_REGEX = {
Expand All @@ -35,6 +38,8 @@ const MARKDOWN_REGEX = {

const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
let EMOJI_REGEX = null;
window.EMOJI_REGEX = EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be added to window? And why the different one in src/autocomplete/EmojiProvider.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While putting it on window looks like debugging code, this regexp is different (should be) different from the one in EmojiProvider since this handles rendering of unicode emoji in the message composer (a feature which is disabled for now due to bugs in Draft.js)


export function contentStateToHTML(contentState: ContentState): string {
return contentState.getBlockMap().map((block) => {
Expand Down Expand Up @@ -89,6 +94,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
return <span className="mx_UserPill">{avatar} {props.children}</span>;
}
};

let roomDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(ROOM_REGEX, contentBlock, callback);
Expand All @@ -98,6 +104,16 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
}
};

// Unused for now, due to https://github.com/facebook/draft-js/issues/414
let emojiDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(EMOJI_REGEX, contentBlock, callback);
},
component: (props) => {
return <span dangerouslySetInnerHTML={{__html: ' ' + emojione.unicodeToImage(props.children[0].props.text)}}/>
}
};

return [usernameDecorator, roomDecorator];
}

Expand Down Expand Up @@ -154,7 +170,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
text = "";


for(let currentKey = startKey;
for (let currentKey = startKey;
currentKey && currentKey !== endKey;
currentKey = contentState.getKeyAfter(currentKey)) {
let blockText = getText(currentKey);
Expand All @@ -175,14 +191,14 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
* Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
* Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
*/
export function getTextSelectionOffsets(selectionState: SelectionState,
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
export function selectionStateToTextOffsets(selectionState: SelectionState,
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
let offset = 0, start = 0, end = 0;
for(let block of contentBlocks) {
if (selectionState.getStartKey() == block.getKey()) {
if (selectionState.getStartKey() === block.getKey()) {
start = offset + selectionState.getStartOffset();
}
if (selectionState.getEndKey() == block.getKey()) {
if (selectionState.getEndKey() === block.getKey()) {
end = offset + selectionState.getEndOffset();
break;
}
Expand All @@ -191,6 +207,37 @@ export function getTextSelectionOffsets(selectionState: SelectionState,

return {
start,
end
end,
};
}

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

for (let block of contentBlocks) {
let blockLength = block.getLength();

if (start !== -1 && start < blockLength) {
selectionState = selectionState.merge({
anchorKey: block.getKey(),
anchorOffset: start,
});
start = -1;
} else {
start -= blockLength;
}

if (end !== -1 && end <= blockLength) {
selectionState = selectionState.merge({
focusKey: block.getKey(),
focusOffset: end,
});
end = -1;
} else {
end -= blockLength;
}
}

return selectionState;
}
29 changes: 20 additions & 9 deletions src/autocomplete/AutocompleteProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,36 @@ 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)
if (this.commandRegex == null) {
return null;
}

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

console.log(match);

if(selection.start <= matchEnd && selection.end >= matchStart) {
return match;
if (selection.start <= matchEnd && selection.end >= matchStart) {
return {
command: match,
range: {
start: matchStart,
end: matchEnd,
},
};
}
}
this.commandRegex.lastIndex = 0;
return null;
return {
command: null,
range: {
start: -1,
end: -1,
},
};
}

getCompletions(query: String, selection: {start: number, end: number}) {
getCompletions(query: string, selection: {start: number, end: number}) {
return Q.when([]);
}

Expand Down
4 changes: 2 additions & 2 deletions src/autocomplete/Autocompleter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ const PROVIDERS = [
CommandProvider,
DuckDuckGoProvider,
RoomProvider,
EmojiProvider
EmojiProvider,
].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
provider,
};
});
}
43 changes: 25 additions & 18 deletions src/autocomplete/CommandProvider.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import React from 'react';
import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import Fuse from 'fuse.js';
import {TextualCompletion} from './Components';

const COMMANDS = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the longer term, we should try to get this information from the definitions of the commands in src/SlashCommands.js rather than duplicating the usage text here

{
command: '/me',
args: '<message>',
description: 'Displays action'
description: 'Displays action',
},
{
command: '/ban',
args: '<user-id> [reason]',
description: 'Bans user with given id'
description: 'Bans user with given id',
},
{
command: '/deop'
command: '/deop',
args: '<user-id>',
description: 'Deops user with given id',
},
{
command: '/encrypt'
},
{
command: '/invite'
command: '/invite',
args: '<user-id>',
description: 'Invites user with given id to current room'
},
{
command: '/join',
args: '<room-alias>',
description: 'Joins room with given alias'
description: 'Joins room with given alias',
},
{
command: '/kick',
args: '<user-id> [reason]',
description: 'Kicks user with given id'
description: 'Kicks user with given id',
},
{
command: '/nick',
args: '<display-name>',
description: 'Changes your display nickname'
}
description: 'Changes your display nickname',
},
];

let COMMAND_RE = /(^\/\w*)/g;
Expand All @@ -47,19 +50,23 @@ export default class CommandProvider extends AutocompleteProvider {
constructor() {
super(COMMAND_RE);
this.fuse = new Fuse(COMMANDS, {
keys: ['command', 'args', 'description']
keys: ['command', 'args', 'description'],
});
}

getCompletions(query: string, selection: {start: number, end: number}) {
let completions = [];
const command = this.getCurrentCommand(query, selection);
if(command) {
let {command, range} = this.getCurrentCommand(query, selection);
if (command) {
completions = this.fuse.search(command[0]).map(result => {
return {
title: result.command,
subtitle: result.args,
description: result.description
completion: result.command + ' ',
component: (<TextualCompletion
title={result.command}
subtitle={result.args}
description={result.description}
/>),
range,
};
});
}
Expand All @@ -71,7 +78,7 @@ export default class CommandProvider extends AutocompleteProvider {
}

static getInstance(): CommandProvider {
if(instance == null)
if (instance == null)
instance = new CommandProvider();

return instance;
Expand Down
14 changes: 10 additions & 4 deletions src/autocomplete/Components.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
export function TextualCompletion(props: {
import React from 'react';

export function TextualCompletion({
title,
subtitle,
description,
}: {
title: ?string,
subtitle: ?string,
description: ?string
}) {
return (
<div className="mx_Autocomplete_Completion">
<span>{completion.title}</span>
<em>{completion.subtitle}</em>
<span style={{color: 'gray', float: 'right'}}>{completion.description}</span>
<span>{title}</span>
<em>{subtitle}</em>
<span style={{color: 'gray', float: 'right'}}>{description}</span>
</div>
);
}
Loading