From 4d728a500eb675c68f7c237aedef7fae287ab296 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Sun, 6 Oct 2019 08:42:06 +0800 Subject: [PATCH] Support RegExp search and replace in file edit. (#1422) * Update style of search component * Opti folder structure * Finish UI * Finish all search and replace function * add notification when match too more or invalid regular expression * Modify empty string * Modify stile * Do init search when press cmd + f --- package.json | 1 + src/muya/lib/config/index.js | 7 + src/muya/lib/contentState/searchCtrl.js | 89 ++-- .../components/editorWithTabs/editor.vue | 8 +- .../{rename.vue => rename/index.vue} | 2 +- src/renderer/components/search.vue | 298 ------------ src/renderer/components/search/index.vue | 449 ++++++++++++++++++ .../{titleBar.vue => titleBar/index.vue} | 4 +- src/renderer/store/editor.js | 1 + yarn.lock | 19 + 10 files changed, 540 insertions(+), 338 deletions(-) rename src/renderer/components/{rename.vue => rename/index.vue} (98%) delete mode 100644 src/renderer/components/search.vue create mode 100644 src/renderer/components/search/index.vue rename src/renderer/components/{titleBar.vue => titleBar/index.vue} (99%) diff --git a/package.json b/package.json index cf84faff..f3e09cc5 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "electron-window-state": "^5.0.3", "element-resize-detector": "^1.2.0", "element-ui": "^2.10.1", + "execall": "^2.0.0", "file-icons-js": "^1.0.3", "flowchart.js": "^1.12.1", "fontmanager-redux": "^0.4.0", diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js index a7303810..8a2e59ac 100644 --- a/src/muya/lib/config/index.js +++ b/src/muya/lib/config/index.js @@ -279,3 +279,10 @@ export const URL_REG = /^http(s)?:\/\/([a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localho // The smallest transparent gif base64 image. // export const SMALLEST_BASE64 = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' // export const isIOS = /(?:iPhone|iPad|iPod|iOS)/i.test(window.navigator.userAgent) +export const defaultSearchOption = { + isCaseSensitive: false, + isWholeWord: false, + isRegexp: false, + selectHighlight: false, + highlightIndex: -1 +} diff --git a/src/muya/lib/contentState/searchCtrl.js b/src/muya/lib/contentState/searchCtrl.js index a0f0d595..b35977b8 100644 --- a/src/muya/lib/contentState/searchCtrl.js +++ b/src/muya/lib/contentState/searchCtrl.js @@ -1,26 +1,51 @@ -const defaultSearchOption = { - caseSensitive: false, - selectHighlight: false, - highlightIndex: -1 +import execall from 'execall' +import { defaultSearchOption } from '../config' + +const matchString = (text, value, options) => { + const { isCaseSensitive, isWholeWord, isRegexp } = options + /* eslint-disable no-useless-escape */ + const SPECIAL_CHAR_REG = /[\[\]\\^$.\|\?\*\+\(\)\/]{1}/g + /* eslint-enable no-useless-escape */ + let SEARCH_REG = null + let regStr = value + let flag = 'g' + + if (!isCaseSensitive) { + flag += 'i' + } + + if (!isRegexp) { + regStr = value.replace(SPECIAL_CHAR_REG, (p) => { + return p === '\\' ? '\\\\' : `\\${p}` + }) + } + + if (isWholeWord) { + regStr = `\\b${regStr}\\b` + } + + try { + // Add try catch expression because not all string can generate a valid RegExp. for example `\`. + SEARCH_REG = new RegExp(regStr, flag) + return execall(SEARCH_REG, text) + } catch (err) { + return [] + } } const searchCtrl = ContentState => { ContentState.prototype.replaceOne = function (match, value) { - const { - start, - end, - key - } = match + const { start, end, key } = match const block = this.getBlock(key) - const { - text - } = block + const { text } = block block.text = text.substring(0, start) + value + text.substring(end) } ContentState.prototype.replace = function (replaceValue, opt = { isSingle: true }) { - const { isSingle, caseSensitive } = opt + const { isSingle } = opt + delete opt.isSingle + const searchOptions = Object.assign({}, defaultSearchOption, opt) const { matches, value, index } = this.searchMatches if (matches.length) { if (isSingle) { @@ -32,7 +57,7 @@ const searchCtrl = ContentState => { } } const highlightIndex = index < matches.length - 1 ? index : index - 1 - this.search(value, { caseSensitive, highlightIndex: isSingle ? highlightIndex : -1 }) + this.search(value, { ...searchOptions, highlightIndex: isSingle ? highlightIndex : -1 }) } } @@ -69,34 +94,30 @@ const searchCtrl = ContentState => { } ContentState.prototype.search = function (value, opt = {}) { - value = value.trim() const matches = [] - const { caseSensitive, highlightIndex } = Object.assign(defaultSearchOption, opt) + const options = Object.assign({}, defaultSearchOption, opt) + const { highlightIndex } = options const { blocks } = this - const search = blocks => { + const travel = blocks => { for (const block of blocks) { let { text, key } = block - if (!caseSensitive) { - text = text.toLowerCase() - value = value.toLowerCase() - } - if (text) { - let i = text.indexOf(value) - while (i > -1) { - matches.push({ + + if (text && typeof text === 'string') { + const strMatches = matchString(text, value, options) + matches.push(...strMatches.map(m => { + return { key, - start: i, - end: i + value.length - }) - i = text.indexOf(value, i + value.length) - } + start: m.index, + end: m.index + m.match.length + } + })) } if (block.children.length) { - search(block.children) + travel(block.children) } } } - if (value) search(blocks) + if (value) travel(blocks) let index = -1 if (highlightIndex !== -1) { index = highlightIndex // If set the highlight index, then highlight the highlighIndex @@ -104,7 +125,9 @@ const searchCtrl = ContentState => { index = 0 // highlight the first word that matches. } Object.assign(this.searchMatches, { value, matches, index }) - if (value) this.setCursorToHighlight() + if (value) { + this.setCursorToHighlight() + } return matches } } diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index a8f21c99..01f9dcbc 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -88,7 +88,7 @@ import FormatPicker from 'muya/lib/ui/formatPicker' import LinkTools from 'muya/lib/ui/linkTools' import FrontMenu from 'muya/lib/ui/frontMenu' import bus from '../../bus' -import Search from '../search.vue' +import Search from '../search' import { animatedScrollTo } from '../../util' import { addCommonStyle, setEditorWidth } from '../../util/theme' import { guessClipboardFilePath } from '../../util/clipboard' @@ -412,7 +412,7 @@ export default { bus.$on('format', this.handleInlineFormat) bus.$on('searchValue', this.handleSearch) bus.$on('replaceValue', this.handReplace) - bus.$on('find', this.handleFind) + bus.$on('find-action', this.handleFindAction) bus.$on('insert-image', this.insertImage) bus.$on('image-uploaded', this.handleUploadedImage) bus.$on('file-changed', this.handleFileChange) @@ -638,7 +638,7 @@ export default { } }, - handleFind (action) { + handleFindAction (action) { const searchMatches = this.editor.find(action) this.$store.dispatch('SEARCH', searchMatches) this.scrollToHighlight() @@ -783,7 +783,7 @@ export default { bus.$off('format', this.handleInlineFormat) bus.$off('searchValue', this.handleSearch) bus.$off('replaceValue', this.handReplace) - bus.$off('find', this.handleFind) + bus.$off('find-action', this.handleFindAction) bus.$off('insert-image', this.insertImage) bus.$off('image-uploaded', this.handleUploadedImage) bus.$off('file-changed', this.handleFileChange) diff --git a/src/renderer/components/rename.vue b/src/renderer/components/rename/index.vue similarity index 98% rename from src/renderer/components/rename.vue rename to src/renderer/components/rename/index.vue index 54f02494..d1b9037e 100644 --- a/src/renderer/components/rename.vue +++ b/src/renderer/components/rename/index.vue @@ -24,7 +24,7 @@ - - diff --git a/src/renderer/components/search/index.vue b/src/renderer/components/search/index.vue new file mode 100644 index 00000000..a7d61c66 --- /dev/null +++ b/src/renderer/components/search/index.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/src/renderer/components/titleBar.vue b/src/renderer/components/titleBar/index.vue similarity index 99% rename from src/renderer/components/titleBar.vue rename to src/renderer/components/titleBar/index.vue index c4821af9..a837f591 100644 --- a/src/renderer/components/titleBar.vue +++ b/src/renderer/components/titleBar/index.vue @@ -100,8 +100,8 @@