mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 10:49:21 +08:00
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
This commit is contained in:
parent
2f65f6cec0
commit
4d728a500e
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -24,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../bus'
|
||||
import bus from '../../bus'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
@ -1,298 +0,0 @@
|
||||
<template>
|
||||
<div class="search-bar"
|
||||
@click.stop="noop"
|
||||
v-show="showSearch"
|
||||
>
|
||||
<section class="search">
|
||||
<div class="button-group">
|
||||
<el-tooltip class="item"
|
||||
effect="dark"
|
||||
content="Replacement"
|
||||
placement="top"
|
||||
:visible-arrow="false"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<button
|
||||
class="button"
|
||||
v-if="type !== 'replace'"
|
||||
@click="typeClick"
|
||||
>
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-findreplace"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item"
|
||||
effect="dark"
|
||||
content="Case sensitive"
|
||||
placement="top"
|
||||
:visible-arrow="false"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<button class="button left" @click="caseClick" :class="{ 'active': caseSensitive }">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-case"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchValue"
|
||||
@keyup="search($event)"
|
||||
ref="search"
|
||||
placeholder="Search"
|
||||
>
|
||||
<span class="search-result">{{`${highlightIndex + 1} / ${highlightCount}`}}</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="button right" @click="find('prev')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="button" @click="find('next')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-arrowdown"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="replace" v-if="type === 'replace'">
|
||||
<button class="button active left" @click="typeClick">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-findreplace"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="input-wrapper replace-input">
|
||||
<input type="text" v-model="replaceValue" placeholder="Replacement">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<el-tooltip class="item"
|
||||
effect="dark"
|
||||
content="Replace All"
|
||||
placement="top"
|
||||
:visible-arrow="false"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<button class="button right" @click="replace(false)">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-all-inclusive"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item"
|
||||
effect="dark"
|
||||
content="Replace Single"
|
||||
placement="top"
|
||||
:visible-arrow="false"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<button class="button" @click="replace(true)">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-replace"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../bus'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
showSearch: false,
|
||||
type: 'search',
|
||||
searchValue: '',
|
||||
replaceValue: '',
|
||||
caseSensitive: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchMatches: function (newValue, oldValue) {
|
||||
if (!newValue || !oldValue) return
|
||||
const { value } = newValue
|
||||
if (value && value !== oldValue.value) {
|
||||
this.searchValue = value
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
searchMatches: state => state.editor.currentFile.searchMatches
|
||||
}),
|
||||
highlightIndex () {
|
||||
if (this.searchMatches) {
|
||||
return this.searchMatches.index
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
},
|
||||
highlightCount () {
|
||||
if (this.searchMatches) {
|
||||
return this.searchMatches.matches.length
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
bus.$on('find', this.listenFind)
|
||||
bus.$on('replace', this.listenReplace)
|
||||
bus.$on('findNext', this.listenFindNext)
|
||||
bus.$on('findPrev', this.listenFindPrev)
|
||||
document.addEventListener('click', this.docClick)
|
||||
document.addEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
beforeDestroy () {
|
||||
bus.$off('find', this.listenFind)
|
||||
bus.$off('replace', this.listenReplace)
|
||||
bus.$off('findNext', this.listenFindNext)
|
||||
bus.$off('findPrev', this.listenFindPrev)
|
||||
document.removeEventListener('click', this.docClick)
|
||||
document.removeEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
methods: {
|
||||
listenFind () {
|
||||
this.showSearch = true
|
||||
this.type = 'search'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus()
|
||||
})
|
||||
},
|
||||
listenReplace () {
|
||||
this.showSearch = true
|
||||
this.type = 'replace'
|
||||
},
|
||||
listenFindNext () {
|
||||
this.find('next')
|
||||
},
|
||||
listenFindPrev () {
|
||||
this.find('prev')
|
||||
},
|
||||
docKeyup (event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.emitSearch(true)
|
||||
}
|
||||
},
|
||||
docClick (isSelect) {
|
||||
if (!this.showSearch) return
|
||||
this.emitSearch()
|
||||
},
|
||||
emitSearch (selectHighlight = false) {
|
||||
this.showSearch = false
|
||||
const searchValue = this.searchValue = ''
|
||||
this.replaceValue = ''
|
||||
bus.$emit('searchValue', searchValue, { selectHighlight })
|
||||
},
|
||||
caseClick () {
|
||||
this.caseSensitive = !this.caseSensitive
|
||||
},
|
||||
typeClick () {
|
||||
this.type = this.type === 'search' ? 'replace' : 'search'
|
||||
},
|
||||
find (action) {
|
||||
bus.$emit('find', action)
|
||||
},
|
||||
search (event) {
|
||||
if (event.key === 'Escape') return
|
||||
if (event.key !== 'Enter') {
|
||||
const { caseSensitive } = this
|
||||
bus.$emit('searchValue', this.searchValue, { caseSensitive })
|
||||
} else {
|
||||
this.find('next')
|
||||
}
|
||||
},
|
||||
replace (isSingle = true) {
|
||||
const { caseSensitive, replaceValue } = this
|
||||
bus.$emit('replaceValue', replaceValue, { caseSensitive, isSingle })
|
||||
},
|
||||
noop () {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
padding: 5px;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
box-shadow: 0 4px 8px 0 var(--floatBorderColor);
|
||||
border: 1px solid var(--floatBorderColor);
|
||||
border-radius: 5px;
|
||||
background: var(--floatBgColor);
|
||||
}
|
||||
.search {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.search, .replace {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
padding: 4px 10px 0 10px;
|
||||
}
|
||||
.search-bar .button {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
color: var(--sideBarIconColor);
|
||||
&.left {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&.right {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.button.active {
|
||||
color: var(--themeColor);
|
||||
}
|
||||
.search-bar .button > svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.search-bar .button:active {
|
||||
opacity: .5;
|
||||
}
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin-right: 5px;
|
||||
background: var(--floatHoverColor);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.input-wrapper .search-result {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--sideBarTitleColor);
|
||||
}
|
||||
.input-wrapper input {
|
||||
flex: 1;
|
||||
padding: 0 8px;
|
||||
height: 30px;
|
||||
outline: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
color: var(--editorColor);
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
449
src/renderer/components/search/index.vue
Normal file
449
src/renderer/components/search/index.vue
Normal file
@ -0,0 +1,449 @@
|
||||
<template>
|
||||
<div class="search-bar"
|
||||
@click.stop="noop"
|
||||
v-show="showSearch"
|
||||
>
|
||||
<div
|
||||
class="left-arrow"
|
||||
@click="toggleSearchType"
|
||||
>
|
||||
<svg
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
:class="{'arrow-right': type === 'search'}"
|
||||
>
|
||||
<use xlink:href="#icon-arrowdown"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="right-controls">
|
||||
<section class="search">
|
||||
<div
|
||||
class="input-wrapper"
|
||||
:class="{'error': !!searchErrorMsg}"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchValue"
|
||||
@keyup="search($event)"
|
||||
ref="search"
|
||||
placeholder="Search"
|
||||
>
|
||||
<div class="controls">
|
||||
<span class="search-result">{{`${highlightIndex + 1} / ${highlightCount}`}}</span>
|
||||
<span
|
||||
title="Case Sensitive"
|
||||
class="is-case-sensitive"
|
||||
:class="{'active': isCaseSensitive}"
|
||||
@click.stop="toggleCtrl('isCaseSensitive')"
|
||||
>
|
||||
<svg :viewBox="FindCaseIcon.viewBox" aria-hidden="true">
|
||||
<use :xlink:href="FindCaseIcon.url" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
title="Select whole word"
|
||||
class="is-whole-word"
|
||||
:class="{'active': isWholeWord}"
|
||||
@click.stop="toggleCtrl('isWholeWord')"
|
||||
>
|
||||
<svg :viewBox="FindWordIcon.viewBox" aria-hidden="true">
|
||||
<use :xlink:href="FindWordIcon.url" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
title="Use query as RegEx"
|
||||
class="is-regex"
|
||||
:class="{'active': isRegexp}"
|
||||
@click.stop="toggleCtrl('isRegexp')"
|
||||
>
|
||||
<svg :viewBox="FindRegexIcon.viewBox" aria-hidden="true">
|
||||
<use :xlink:href="FindRegexIcon.url" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="error-msg" v-if="searchErrorMsg">
|
||||
{{searchErrorMsg}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="button right" @click="find('prev')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-arrow-up"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="button" @click="find('next')">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-arrowdown"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="replace" v-if="type === 'replace'">
|
||||
<div class="input-wrapper replace-input">
|
||||
<input type="text" v-model="replaceValue" placeholder="Replacement">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<el-tooltip class="item"
|
||||
effect="dark"
|
||||
content="Replace All"
|
||||
placement="top"
|
||||
:visible-arrow="false"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<button class="button right" @click="replace(false)">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-all-inclusive"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item"
|
||||
effect="dark"
|
||||
content="Replace Single"
|
||||
placement="top"
|
||||
:visible-arrow="false"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<button class="button" @click="replace(true)">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-replace"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../../bus'
|
||||
import { mapState } from 'vuex'
|
||||
import FindCaseIcon from '@/assets/icons/searchIcons/iconCase.svg'
|
||||
import FindWordIcon from '@/assets/icons/searchIcons/iconWord.svg'
|
||||
import FindRegexIcon from '@/assets/icons/searchIcons/iconRegex.svg'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
this.FindCaseIcon = FindCaseIcon
|
||||
this.FindWordIcon = FindWordIcon
|
||||
this.FindRegexIcon = FindRegexIcon
|
||||
return {
|
||||
showSearch: false,
|
||||
isCaseSensitive: false,
|
||||
isWholeWord: false,
|
||||
isRegexp: false,
|
||||
type: 'search',
|
||||
searchValue: '',
|
||||
replaceValue: '',
|
||||
searchErrorMsg: ''
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchMatches: function (newValue, oldValue) {
|
||||
if (!newValue || !oldValue) return
|
||||
const { value } = newValue
|
||||
if (value && value !== oldValue.value) {
|
||||
this.searchValue = value
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
searchMatches: state => state.editor.currentFile.searchMatches
|
||||
}),
|
||||
highlightIndex () {
|
||||
if (this.searchMatches) {
|
||||
return this.searchMatches.index
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
},
|
||||
highlightCount () {
|
||||
if (this.searchMatches) {
|
||||
return this.searchMatches.matches.length
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
bus.$on('find', this.listenFind)
|
||||
bus.$on('replace', this.listenReplace)
|
||||
bus.$on('findNext', this.listenFindNext)
|
||||
bus.$on('findPrev', this.listenFindPrev)
|
||||
document.addEventListener('click', this.docClick)
|
||||
document.addEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
bus.$off('find', this.listenFind)
|
||||
bus.$off('replace', this.listenReplace)
|
||||
bus.$off('findNext', this.listenFindNext)
|
||||
bus.$off('findPrev', this.listenFindPrev)
|
||||
document.removeEventListener('click', this.docClick)
|
||||
document.removeEventListener('keyup', this.docKeyup)
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleCtrl (ctrl) {
|
||||
this[ctrl] = !this[ctrl]
|
||||
this.search()
|
||||
},
|
||||
|
||||
listenFind () {
|
||||
this.showSearch = true
|
||||
this.type = 'search'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus()
|
||||
if (this.searchValue) {
|
||||
this.search()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
listenReplace () {
|
||||
this.showSearch = true
|
||||
this.type = 'replace'
|
||||
},
|
||||
|
||||
listenFindNext () {
|
||||
this.find('next')
|
||||
},
|
||||
|
||||
listenFindPrev () {
|
||||
this.find('prev')
|
||||
},
|
||||
|
||||
docKeyup (event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.emptySearch(true)
|
||||
}
|
||||
},
|
||||
|
||||
docClick () {
|
||||
if (!this.showSearch) return
|
||||
this.emptySearch()
|
||||
},
|
||||
|
||||
emptySearch (selectHighlight = false) {
|
||||
this.showSearch = false
|
||||
const searchValue = this.searchValue = ''
|
||||
this.replaceValue = ''
|
||||
bus.$emit('searchValue', searchValue, { selectHighlight })
|
||||
},
|
||||
|
||||
toggleSearchType () {
|
||||
this.type = this.type === 'search' ? 'replace' : 'search'
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the previous or next search result.
|
||||
* action: prev or next
|
||||
*/
|
||||
find (action) {
|
||||
bus.$emit('find-action', action)
|
||||
},
|
||||
|
||||
search (event) {
|
||||
if (event && event.key === 'Escape') {
|
||||
return
|
||||
}
|
||||
|
||||
if (event && event.key === 'Enter') {
|
||||
return this.find('next')
|
||||
}
|
||||
|
||||
const { searchValue, isCaseSensitive, isWholeWord, isRegexp } = this
|
||||
if (isRegexp) {
|
||||
// Handle invalid regexp.
|
||||
try {
|
||||
new RegExp(searchValue)
|
||||
this.searchErrorMsg = ''
|
||||
} catch (err) {
|
||||
this.searchErrorMsg = `Invalid regular expression: /${searchValue}/.`
|
||||
return
|
||||
}
|
||||
// Handle match empty string, no need to search.
|
||||
try {
|
||||
const SEARCH_REG = new RegExp(searchValue)
|
||||
if (searchValue && SEARCH_REG.test('')) {
|
||||
throw new Error()
|
||||
}
|
||||
this.searchErrorMsg = ''
|
||||
} catch (err) {
|
||||
this.searchErrorMsg = `RegExp: /${searchValue}/ match empty string.`
|
||||
return
|
||||
}
|
||||
}
|
||||
bus.$emit('searchValue', searchValue, {
|
||||
isCaseSensitive,
|
||||
isWholeWord,
|
||||
isRegexp
|
||||
})
|
||||
},
|
||||
|
||||
replace (isSingle = true) {
|
||||
const { replaceValue, isCaseSensitive, isWholeWord, isRegexp } = this
|
||||
bus.$emit('replaceValue', replaceValue, {
|
||||
isSingle,
|
||||
isCaseSensitive,
|
||||
isWholeWord,
|
||||
isRegexp
|
||||
})
|
||||
},
|
||||
|
||||
noop () {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
padding: 0;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
border-radius: 3px;
|
||||
box-shadow: var(--floatShadow);
|
||||
background: var(--floatBgColor);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.search-bar .left-arrow {
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-bar .left-arrow:hover {
|
||||
background: var(--floatHoverColor);
|
||||
}
|
||||
.search-bar .left-arrow svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
.search-bar .left-arrow svg.arrow-right {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.search-bar .right-controls {
|
||||
flex: 1;
|
||||
}
|
||||
.search, .replace {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
padding: 4px 10px 0 4px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.search-bar .button {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
color: var(--sideBarIconColor);
|
||||
&.left {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&.right {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.button.active {
|
||||
color: var(--themeColor);
|
||||
}
|
||||
.search-bar .button > svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.search-bar .button:active {
|
||||
opacity: .5;
|
||||
}
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border: 1px solid var(--inputBgColor);
|
||||
background: var(--inputBgColor);
|
||||
border-radius: 3px;
|
||||
overflow: visible;
|
||||
}
|
||||
.input-wrapper.error {
|
||||
border: 1px solid var(--notificationErrorBg);
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.input-wrapper .controls {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
color: var(--sideBarTitleColor);
|
||||
& > span.search-result {
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
line-height: 17px;
|
||||
}
|
||||
& > span:not(.search-result) {
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
&:hover {
|
||||
color: var(--sideBarIconColor);
|
||||
}
|
||||
& > svg {
|
||||
fill: var(--sideBarIconColor);
|
||||
&:hover {
|
||||
fill: var(--highlightThemeColor);
|
||||
}
|
||||
}
|
||||
&.active svg {
|
||||
fill: var(--highlightThemeColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper .error-msg {
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
width: calc(100% + 2px);
|
||||
height: 28px;
|
||||
left: -1px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
background: var(--notificationErrorBg);
|
||||
line-height: 28px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.input-wrapper input {
|
||||
flex: 1;
|
||||
padding: 0 8px;
|
||||
height: 26px;
|
||||
outline: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
color: var(--editorColor);
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
@ -100,8 +100,8 @@
|
||||
<script>
|
||||
import { ipcRenderer, remote } from 'electron'
|
||||
import { mapState } from 'vuex'
|
||||
import { minimizePath, restorePath, maximizePath, closePath } from '../assets/window-controls.js'
|
||||
import { PATH_SEPARATOR } from '../config'
|
||||
import { minimizePath, restorePath, maximizePath, closePath } from '../../assets/window-controls.js'
|
||||
import { PATH_SEPARATOR } from '../../config'
|
||||
import { isOsx } from '@/util'
|
||||
|
||||
export default {
|
@ -902,6 +902,7 @@ const actions = {
|
||||
|
||||
SELECTION_CHANGE ({ commit }, changes) {
|
||||
const { start, end } = changes
|
||||
// Set search keyword to store.
|
||||
if (start.key === end.key && start.block.text) {
|
||||
const value = start.block.text.substring(start.offset, end.offset)
|
||||
commit('SET_SEARCH', {
|
||||
|
19
yarn.lock
19
yarn.lock
@ -2552,6 +2552,13 @@ clone-deep@^4.0.1:
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clone-regexp@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
|
||||
integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==
|
||||
dependencies:
|
||||
is-regexp "^2.0.0"
|
||||
|
||||
clone-response@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
|
||||
@ -4797,6 +4804,13 @@ execa@^1.0.0:
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execall@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45"
|
||||
integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==
|
||||
dependencies:
|
||||
clone-regexp "^2.1.0"
|
||||
|
||||
exit-hook@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
|
||||
@ -6498,6 +6512,11 @@ is-regexp@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
|
||||
integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
|
||||
|
||||
is-regexp@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
|
||||
integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
Loading…
Reference in New Issue
Block a user