diff --git a/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/AutocompleteContainer.js b/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/AutocompleteContainer.js index a9711ece..5cb885ac 100644 --- a/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/AutocompleteContainer.js +++ b/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/AutocompleteContainer.js @@ -5,6 +5,7 @@ import clickOutside from 'react-click-outside'; import AutocompleteWidget from './AutocompleteWidget'; import { getSlateEditor } from '../../utils'; import { getAccents, getPosAfterEmphasis } from '../../slate/transforms'; +import { matchUnderCursor } from './utils'; const escapeCode = 27; const arrowUpCode = 38; @@ -170,7 +171,18 @@ class AutocompleteContainer extends PureComponent { if (extension && item) { const change = state.change(); - change.deleteBackward(term.length).insertText(extension.markdownText(item, term)).focus(); + + const text = change.anchorText.text + const cursor = change.anchorOffset; + + const { start, end } = matchUnderCursor({ text, cursor, regexp: extension.termRegex }); + + change. + deleteBackward(cursor - start). + deleteForward(end - cursor). + insertText(extension.markdownText(item, term)). + focus(); + this.props.onChange(change.state, true); } diff --git a/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/utils.js b/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/utils.js new file mode 100644 index 00000000..d8cb7067 --- /dev/null +++ b/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/utils.js @@ -0,0 +1,20 @@ +export const matchUnderCursor = ({ text, regexp, cursor }) => { + const result = { + match: '', + start: cursor, + end: cursor + } + + for (let start = cursor; start >= 0; start--) { + for (let end = cursor; end <= text.length; end++) { + const chunk = text.slice(start, end); + if (regexp.test(chunk) && chunk.length > result.match.length) { + result.match = chunk; + result.start = start; + result.end = end; + } + } + } + + return result; +} diff --git a/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/utils.spec.js b/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/utils.spec.js new file mode 100644 index 00000000..7c0a977b --- /dev/null +++ b/src/client/components/PlainMarkdownInput/plugins/slate-autocomplete-plugin/utils.spec.js @@ -0,0 +1,42 @@ +import { expect } from 'chai'; + +import { matchUnderCursor } from './utils'; + +describe('matchUnderCursor', () => { + it('should find regexp match under cursor', () => { + const regexp = /^\$(\w*)$/; + + const tests = [ + // returns match under cursor + { + args: { text: 'abc $def ghi $jkl mno', regexp, cursor: 5 }, + want: { match: '$def', start: 4, end: 8 } + }, + { + args: { text: 'abc $def ghi $jkl mno', regexp, cursor: 14 }, + want: { match: '$jkl', start: 13, end: 17 } + }, + // move cursor left to right to test all cases + ...('abc $def ghi'.split('').reduce((acc, ch, cursor) => { + return [ + ...acc, + { + args: { text: 'abc $def ghi', regexp, cursor }, + ...( + // if cursor is out of match then return empty match + (cursor < 4 || cursor > 8) ? + { want: { match: '', start: cursor, end: cursor } } : + // if cursor is on (or on edges of) match then return match and its boundaries + { want: { match: '$def', start: 4, end: 8 } } + ) + } + ] + }, [])) + ] + + tests.forEach(({ args, want }) => { + const got = matchUnderCursor(args) + expect(got).to.deep.equal(want, `input: ${JSON.stringify(args)}`) + }) + }) +})