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:
Ran Luo 2019-10-06 08:42:06 +08:00 committed by GitHub
parent 2f65f6cec0
commit 4d728a500e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 540 additions and 338 deletions

View File

@ -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",

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -24,7 +24,7 @@
</template>
<script>
import bus from '../bus'
import bus from '../../bus'
import { mapState } from 'vuex'
export default {

View File

@ -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>

View 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>

View File

@ -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 {

View File

@ -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', {

View File

@ -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"