From db4f2a447845cee6b965af78fead5a733f2f366d Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Tue, 1 Sep 2020 22:28:08 +0200 Subject: [PATCH] add missing autocompletions (#514) * added missing autocompletions: - code-block - container - header - image - link - pdf * added extraTags ([name=], [time=], [color=]) to the link autocompletion, because they trigger on the same characters added getUser in /redux/user/methods to retrive the current user outside of .tsx files improve the regexps on several autocompletion * renamed hints to auto Co-authored-by: Erik Michelson Co-authored-by: Tilman Vatteroth --- cypress/integration/autocompletion.spec.ts | 274 +++++++++++++++--- .../editor-pane/autocompletion/code-block.ts | 39 +++ .../editor-pane/autocompletion/container.ts | 37 +++ .../{hints => autocompletion}/emoji.ts | 56 ++-- .../editor-pane/autocompletion/header.ts | 43 +++ .../editor-pane/autocompletion/image.ts | 40 +++ .../editor-pane/autocompletion/index.ts | 59 ++++ .../autocompletion/link-and-extra-tag.ts | 69 +++++ .../editor/editor-pane/autocompletion/pdf.ts | 35 +++ .../editor/editor-pane/editor-pane.tsx | 29 +- src/redux/user/methods.ts | 4 + 11 files changed, 599 insertions(+), 86 deletions(-) create mode 100644 src/components/editor/editor-pane/autocompletion/code-block.ts create mode 100644 src/components/editor/editor-pane/autocompletion/container.ts rename src/components/editor/editor-pane/{hints => autocompletion}/emoji.ts (55%) create mode 100644 src/components/editor/editor-pane/autocompletion/header.ts create mode 100644 src/components/editor/editor-pane/autocompletion/image.ts create mode 100644 src/components/editor/editor-pane/autocompletion/index.ts create mode 100644 src/components/editor/editor-pane/autocompletion/link-and-extra-tag.ts create mode 100644 src/components/editor/editor-pane/autocompletion/pdf.ts diff --git a/cypress/integration/autocompletion.spec.ts b/cypress/integration/autocompletion.spec.ts index aa8f63e2b..4d37239c2 100644 --- a/cypress/integration/autocompletion.spec.ts +++ b/cypress/integration/autocompletion.spec.ts @@ -8,56 +8,260 @@ describe('Autocompletion', () => { .type('{backspace}') }) - describe('normal emoji', () => { + describe('code block', () => { it('via Enter', () => { cy.get('.CodeMirror textarea') - .type(':book') + .type('```') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') .type('{enter}') cy.get('.CodeMirror-hints') .should('not.exist') - cy.get('.CodeMirror-activeline > .CodeMirror-line > span') - .should('have.text', ':book:') - cy.get('.markdown-body') - .should('have.text', '📖') - }) - it('via doubleclick', () => { - cy.get('.CodeMirror textarea') - .type(':book') - cy.get('.CodeMirror-hints > li') - .first() - .dblclick() - cy.get('.CodeMirror-hints') - .should('not.exist') - cy.get('.CodeMirror-activeline > .CodeMirror-line > span') - .should('have.text', ':book:') - cy.get('.markdown-body') - .should('have.text', '📖') - }) - }) - - describe('fork-awesome-icon', () => { - it('via Enter', () => { - cy.get('.CodeMirror textarea') - .type(':facebook') - .type('{enter}') - cy.get('.CodeMirror-hints') - .should('not.exist') - cy.get('.CodeMirror-activeline > .CodeMirror-line > span') - .should('have.text', ':fa-facebook:') - cy.get('.markdown-body > p > i.fa.fa-facebook') + cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span') + .should('have.text', '```1c') + cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span') + .should('have.text', '```') + cy.get('.markdown-body > pre > code') .should('exist') }) it('via doubleclick', () => { cy.get('.CodeMirror textarea') - .type(':facebook') + .type('```') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span') + .should('have.text', '```1c') + cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span') + .should('have.text', '```') + cy.get('.markdown-body > pre > code') + .should('exist') + }) + }) + + describe('container', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type(':::') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span') + .should('have.text', ':::success') + cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span') + .should('have.text', ':::') + cy.get('.markdown-body > div.alert') + .should('exist') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type(':::') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span') + .should('have.text', ':::success') + cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span') + .should('have.text', ':::') + cy.get('.markdown-body > div.alert') + .should('exist') + }) + }) + + describe('emoji', () => { + describe('normal emoji', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type(':book') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', ':book:') + cy.get('.markdown-body') + .should('have.text', '📖') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type(':book') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', ':book:') + cy.get('.markdown-body') + .should('have.text', '📖') + }) + }) + + describe('fork-awesome-icon', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type(':facebook') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', ':fa-facebook:') + cy.get('.markdown-body > p > i.fa.fa-facebook') + .should('exist') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type(':facebook') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', ':fa-facebook:') + cy.get('.markdown-body > p > i.fa.fa-facebook') + .should('exist') + }) + }) + }) + + describe('header', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type('#') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '# ') + cy.get('.markdown-body > h1 ') + .should('have.text', ' ') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type('#') cy.get('.CodeMirror-hints > li') .first() .dblclick() cy.get('.CodeMirror-hints') .should('not.exist') cy.get('.CodeMirror-activeline > .CodeMirror-line > span') - .should('have.text', ':fa-facebook:') - cy.get('.markdown-body > p > i.fa.fa-facebook') + .should('have.text', '# ') + cy.get('.markdown-body > h1') + .should('have.text', ' ') + }) + }) + + describe('images', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type('!') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '![image alt](https:// "title")') + cy.get('.markdown-body > p > img') + .should('have.attr', 'alt', 'image alt') + .should('have.attr', 'src', 'https://') + .should('have.attr', 'title', 'title') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type('!') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '![image alt](https:// "title")') + cy.get('.markdown-body > p > img') + .should('have.attr', 'alt', 'image alt') + .should('have.attr', 'src', 'https://') + .should('have.attr', 'title', 'title') + }) + }) + + describe('links', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type('[') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '[link text](https:// "title") ') + cy.get('.markdown-body > p > a') + .should('have.text', 'link text') + .should('have.attr', 'href', 'https://') + .should('have.attr', 'title', 'title') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type('[') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '[link text](https:// "title") ') + cy.get('.markdown-body > p > a') + .should('have.text', 'link text') + .should('have.attr', 'href', 'https://') + .should('have.attr', 'title', 'title') + }) + }) + + describe('pdf', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type('{') + cy.get('.CodeMirror-hints') + .should('exist') + cy.get('.CodeMirror textarea') + .type('{enter}') + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '{%pdf https:// %}') + cy.get('.markdown-body > p') + .should('exist') + }) + it('via doubleclick', () => { + cy.get('.CodeMirror textarea') + .type('{') + cy.get('.CodeMirror-hints > li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '{%pdf https:// %}') + cy.get('.markdown-body > p') .should('exist') }) }) diff --git a/src/components/editor/editor-pane/autocompletion/code-block.ts b/src/components/editor/editor-pane/autocompletion/code-block.ts new file mode 100644 index 000000000..cc5b8f564 --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/code-block.ts @@ -0,0 +1,39 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import hljs from 'highlight.js' +import { findWordAtCursor, Hinter, search } from './index' + +const allowedChars = /[`\w-_+]/ +const wordRegExp = /^```((\w|-|_|\+)*)$/ +const allSupportedLanguages = hljs.listLanguages().concat('csv', 'flow', 'html') + +const codeBlockHint = (editor: Editor): Promise< Hints| null > => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const term = searchResult[1] + const suggestions = search(term, allSupportedLanguages) + const cursor = editor.getCursor() + if (!suggestions) { + resolve(null) + } else { + resolve({ + list: suggestions.map((suggestion: string): Hint => ({ + text: '```' + suggestion + '\n\n```\n', + displayText: suggestion + })), + from: Pos(cursor.line, searchTerm.start), + to: Pos(cursor.line, searchTerm.end) + }) + } + }) +} + +export const CodeBlockHinter: Hinter = { + allowedChars, + wordRegExp, + hint: codeBlockHint +} diff --git a/src/components/editor/editor-pane/autocompletion/container.ts b/src/components/editor/editor-pane/autocompletion/container.ts new file mode 100644 index 000000000..79107060e --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/container.ts @@ -0,0 +1,37 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import { findWordAtCursor, Hinter } from './index' + +const allowedChars = /[:\w-_+]/ +const wordRegExp = /^:::((\w|-|_|\+)*)$/ +const allSupportedConatiner = ['success', 'info', 'warning', 'danger'] + +const containerHint = (editor: Editor): Promise< Hints| null > => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const suggestions = allSupportedConatiner + const cursor = editor.getCursor() + if (!suggestions) { + resolve(null) + } else { + resolve({ + list: suggestions.map((suggestion: string): Hint => ({ + text: ':::' + suggestion + '\n\n:::\n', + displayText: suggestion + })), + from: Pos(cursor.line, searchTerm.start), + to: Pos(cursor.line, searchTerm.end) + }) + } + }) +} + +export const ContainerHinter: Hinter = { + allowedChars, + wordRegExp, + hint: containerHint +} diff --git a/src/components/editor/editor-pane/hints/emoji.ts b/src/components/editor/editor-pane/autocompletion/emoji.ts similarity index 55% rename from src/components/editor/editor-pane/hints/emoji.ts rename to src/components/editor/editor-pane/autocompletion/emoji.ts index 6ed290e47..749769011 100644 --- a/src/components/editor/editor-pane/hints/emoji.ts +++ b/src/components/editor/editor-pane/autocompletion/emoji.ts @@ -1,64 +1,40 @@ import { Editor, Hint, Hints, Pos } from 'codemirror' import { Data, EmojiData, NimbleEmojiIndex } from 'emoji-mart' import data from 'emoji-mart/data/twitter.json' -import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils' import { customEmojis } from '../tool-bar/emoji-picker/emoji-picker' +import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils' +import { findWordAtCursor, Hinter } from './index' -interface findWordAtCursorResponse { - start: number, - end: number, - text: string -} - -const allowedCharsInEmojiCodeRegex = /(:|\w|-|_|\+)/ +const allowedCharsInEmojiCodeRegex = /[:\w-_+]/ const emojiIndex = new NimbleEmojiIndex(data as unknown as Data) -export const emojiWordRegex = /^:((\w|-|_|\+)+)$/ +const emojiWordRegex = /^:([\w-_+]*)$/ -export const findWordAtCursor = (editor: Editor): findWordAtCursorResponse => { - const cursor = editor.getCursor() - const line = editor.getLine(cursor.line) - let start = cursor.ch - let end = cursor.ch - while (start && allowedCharsInEmojiCodeRegex.test(line.charAt(start - 1))) { - --start - } - while (end < line.length && allowedCharsInEmojiCodeRegex.test(line.charAt(end))) { - ++end - } - - return { - text: line.slice(start, end).toLowerCase(), - start: start, - end: end - } -} - -export const generateEmojiHints = (editor: Editor): Promise< Hints| null > => { +const generateEmojiHints = (editor: Editor): Promise< Hints| null > => { return new Promise((resolve) => { - const searchTerm = findWordAtCursor(editor) + const searchTerm = findWordAtCursor(editor, allowedCharsInEmojiCodeRegex) const searchResult = emojiWordRegex.exec(searchTerm.text) if (searchResult === null) { resolve(null) return } const term = searchResult[1] - if (!term) { - resolve(null) - return - } - const search = emojiIndex.search(term, { + let search: EmojiData[] | null = emojiIndex.search(term, { emojisToShowFilter: () => true, - maxResults: 5, + maxResults: 7, include: [], exclude: [], custom: customEmojis as EmojiData[] }) + if (search === null) { + // set search to the first seven emojis in data + search = Object.values(emojiIndex.emojis).slice(0, 7) + } const cursor = editor.getCursor() if (!search) { resolve(null) } else { resolve({ - list: search.map((emojiData: EmojiData): Hint => ({ + list: search.map((emojiData): Hint => ({ text: getEmojiShortCode(emojiData), render: (parent: HTMLLIElement) => { const wrapper = document.createElement('div') @@ -72,3 +48,9 @@ export const generateEmojiHints = (editor: Editor): Promise< Hints| null > => { } }) } + +export const EmojiHinter: Hinter = { + allowedChars: allowedCharsInEmojiCodeRegex, + wordRegExp: emojiWordRegex, + hint: generateEmojiHints +} diff --git a/src/components/editor/editor-pane/autocompletion/header.ts b/src/components/editor/editor-pane/autocompletion/header.ts new file mode 100644 index 000000000..cc11361c6 --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/header.ts @@ -0,0 +1,43 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import { findWordAtCursor, Hinter, search } from './index' + +const allowedChars = /#/ +const wordRegExp = /^(\s{0,3})(#{1,6})$/ +const allSupportedHeaders = ['# h1', '## h2', '### h3', '#### h4', '##### h5', '###### h6', '###### tags: `example`'] +const allSupportedHeadersTextToInsert = ['# ', '## ', '### ', '#### ', '##### ', '###### ', '###### tags: `example`'] + +const headerHint = (editor: Editor): Promise< Hints| null > => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const term = searchResult[0] + if (!term) { + resolve(null) + return + } + const suggestions = search(term, allSupportedHeaders) + const cursor = editor.getCursor() + if (!suggestions) { + resolve(null) + } else { + resolve({ + list: suggestions.map((suggestion, index): Hint => ({ + text: allSupportedHeadersTextToInsert[index], + displayText: suggestion + })), + from: Pos(cursor.line, searchTerm.start), + to: Pos(cursor.line, searchTerm.end) + }) + } + }) +} + +export const HeaderHinter: Hinter = { + allowedChars, + wordRegExp, + hint: headerHint +} diff --git a/src/components/editor/editor-pane/autocompletion/image.ts b/src/components/editor/editor-pane/autocompletion/image.ts new file mode 100644 index 000000000..c2932b7ac --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/image.ts @@ -0,0 +1,40 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import { findWordAtCursor, Hinter } from './index' + +const allowedChars = /[![\]\w]/ +const wordRegExp = /^(!(\[.*])?)$/ +const allSupportedImages = [ + '![image alt](https:// "title")', + '![image alt](https:// "title" =WidthxHeight)', + '![image alt][reference]' +] + +const imageHint = (editor: Editor): Promise< Hints| null > => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const suggestions = allSupportedImages + const cursor = editor.getCursor() + if (!suggestions) { + resolve(null) + } else { + resolve({ + list: suggestions.map((suggestion: string): Hint => ({ + text: suggestion + })), + from: Pos(cursor.line, searchTerm.start), + to: Pos(cursor.line, searchTerm.end + 1) + }) + } + }) +} + +export const ImageHinter: Hinter = { + allowedChars, + wordRegExp, + hint: imageHint +} diff --git a/src/components/editor/editor-pane/autocompletion/index.ts b/src/components/editor/editor-pane/autocompletion/index.ts new file mode 100644 index 000000000..f290a5255 --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/index.ts @@ -0,0 +1,59 @@ +import { Editor, Hints } from 'codemirror' +import { CodeBlockHinter } from './code-block' +import { ContainerHinter } from './container' +import { EmojiHinter } from './emoji' +import { HeaderHinter } from './header' +import { ImageHinter } from './image' +import { LinkAndExtraTagHinter } from './link-and-extra-tag' +import { PDFHinter } from './pdf' + +interface findWordAtCursorResponse { + start: number, + end: number, + text: string +} + +export interface Hinter { + allowedChars: RegExp, + wordRegExp: RegExp, + hint: (editor: Editor) => Promise< Hints| null > +} + +export const findWordAtCursor = (editor: Editor, allowedChars: RegExp): findWordAtCursorResponse => { + const cursor = editor.getCursor() + const line = editor.getLine(cursor.line) + let start = cursor.ch + let end = cursor.ch + while (start && allowedChars.test(line.charAt(start - 1))) { + --start + } + while (end < line.length && allowedChars.test(line.charAt(end))) { + ++end + } + + return { + text: line.slice(start, end).toLowerCase(), + start: start, + end: end + } +} + +export const search = (term: string, list: string[]): string[] => { + const suggestions: string[] = [] + list.forEach(item => { + if (item.toLowerCase().startsWith(term.toLowerCase())) { + suggestions.push(item) + } + }) + return suggestions.slice(0, 7) +} + +export const allHinters: Hinter[] = [ + CodeBlockHinter, + ContainerHinter, + EmojiHinter, + HeaderHinter, + ImageHinter, + LinkAndExtraTagHinter, + PDFHinter +] diff --git a/src/components/editor/editor-pane/autocompletion/link-and-extra-tag.ts b/src/components/editor/editor-pane/autocompletion/link-and-extra-tag.ts new file mode 100644 index 000000000..951a9a3b9 --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/link-and-extra-tag.ts @@ -0,0 +1,69 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import moment from 'moment' +import { getUser } from '../../../../redux/user/methods' +import { findWordAtCursor, Hinter } from './index' + +const allowedChars = /[[\]\w]/ +const wordRegExp = /^(\[(.*])?)$/ +const allSupportedLinks = [ + '[link text](https:// "title")', + '[reference]: https:// "title"', + '[link text][reference]', + '[reference]', + '[^footnote reference]: https://', + '[^footnote reference]', + '^[inline footnote]', + '[TOC]', + 'name', + 'time', + '[color=#FFFFFF]' + +] + +const linkAndExtraTagHint = (editor: Editor): Promise< Hints| null > => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const suggestions = allSupportedLinks + const cursor = editor.getCursor() + if (!suggestions) { + resolve(null) + } else { + resolve({ + list: suggestions.map((suggestion: string): Hint => { + const user = getUser() + const userName = user ? user.name : 'Anonymous' + switch (suggestion) { + case 'name': + // Get the user when a completion happens, this prevents to early calls resulting in 'Anonymous' + return { + text: `[name=${userName}]` + } + case 'time': + // show the current time when the autocompletion is opened and not when the function is loaded + return { + text: `[time=${moment(new Date()).format('llll')}]` + } + default: + return { + text: suggestion + ' ', + displayText: suggestion + } + } + }), + from: Pos(cursor.line, searchTerm.start), + to: Pos(cursor.line, searchTerm.end + 1) + }) + } + }) +} + +export const LinkAndExtraTagHinter: Hinter = { + allowedChars, + wordRegExp, + hint: linkAndExtraTagHint +} diff --git a/src/components/editor/editor-pane/autocompletion/pdf.ts b/src/components/editor/editor-pane/autocompletion/pdf.ts new file mode 100644 index 000000000..edc680837 --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/pdf.ts @@ -0,0 +1,35 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import { findWordAtCursor, Hinter } from './index' + +const allowedChars = /[{%]/ +const wordRegExp = /^({[%}]?)$/ + +const pdfHint = (editor: Editor): Promise< Hints| null > => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const suggestions = ['{%pdf https:// %}'] + const cursor = editor.getCursor() + if (!suggestions) { + resolve(null) + } else { + resolve({ + list: suggestions.map((suggestion: string): Hint => ({ + text: suggestion + })), + from: Pos(cursor.line, searchTerm.start), + to: Pos(cursor.line, searchTerm.end + 1) + }) + } + }) +} + +export const PDFHinter: Hinter = { + allowedChars, + wordRegExp, + hint: pdfHint +} diff --git a/src/components/editor/editor-pane/editor-pane.tsx b/src/components/editor/editor-pane/editor-pane.tsx index 89fa84276..341e7258e 100644 --- a/src/components/editor/editor-pane/editor-pane.tsx +++ b/src/components/editor/editor-pane/editor-pane.tsx @@ -12,20 +12,20 @@ import 'codemirror/addon/edit/matchtags' import 'codemirror/addon/fold/foldcode' import 'codemirror/addon/fold/foldgutter' import 'codemirror/addon/hint/show-hint' -import 'codemirror/addon/search/search' import 'codemirror/addon/search/jump-to-line' import 'codemirror/addon/search/match-highlighter' +import 'codemirror/addon/search/search' import 'codemirror/addon/selection/active-line' -import 'codemirror/keymap/sublime' import 'codemirror/keymap/emacs' +import 'codemirror/keymap/sublime' import 'codemirror/keymap/vim' import 'codemirror/mode/gfm/gfm' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Controlled as ControlledCodeMirror } from 'react-codemirror2' import { useTranslation } from 'react-i18next' -import './editor-pane.scss' import { ScrollProps, ScrollState } from '../scroll/scroll-props' -import { generateEmojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji' +import { allHinters, findWordAtCursor } from './autocompletion' +import './editor-pane.scss' import { defaultKeyMap } from './key-map' import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar' import { ToolBar } from './tool-bar/tool-bar' @@ -35,17 +35,18 @@ export interface EditorPaneProps { content: string } -const hintOptions = { - hint: generateEmojiHints, - completeSingle: false, - completeOnSingleClick: false, - alignWithWord: true -} - const onChange = (editor: Editor) => { - const searchTerm = findWordAtCursor(editor) - if (emojiWordRegex.test(searchTerm.text)) { - editor.showHint(hintOptions) + for (const hinter of allHinters) { + const searchTerm = findWordAtCursor(editor, hinter.allowedChars) + if (hinter.wordRegExp.test(searchTerm.text)) { + editor.showHint({ + hint: hinter.hint, + completeSingle: false, + completeOnSingleClick: false, + alignWithWord: true + }) + return + } } } diff --git a/src/redux/user/methods.ts b/src/redux/user/methods.ts index e2b22d559..e672f4d43 100644 --- a/src/redux/user/methods.ts +++ b/src/redux/user/methods.ts @@ -15,3 +15,7 @@ export const clearUser: () => void = () => { } store.dispatch(action) } + +export const getUser = (): UserState | null => { + return store.getState().user +}