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

Commit cccc58b

Browse files
committed
feat: implement autocomplete replacement
1 parent 8961c87 commit cccc58b

13 files changed

+270
-120
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"glob": "^5.0.14",
3838
"highlight.js": "^8.9.1",
3939
"linkifyjs": "^2.0.0-beta.4",
40+
"lodash": "^4.13.1",
4041
"marked": "^0.3.5",
4142
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
4243
"optimist": "^0.6.1",

src/RichText.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import React from 'react';
12
import {
23
Editor,
34
Modifier,
45
ContentState,
6+
ContentBlock,
57
convertFromHTML,
68
DefaultDraftBlockRenderMap,
79
DefaultDraftInlineStyle,
810
CompositeDecorator,
9-
SelectionState
11+
SelectionState,
1012
} from 'draft-js';
1113
import * as sdk from './index';
1214
import * as emojione from 'emojione';
@@ -25,7 +27,7 @@ const STYLES = {
2527
CODE: 'code',
2628
ITALIC: 'em',
2729
STRIKETHROUGH: 's',
28-
UNDERLINE: 'u'
30+
UNDERLINE: 'u',
2931
};
3032

3133
const MARKDOWN_REGEX = {
@@ -168,7 +170,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
168170
text = "";
169171

170172

171-
for(let currentKey = startKey;
173+
for (let currentKey = startKey;
172174
currentKey && currentKey !== endKey;
173175
currentKey = contentState.getKeyAfter(currentKey)) {
174176
let blockText = getText(currentKey);
@@ -189,14 +191,14 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
189191
* Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
190192
* Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
191193
*/
192-
export function getTextSelectionOffsets(selectionState: SelectionState,
193-
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
194+
export function selectionStateToTextOffsets(selectionState: SelectionState,
195+
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
194196
let offset = 0, start = 0, end = 0;
195197
for(let block of contentBlocks) {
196-
if (selectionState.getStartKey() == block.getKey()) {
198+
if (selectionState.getStartKey() === block.getKey()) {
197199
start = offset + selectionState.getStartOffset();
198200
}
199-
if (selectionState.getEndKey() == block.getKey()) {
201+
if (selectionState.getEndKey() === block.getKey()) {
200202
end = offset + selectionState.getEndOffset();
201203
break;
202204
}
@@ -205,6 +207,37 @@ export function getTextSelectionOffsets(selectionState: SelectionState,
205207

206208
return {
207209
start,
208-
end
210+
end,
211+
};
212+
}
213+
214+
export function textOffsetsToSelectionState({start, end}: {start: number, end: number},
215+
contentBlocks: Array<ContentBlock>): SelectionState {
216+
let selectionState = SelectionState.createEmpty();
217+
218+
for (let block of contentBlocks) {
219+
let blockLength = block.getLength();
220+
221+
if (start !== -1 && start < blockLength) {
222+
selectionState = selectionState.merge({
223+
anchorKey: block.getKey(),
224+
anchorOffset: start,
225+
});
226+
start = -1;
227+
} else {
228+
start -= blockLength;
229+
}
230+
231+
if (end !== -1 && end <= blockLength) {
232+
selectionState = selectionState.merge({
233+
focusKey: block.getKey(),
234+
focusOffset: end,
235+
});
236+
end = -1;
237+
} else {
238+
end -= blockLength;
239+
}
209240
}
241+
242+
return selectionState;
210243
}

src/autocomplete/AutocompleteProvider.js

+20-9
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,36 @@ export default class AutocompleteProvider {
1414
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
1515
*/
1616
getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array<string> {
17-
if(this.commandRegex == null)
17+
if (this.commandRegex == null) {
1818
return null;
19+
}
1920

20-
let match = null;
21-
while((match = this.commandRegex.exec(query)) != null) {
21+
let match;
22+
while ((match = this.commandRegex.exec(query)) != null) {
2223
let matchStart = match.index,
2324
matchEnd = matchStart + match[0].length;
24-
25-
console.log(match);
2625

27-
if(selection.start <= matchEnd && selection.end >= matchStart) {
28-
return match;
26+
if (selection.start <= matchEnd && selection.end >= matchStart) {
27+
return {
28+
command: match,
29+
range: {
30+
start: matchStart,
31+
end: matchEnd,
32+
},
33+
};
2934
}
3035
}
3136
this.commandRegex.lastIndex = 0;
32-
return null;
37+
return {
38+
command: null,
39+
range: {
40+
start: -1,
41+
end: -1,
42+
},
43+
};
3344
}
3445

35-
getCompletions(query: String, selection: {start: number, end: number}) {
46+
getCompletions(query: string, selection: {start: number, end: number}) {
3647
return Q.when([]);
3748
}
3849

src/autocomplete/Autocompleter.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ const PROVIDERS = [
99
CommandProvider,
1010
DuckDuckGoProvider,
1111
RoomProvider,
12-
EmojiProvider
12+
EmojiProvider,
1313
].map(completer => completer.getInstance());
1414

1515
export function getCompletions(query: string, selection: {start: number, end: number}) {
1616
return PROVIDERS.map(provider => {
1717
return {
1818
completions: provider.getCompletions(query, selection),
19-
provider
19+
provider,
2020
};
2121
});
2222
}

src/autocomplete/CommandProvider.js

+25-18
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
1+
import React from 'react';
12
import AutocompleteProvider from './AutocompleteProvider';
23
import Q from 'q';
34
import Fuse from 'fuse.js';
5+
import {TextualCompletion} from './Components';
46

57
const COMMANDS = [
68
{
79
command: '/me',
810
args: '<message>',
9-
description: 'Displays action'
11+
description: 'Displays action',
1012
},
1113
{
1214
command: '/ban',
1315
args: '<user-id> [reason]',
14-
description: 'Bans user with given id'
16+
description: 'Bans user with given id',
1517
},
1618
{
17-
command: '/deop'
19+
command: '/deop',
20+
args: '<user-id>',
21+
description: 'Deops user with given id',
1822
},
1923
{
20-
command: '/encrypt'
21-
},
22-
{
23-
command: '/invite'
24+
command: '/invite',
25+
args: '<user-id>',
26+
description: 'Invites user with given id to current room'
2427
},
2528
{
2629
command: '/join',
2730
args: '<room-alias>',
28-
description: 'Joins room with given alias'
31+
description: 'Joins room with given alias',
2932
},
3033
{
3134
command: '/kick',
3235
args: '<user-id> [reason]',
33-
description: 'Kicks user with given id'
36+
description: 'Kicks user with given id',
3437
},
3538
{
3639
command: '/nick',
3740
args: '<display-name>',
38-
description: 'Changes your display nickname'
39-
}
41+
description: 'Changes your display nickname',
42+
},
4043
];
4144

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

5457
getCompletions(query: string, selection: {start: number, end: number}) {
5558
let completions = [];
56-
const command = this.getCurrentCommand(query, selection);
57-
if(command) {
59+
let {command, range} = this.getCurrentCommand(query, selection);
60+
if (command) {
5861
completions = this.fuse.search(command[0]).map(result => {
5962
return {
60-
title: result.command,
61-
subtitle: result.args,
62-
description: result.description
63+
completion: result.command + ' ',
64+
component: (<TextualCompletion
65+
title={result.command}
66+
subtitle={result.args}
67+
description={result.description}
68+
/>),
69+
range,
6370
};
6471
});
6572
}
@@ -71,7 +78,7 @@ export default class CommandProvider extends AutocompleteProvider {
7178
}
7279

7380
static getInstance(): CommandProvider {
74-
if(instance == null)
81+
if (instance == null)
7582
instance = new CommandProvider();
7683

7784
return instance;

src/autocomplete/Components.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
export function TextualCompletion(props: {
1+
import React from 'react';
2+
3+
export function TextualCompletion({
4+
title,
5+
subtitle,
6+
description,
7+
}: {
28
title: ?string,
39
subtitle: ?string,
410
description: ?string
511
}) {
612
return (
713
<div className="mx_Autocomplete_Completion">
8-
<span>{completion.title}</span>
9-
<em>{completion.subtitle}</em>
10-
<span style={{color: 'gray', float: 'right'}}>{completion.description}</span>
14+
<span>{title}</span>
15+
<em>{subtitle}</em>
16+
<span style={{color: 'gray', float: 'right'}}>{description}</span>
1117
</div>
1218
);
1319
}
+40-17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import React from 'react';
12
import AutocompleteProvider from './AutocompleteProvider';
23
import Q from 'q';
34
import 'whatwg-fetch';
45

6+
import {TextualCompletion} from './Components';
7+
58
const DDG_REGEX = /\/ddg\s+(.+)$/g;
6-
const REFERER = 'vector';
9+
const REFERRER = 'vector';
710

811
let instance = null;
912

@@ -14,42 +17,62 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
1417

1518
static getQueryUri(query: String) {
1619
return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
17-
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`;
20+
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
1821
}
1922

2023
getCompletions(query: string, selection: {start: number, end: number}) {
21-
let command = this.getCurrentCommand(query, selection);
22-
if(!query || !command)
24+
let {command, range} = this.getCurrentCommand(query, selection);
25+
if (!query || !command) {
2326
return Q.when([]);
27+
}
2428

2529
return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
26-
method: 'GET'
30+
method: 'GET',
2731
})
2832
.then(response => response.json())
2933
.then(json => {
3034
let results = json.Results.map(result => {
3135
return {
32-
title: result.Text,
33-
description: result.Result
36+
completion: result.Text,
37+
component: (
38+
<TextualCompletion
39+
title={result.Text}
40+
description={result.Result} />
41+
),
42+
range,
3443
};
3544
});
36-
if(json.Answer) {
45+
if (json.Answer) {
3746
results.unshift({
38-
title: json.Answer,
39-
description: json.AnswerType
47+
completion: json.Answer,
48+
component: (
49+
<TextualCompletion
50+
title={json.Answer}
51+
description={json.AnswerType} />
52+
),
53+
range,
4054
});
4155
}
42-
if(json.RelatedTopics && json.RelatedTopics.length > 0) {
56+
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
4357
results.unshift({
44-
title: json.RelatedTopics[0].Text
58+
completion: json.RelatedTopics[0].Text,
59+
component: (
60+
<TextualCompletion
61+
title={json.RelatedTopics[0].Text} />
62+
),
63+
range,
4564
});
4665
}
47-
if(json.AbstractText) {
66+
if (json.AbstractText) {
4867
results.unshift({
49-
title: json.AbstractText
68+
completion: json.AbstractText,
69+
component: (
70+
<TextualCompletion
71+
title={json.AbstractText} />
72+
),
73+
range,
5074
});
5175
}
52-
// console.log(results);
5376
return results;
5477
});
5578
}
@@ -59,9 +82,9 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
5982
}
6083

6184
static getInstance(): DuckDuckGoProvider {
62-
if(instance == null)
85+
if (instance == null) {
6386
instance = new DuckDuckGoProvider();
64-
87+
}
6588
return instance;
6689
}
6790
}

0 commit comments

Comments
 (0)