feat: add advance search

This commit is contained in:
purocean
2022-11-20 10:42:58 +08:00
parent 06055c6aa0
commit a824e861bf
25 changed files with 1164 additions and 51 deletions
+1
View File
@@ -64,6 +64,7 @@ jobs:
- name: Electron-Mac
if: matrix.platform == 'mac'
run: |
sh ./scripts/download-ripgrep.sh
yarn run electron-builder --${{ matrix.platform }} --x64 -p never | sed 's/identityName=.*$//'
find ./out -regex '.*app.asar.unpacked/node_modules/node-pty/build/Release/pty.node$' | grep pty.node
mv out/latest-mac.yml out/latest-mac-x64.yml
+3 -1
View File
@@ -24,6 +24,7 @@
},
"dependencies": {
"@electron/remote": "^2.0.1",
"@vscode/ripgrep": "^1.14.2",
"adm-zip": "^0.5.9",
"command-exists": "^1.2.9",
"dayjs": "^1.10.5",
@@ -47,6 +48,7 @@
"pako": "^2.0.4",
"plantuml-pipe": "^1.4.0",
"request": "^2.88.2",
"ripgrep-wrapper": "^1.1.1",
"safe-buffer": "^5.2.1",
"socket.io": "^2.4.0",
"socks-proxy-agent": "^6.1.1",
@@ -72,7 +74,7 @@
"@types/lodash-es": "^4.17.4",
"@types/markdown-it": "^12.2.3",
"@types/mime": "^2.0.3",
"@types/node": "^15.12.2",
"@types/node": "^18.11.9",
"@types/pako": "^1.0.3",
"@types/request": "^2.48.5",
"@types/socket.io-client": "^1.4.34",
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
set -e
# Runing only on Darwin
if [ "$(uname)" != "Darwin" ]; then
exit 0
fi
NODE_MODULES_PATH="$(dirname "$0")/../node_modules"
BIN_PATH="$(dirname "$0")/../bin"
# Download x64 ripgrep
export npm_config_arch=x64
node "$NODE_MODULES_PATH/@vscode/ripgrep/lib/postinstall.js" --force
mv "$NODE_MODULES_PATH/@vscode/ripgrep/bin/rg" "$BIN_PATH/rg-darwin-x64"
# Download arm64 ripgrep
export npm_config_arch=arm64
node "$NODE_MODULES_PATH/@vscode/ripgrep/lib/postinstall.js" --force
mv "$NODE_MODULES_PATH/@vscode/ripgrep/bin/rg" "$BIN_PATH/rg-darwin-arm64"
+2
View File
@@ -59,6 +59,8 @@ export async function transformProtocolRequest (request: ProtocolRequest) {
const res = new ServerResponse(req)
res.write = out.write.bind(out)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
res.end = out.end.bind(out)
return { req, res, out }
+4 -5
View File
@@ -9,6 +9,7 @@ import request from 'request'
import { promisify } from 'util'
import { STATIC_DIR, HOME_DIR, HELP_DIR, USER_PLUGIN_DIR, FLAG_DISABLE_SERVER, APP_NAME, USER_THEME_DIR, RESOURCES_DIR, BUILD_IN_STYLES, USER_EXTENSION_DIR } from '../constant'
import * as file from './file'
import * as search from './search'
import run from './run'
import convert from './convert'
import plantuml from './plantuml'
@@ -175,11 +176,9 @@ const attachment = async (ctx: any, next: any) => {
}
const searchFile = async (ctx: any, next: any) => {
if (ctx.path.startsWith('/api/search')) {
const search = ctx.query.search
const repo = ctx.query.repo
ctx.body = result('ok', 'success', await file.search(repo, search))
if (ctx.path.startsWith('/api/search') && ctx.method === 'POST') {
const query = ctx.request.body.query
ctx.body = await search.search(query)
} else {
await next()
}
+150
View File
@@ -0,0 +1,150 @@
import os from 'os'
import { ReadableStream } from 'stream/web'
import { Readable } from 'node:stream'
import { CancellationTokenSource, ITextQuery, TextSearchEngineAdapter } from 'ripgrep-wrapper'
import { rgPath } from '@vscode/ripgrep'
import { SearchMessage } from '../../share/typings'
import { BIN_DIR } from '../constant'
import { convertAppPath } from '../helper'
let rgDiskPath: string
if (os.platform() === 'darwin') {
rgDiskPath = BIN_DIR + '/rg-darwin-' + os.arch()
} else {
rgDiskPath = convertAppPath(rgPath)
}
function isReadableEnded (stream: any) {
if (stream.readableEnded === true) return true
const rState = stream._readableState
if (!rState || rState.errored) return false
if (typeof rState?.ended !== 'boolean') return null
return rState.ended
}
/**
* from https://github.com/nodejs/node/blob/ba67fe66eb7777d5055c785be153374843fc647e/lib/internal/streams/readable.js
* @param {ReadableStream} readableStream
* @param {{
* highWaterMark? : number,
* encoding? : string,
* objectMode? : boolean,
* signal? : AbortSignal,
* }} [options]
* @returns {Readable}
*/
function newStreamReadableFromReadableStream (readableStream: ReadableStream, options: any = {}) {
const reader = readableStream.getReader()
let closed = false
const readable = new Readable({
...options,
read () {
reader.read().then((chunk) => {
if (chunk.done) {
// Value should always be undefined here.
readable.push(null)
} else {
readable.push(chunk.value)
}
},
(error) => readable.destroy(error))
},
destroy (error, callback) {
function done () {
try {
callback(error)
} catch (error) {
// In a next tick because this is happening within
// a promise context, and if there are any errors
// thrown we don't want those to cause an unhandled
// rejection. Let's just escape the promise and
// handle it separately.
process.nextTick(() => { throw error })
}
}
if (!closed) {
reader.cancel().then(done, done)
return
}
done()
},
})
reader.closed.then(
() => {
closed = true
if (!isReadableEnded(readable)) { readable.push(null) }
},
(error) => {
closed = true
readable.destroy(error)
}
)
return readable
}
export async function search (query: ITextQuery) {
const cts = new CancellationTokenSource()
const cancel = () => {
cts.cancel()
}
const stream = new ReadableStream({
start (controller) {
if (cts.token.isCancellationRequested) {
controller.close()
return
}
const close = () => {
try {
controller.close()
} catch {
// ignore
}
}
const _enqueue = (chunk: any) => {
if (cts.token.isCancellationRequested) {
close()
} else {
controller.enqueue(chunk)
}
}
const enqueue = <T extends 'result' | 'message' | 'done' | 'error'> (type: T, payload: SearchMessage<T>['payload']) => {
const message: SearchMessage<T> = { type, payload }
_enqueue(JSON.stringify(message) + '\n')
}
const adapter = new TextSearchEngineAdapter(rgDiskPath, query)
adapter.search(cts.token, (res) => {
enqueue('result', res)
}, message => {
enqueue('message', message)
}).then((success) => {
enqueue('done', success)
_enqueue(null)
close()
}, (err) => {
enqueue('error', err)
_enqueue(null)
close()
})
}
})
// Readable.fromWeb only available in node 17
// const result = Readable.fromWeb(stream)
const result = newStreamReadableFromReadableStream(stream)
result.once('close', cancel)
result.once('error', cancel)
return result
}
+8 -4
View File
@@ -7,12 +7,15 @@
<svg-icon v-else name="list" />
</div>
</div>
<div v-if="!showOutline">
<template v-if="!showOutline">
<div class="btn flat" @click="showSortMenu()" :title="$t(('tree.sort.by-' + treeSort.by) as any, $t(('tree.sort.' + treeSort.order) as any))">
<svg-icon v-if="treeSort.order === 'asc'" name="arrow-up-wide-short-solid" />
<svg-icon v-else name="arrow-down-short-wide-solid" />
</div>
</div>
<div class="btn flat" @click="findInFolder()" :title="$t('search-panel.search-files') + ' ' + getKeysLabel('tree.find-in-folder')">
<svg-icon name="search-solid" />
</div>
</template>
</div>
<div class="title">{{$t(showOutline ? 'outline' : 'files')}}</div>
<div class="btns" v-if="navigation">
@@ -37,11 +40,12 @@ import { registerAction, removeAction } from '@fe/core/action'
import { getSchema, Schema } from '@fe/services/control-center'
import { useContextMenu } from '@fe/support/ui/context-menu'
import type { AppState } from '@fe/support/store'
import SvgIcon from './SvgIcon.vue'
import { useI18n } from '@fe/services/i18n'
import { toggleOutline } from '@fe/services/layout'
import { findInFolder } from '@fe/services/tree'
import { getKeysLabel } from '@fe/core/command'
import { FileSort } from '@fe/types'
import type { FileSort } from '@fe/types'
import SvgIcon from './SvgIcon.vue'
const store = useStore<AppState>()
const navigation = ref<Schema['navigation']>()
+1 -30
View File
@@ -46,10 +46,8 @@
</template>
<script lang="ts">
import { debounce } from 'lodash-es'
import { computed, defineComponent, nextTick, onMounted, ref, toRefs, watch } from 'vue'
import { useStore } from 'vuex'
import * as api from '@fe/support/api'
import { useI18n } from '@fe/services/i18n'
import fuzzyMatch from '@fe/others/fuzzy-match'
import { fetchSettings } from '@fe/services/setting'
@@ -79,20 +77,16 @@ export default defineComponent({
const refFilepath = ref<HTMLElement[]>([])
const markedFiles = ref<PathItem[]>(markedFilesCache)
const { currentRepo, recentOpenTime, tree } = toRefs(store.state)
const { recentOpenTime, tree } = toRefs(store.state)
const selected = ref<any>(null)
const searchText = ref('')
const currentTab = ref<TabKey>(lastTab)
const list = ref<any>([])
const lastFetchTime = ref(0)
const repo = computed(() => currentRepo.value?.name)
const tabs = computed(() => {
const arr: {key: TabKey; label: string}[] = [
{ key: 'file', label: t('quick-open.files') },
{ key: 'search', label: t('quick-open.search') },
]
if (props.withMarked) {
@@ -168,20 +162,6 @@ export default defineComponent({
return sortList(arr).slice(0, 70)
})
const searchWithDebounce = debounce(async (text: string, call: Function) => {
if (repo.value && text.trim()) {
const fetchTime = new Date().getTime()
lastFetchTime.value = fetchTime
const data = await api.search(repo.value, text.trim())
// ensure last result be ahead of list.
if (fetchTime >= lastFetchTime.value) {
call(data)
}
} else {
call([])
}
}, 500)
function highlightText (search: string) {
if (refFilename.value && refFilepath.value) {
search = search.toLowerCase()
@@ -221,15 +201,6 @@ export default defineComponent({
list.value = files.value
} else if (currentTab.value === 'marked') {
list.value = markedFiles.value
} else if (currentTab.value === 'search') {
const keyword = searchText.value.trim()
list.value = keyword ? null : []
searchWithDebounce(keyword, (data: any[]) => {
if (currentTab.value === 'search') {
list.value = data
}
})
}
}
+684
View File
@@ -0,0 +1,684 @@
<template>
<transition name="search-panel-wrapper">
<div v-show="visible" class="search-panel-wrapper" @keydown.esc="close">
<transition name="search-panel">
<div v-if="visible" class="search-panel">
<div class="title">{{$t('search-panel.search-files')}}</div>
<div class="close-btn" @click="close" :title="$t('close')">
<svg-icon class="close-btn-icon" name="times" width="8px" />
</div>
<div class="search">
<div class="search-row">
<textarea
class="search-input search-pattern"
ref="patternInputRef"
v-model="pattern"
type="text"
rows="1"
v-up-down-history
v-placeholder="{
blur: $t('search-panel.placeholder-search'),
focus: $t('search-panel.placeholder-search') + ' ' + $t('search-panel.for-history')
}"
v-auto-resize="{ maxRows: 6, minRows: 1 }"
@keydown.enter.prevent="onKeydownEnter"
/>
<div class="option-btns">
<div
:class="{'option-btn': true, active: option.isCaseSensitive}"
:title="$t('search-panel.match-case')"
@click="toggleOption('isCaseSensitive')
">
<svg-icon name="codicon-case-sensitive" width="15px" />
</div>
<div
:class="{'option-btn': true, active: option.isWordMatch}"
:title="$t('search-panel.match-whole-word')"
@click="toggleOption('isWordMatch')"
>
<svg-icon name="codicon-whole-word" width="15px" />
</div>
<div
:class="{'option-btn': true, active: option.isRegExp}"
:title="$t('search-panel.use-regex')"
@click="toggleOption('isRegExp')"
>
<svg-icon name="codicon-regex" width="15px" />
</div>
</div>
</div>
<div class="search-input-label">{{$t('search-panel.files-to-include')}}</div>
<input
class="search-input"
type="text"
v-model="include"
@keydown.enter.prevent="onKeydownEnter"
v-up-down-history
v-placeholder="{
blur: '',
focus: 'e.g. foo/**/include ' + $t('search-panel.for-history')
}"
/>
<div class="search-input-label">{{$t('search-panel.files-to-exclude')}}</div>
<input
class="search-input"
type="text"
v-model="exclude"
@keydown.enter.prevent="onKeydownEnter"
v-up-down-history
v-placeholder="{
blur: '',
focus: 'e.g. bar/**/exclude ' + $t('search-panel.for-history')
}"
/>
</div>
<div class="message-wrapper">
<div class="message">{{message}}</div>
<a v-show="loading" class="action-btn" href="javascript:void(0)" @click="stop">{{$t('cancel')}}</a>
</div>
<div class="results" v-if="result.length > 0">
<details class="item" v-for="item in result" :key="item.path" open>
<summary :title="item.path">
<div class="item-info">
<span class="item-name">{{basename(item.path)}}</span>
<span class="item-dir">{{dirname(item.path)}}</span>
</div>
<div class="item-count">{{item.numMatches}}</div>
</summary>
<div class="matches">
<div
:class="{match: true, active: currentItemKey === match.key}"
v-for="match of (item.results as any)"
:key="match.key"
@click="chooseMatch(item as any, match, 0)"
>
<component
v-for="(fragment, i) in markText(match.preview.text, match.preview.matches)"
:key="i"
:is="fragment.type">{{fragment.value}}</component>
</div>
</div>
</details>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, reactive, ref, shallowRef, watch, watchEffect } from 'vue'
import type { ISearchRange, ISerializedFileMatch, ISerializedSearchSuccess, ITextQuery, ITextSearchMatch } from 'ripgrep-wrapper'
import { getLogger, sleep } from '@fe/utils'
import { basename, dirname, join, relative } from '@fe/utils/path'
import { registerAction, removeAction } from '@fe/core/action'
import { CtrlCmd, Shift } from '@fe/core/command'
import * as api from '@fe/support/api'
import store from '@fe/support/store'
import { useToast } from '@fe/support/ui/toast'
import { switchDoc } from '@fe/services/document'
import { getIsDefault, highlightLine } from '@fe/services/editor'
import SvgIcon from './SvgIcon.vue'
import { useI18n } from '@fe/services/i18n'
const MAX_RESULTS = 200
const logger = getLogger('search-panel')
const toast = useToast()
useI18n()
const patternInputRef = ref<HTMLInputElement>()
const pattern = ref('')
const include = ref('')
const exclude = ref('')
const option = reactive({
isRegExp: false,
isWordMatch: false,
isCaseSensitive: false,
})
const loading = ref(false)
const result = shallowRef<(ISerializedFileMatch)[]>([])
const success = shallowRef<ISerializedSearchSuccess | null>(null)
const currentItemKey = ref('')
const visible = ref(false)
const message = computed(() => {
if (result.value.length === 0) {
return success.value ? 'No results found' : ''
}
const results = result.value.reduce((acc, cur) => acc + (cur.numMatches || 0), 0)
if (success?.value?.limitHit) {
return `${results} results (limited) in ${result.value.length} files`
} else {
return `${results} results in ${result.value.length} files`
}
})
watchEffect(async () => {
if (visible.value) {
await nextTick()
patternInputRef.value?.focus()
patternInputRef.value?.select()
}
})
watch(() => store.state.currentRepo, () => {
stop()
result.value = []
})
let controller: AbortController | null = null
async function stop () {
logger.debug('stop')
success.value = null
if (controller) {
controller.abort()
sleep(100)
controller = null
}
}
async function search () {
const folder = store.state.currentRepo?.path
const repo = store.state.currentRepo?.name
if (!folder || !repo) {
toast.show('warning', 'Please choose a repository first')
return
}
await stop()
if (!pattern.value) {
loading.value = false
result.value = []
return
}
const buildGlobObject = (str: string) => {
const obj: Record<string, boolean> = {}
str.split(',')
.map(s => s.trim())
.filter(Boolean)
.forEach(s => { obj[s] = true })
return obj
}
controller = new AbortController()
const query: ITextQuery = {
contentPattern: {
pattern: pattern.value,
isRegExp: option.isRegExp,
isWordMatch: option.isWordMatch,
isCaseSensitive: option.isCaseSensitive,
isMultiline: pattern.value.includes('\n')
},
folderQueries: [
{
folder,
includePattern: buildGlobObject('**/*.md,' + include.value),
excludePattern: buildGlobObject(exclude.value),
},
],
maxResults: MAX_RESULTS,
}
try {
loading.value = true
result.value = []
const receiveResult = await api.search(controller, query)
success.value = await receiveResult(
(data) => {
result.value = [
...result.value,
...data.map((item) => ({
repo,
numMatches: item.numMatches,
results: (item.results!).map((match: any, i) => ({
...match,
key: `${item.path}:${i}`,
})),
path: join('/', relative(folder, item.path)),
})),
]
},
(data) => {
logger.debug('onMessage', data)
},
)
} finally {
loading.value = false
}
}
function toggleOption (key: keyof typeof option) {
option[key] = !option[key]
search()
}
function close () {
visible.value = false
stop()
}
async function chooseMatch (result: ISerializedFileMatch & { repo: string }, match: ITextSearchMatch & { key: string }, idx: number) {
const { path, repo } = result
const range = (match.ranges as ISearchRange[])[idx]
if (!range) {
return
}
currentItemKey.value = match.key
const lines: [number, number] = [
range.startLineNumber + 1,
range.endLineNumber + 1,
]
logger.debug('chooseMatch', path, lines)
await switchDoc({ type: 'file', path, repo, name: basename(path) })
if (getIsDefault()) {
await sleep(100)
highlightLine(lines, true, 1000)
}
}
function onKeydownEnter (e: KeyboardEvent) {
if (e.isComposing) {
return
}
const target = e.target as HTMLInputElement
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
const start = target.selectionStart
const end = target.selectionEnd
const content = target.value
if (start !== null && end !== null) {
target.value = content.slice(0, start) + '\n' + content.slice(end)
target.dispatchEvent(new Event('input'))
}
} else {
search()
}
}
function markText (text: string, ranges: ISearchRange[]) {
const lines = text.split('\n')
const result: {type: 'span' | 'mark' | 'br', value?: string }[] = []
let lastLine = 0
let lastColumn = 0
for (const range of ranges) {
const start = range.startLineNumber
const end = range.endLineNumber
const startOffset = range.startColumn
const endOffset = range.endColumn
if (start < lastLine) {
continue
}
if (start === lastLine && startOffset < lastColumn) {
continue
}
// process previous lines
if (start > lastLine) {
const lastTail = lines[lastLine].slice(lastColumn)
lastTail && result.push({ type: 'span', value: lastTail })
result.push({ type: 'br' })
const prevLines = lines.slice(lastLine + 1, start)
prevLines.forEach((line) => {
line && result.push({ type: 'span', value: line })
result.push({ type: 'br' })
})
}
// process current range lines
const currentStartLine = lines[start]
const currentStartLinePrefix = currentStartLine.slice(0, startOffset)
currentStartLinePrefix && result.push({ type: 'span', value: currentStartLine.slice(0, startOffset) })
if (start === end) {
const startLineMarked = currentStartLine.slice(startOffset, endOffset)
startLineMarked && result.push({ type: 'mark', value: startLineMarked })
} else {
const startLineMarked = currentStartLine.slice(startOffset)
startLineMarked && result.push({ type: 'mark', value: startLineMarked })
result.push({ type: 'br' })
const currentMiddleLines = lines.slice(start + 1, end)
currentMiddleLines.forEach((line) => {
line && result.push({ type: 'mark', value: line })
result.push({ type: 'br' })
})
const currentEndLine = lines[end]
const endLineMarked = currentEndLine.slice(0, endOffset)
endLineMarked && result.push({ type: 'mark', value: endLineMarked })
}
lastLine = end
lastColumn = endOffset
}
if (lastLine < lines.length - 1) {
const lastTail = lines[lastLine].slice(lastColumn)
lastTail && result.push({ type: 'span', value: lastTail })
result.push({ type: 'br' })
const restLines = lines.slice(lastLine + 1)
restLines.forEach((line) => {
line && result.push({ type: 'span', value: line })
result.push({ type: 'br' })
})
}
// remove end br
while (result[result.length - 1].type === 'br') {
result.pop()
}
return result
}
registerAction({
name: 'tree.find-in-folder',
keys: [CtrlCmd, Shift, 'f'],
handler: (path) => {
visible.value = true
if (path) {
include.value = path.replace(/^\//, '')
}
},
})
onBeforeUnmount(() => {
removeAction('tree.find-in-folder')
})
</script>
<style lang="scss" scoped>
@import '@fe/styles/mixins.scss';
.search-panel-wrapper {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(1.5px);
z-index: 10;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: flex-end;
overflow: hidden;
opacity: 1;
transition: opacity 0.2s cubic-bezier(1, 0.29, 0.63, 0.94);
}
.search-panel {
margin-top: 36px;
background: var(--g-background-color);
height: calc(100% - 36px);
width: 100%;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s cubic-bezier(1, 0.38, 0.58, 0.97);
.title {
text-align: center;
line-height: 30px;
font-size: 14px;
color: var(--g-color-20);
flex: none;
user-select: none;
}
.close-btn {
position: absolute;
right: 5px;
top: 5px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
color: var(--g-color-30);
&:hover {
color: var(--g-color-0);
background-color: var(--g-color-86);
border-radius: 50%;
}
}
.search {
padding: 6px 4px;
flex: none;
.search-input {
font-size: 13px;
padding: 4px;
border-radius: 2px;
background: var(--g-color-94);
resize: none;
outline: 1px solid var(--g-color-80);
&.search-pattern {
padding-right: 68px;
&::-webkit-scrollbar {
width: 4px;
}
}
&:focus {
background: var(--g-color-90);
}
}
.search-input-label {
font-size: 12px;
color: var(--g-color-30);
margin-top: 6px;
margin-bottom: 2px;
user-select: none;
}
}
}
.search-row {
position: relative;
.option-btns {
position: absolute;
right: 0;
top: 3px;
display: flex;
align-items: center;
.option-btn {
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
color: var(--g-color-30);
margin-right: 2px;
border-radius: var(--g-border-radius);
&:hover {
background-color: var(--g-color-80);
}
&.active {
color: var(--g-color-10);
background-color: var(--g-color-80);
outline: 1px solid var(--g-color-70);
outline-offset: -1px;
}
}
}
}
.message-wrapper {
flex: none;
display: flex;
justify-content: space-between;
font-size: 13px;
padding: 0 4px;
user-select: none;
.message {
color: var(--g-color-30);
overflow-wrap: break-word;
width: 100%;
overflow: hidden;
padding: 4px 0 ;
}
.action-btn {
text-decoration: none;
flex: none;
margin-left: 6px;
}
}
.results {
overflow-y: auto;
height: calc(100% - 40px);
margin-top: 2px;
padding-top: 6px;
border-top: 1px solid var(--g-color-90);
&::-webkit-scrollbar {
width: 4px;
}
details.item {
cursor: pointer;
& > summary {
padding: 4px 0;
display: flex;
align-items: center;
font-size: 14px;
user-select: none;
padding: 6px;
color: var(--g-color-10);
&::-webkit-details-marker,
&::marker {
content: '';
display: none;
}
&::before {
display: inline-block;
width: 10px;
content: url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhciIgZGF0YS1pY29uPSJjaGV2cm9uLWRvd24iIHJvbGU9ImltZyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDQ4IDUxMiIgPjxwYXRoIGZpbGw9IiM3YzdmODIiIGQ9Ik00NDEuOSAxNjcuM2wtMTkuOC0xOS44Yy00LjctNC43LTEyLjMtNC43LTE3IDBMMjI0IDMyOC4yIDQyLjkgMTQ3LjVjLTQuNy00LjctMTIuMy00LjctMTcgMEw2LjEgMTY3LjNjLTQuNyA0LjctNC43IDEyLjMgMCAxN2wyMDkuNCAyMDkuNGM0LjcgNC43IDEyLjMgNC43IDE3IDBsMjA5LjQtMjA5LjRjNC43LTQuNyA0LjctMTIuMyAwLTE3eiIgY2xhc3M9IiI+PC9wYXRoPjwvc3ZnPg==);
margin-right: 4px;
transform: rotate(-90deg);
transition: transform 0.1s;
}
&:hover {
background-color: var(--g-color-95);
}
.item-info {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.item-dir {
color: var(--g-color-50);
font-size: 12px;
margin-left: 10px;
}
}
.item-count {
flex: none;
background-color: var(--g-color-90);
line-height: 16px;
font-size: 13px;
box-sizing: border-box;
min-width: 16px;
text-align: center;
padding: 0 4px;
margin-left: 6px;
border-radius: 8px;
}
}
&[open] > summary::before {
transform: rotate(0);
}
.matches {
font-size: 16px;
.match {
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
box-sizing: border-box;
padding-left: 20px;
user-select: none;
overflow-wrap: break-word;
line-height: 17px;
font-size: 13px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
max-height: 58px;
color: var(--g-color-15);
&:hover {
background-color: var(--g-color-95);
color: var(--g-color-0);
}
&.active {
background-color: var(--g-color-90);
color: var(--g-color-0);
}
}
}
}
}
.search-panel-wrapper-leave-to,
.search-panel-wrapper-enter-from {
opacity: 0;
}
.search-panel-leave-to,
.search-panel-enter-from {
transform: translateY(80vh);
}
mark {
background: #fff8c5 !important;
}
@include dark-theme {
mark {
background: #746900 !important;
color: #ebebeb;
}
}
</style>
+4
View File
@@ -48,6 +48,10 @@ const icons: { [key: string]: string } = {
'moon-solid': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460 512"><path fill="currentColor" d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"/></svg>',
'sun-solid': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM352 256c0 53-43 96-96 96s-96-43-96-96s43-96 96-96s96 43 96 96zm32 0c0-70.7-57.3-128-128-128s-128 57.3-128 128s57.3 128 128 128s128-57.3 128-128z"/></svg>',
'circle-half-stroke-solid': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zm64 0c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z"/></svg>',
'bolt-solid': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288H175.5L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3s-16.6-20.7-30-20.7H272.5L349.4 44.6z"/></svg>',
'codicon-regex': '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z"/></svg>',
'codicon-case-sensitive': '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M8.85352 11.7021H7.85449L7.03809 9.54297H3.77246L3.00439 11.7021H2L4.9541 4H5.88867L8.85352 11.7021ZM6.74268 8.73193L5.53418 5.4502C5.49479 5.34277 5.4554 5.1709 5.41602 4.93457H5.39453C5.35872 5.15299 5.31755 5.32487 5.271 5.4502L4.07324 8.73193H6.74268Z"/><path d="M13.756 11.7021H12.8752V10.8428H12.8537C12.4706 11.5016 11.9066 11.8311 11.1618 11.8311C10.6139 11.8311 10.1843 11.686 9.87273 11.396C9.56479 11.106 9.41082 10.721 9.41082 10.2412C9.41082 9.21354 10.016 8.61556 11.2262 8.44727L12.8752 8.21631C12.8752 7.28174 12.4974 6.81445 11.7419 6.81445C11.0794 6.81445 10.4815 7.04004 9.94793 7.49121V6.58887C10.4886 6.24512 11.1117 6.07324 11.8171 6.07324C13.1097 6.07324 13.756 6.75716 13.756 8.125V11.7021ZM12.8752 8.91992L11.5485 9.10254C11.1403 9.15983 10.8324 9.26188 10.6247 9.40869C10.417 9.55192 10.3132 9.80794 10.3132 10.1768C10.3132 10.4453 10.4081 10.6655 10.5978 10.8374C10.7912 11.0057 11.0472 11.0898 11.3659 11.0898C11.8027 11.0898 12.1626 10.9377 12.4455 10.6333C12.7319 10.3254 12.8752 9.93685 12.8752 9.46777V8.91992Z"/></svg>',
'codicon-whole-word': '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 11H1V13H15V11H16V14H15H1H0V11Z"/><path d="M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"/><path d="M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"/></svg>',
}
export default defineComponent({
+38
View File
@@ -0,0 +1,38 @@
import { App, nextTick } from 'vue'
export function install (app: App) {
app.directive('auto-resize', {
mounted (el, binding) {
const { value } = binding
const { minRows, maxRows } = value
const style = window.getComputedStyle(el)
const lineHeight = parseFloat(style.lineHeight)
const paddingTop = parseFloat(style.paddingTop)
const paddingBottom = parseFloat(style.paddingBottom)
const min = minRows * lineHeight + paddingTop + paddingBottom
const max = maxRows * lineHeight + paddingTop + paddingBottom
const resize = () => {
el.style.height = 'auto'
el.style.overflowY = 'auto'
const height = el.scrollHeight
if (height < min) {
el.style.height = min + 'px'
el.style.overflowY = 'auto'
} else if (height > max) {
el.style.height = max + 'px'
el.style.overflowY = 'auto'
} else {
el.style.height = height + 'px'
el.style.overflowY = 'hidden'
}
}
el.addEventListener('input', resize)
nextTick(resize)
}
})
}
+10
View File
@@ -0,0 +1,10 @@
import { App } from 'vue'
import * as autoResize from './auto-resize'
import * as placeholder from './placeholder'
import * as upDownHistory from './up-down-history'
export default function (app: App) {
autoResize.install(app)
placeholder.install(app)
upDownHistory.install(app)
}
+24
View File
@@ -0,0 +1,24 @@
import { App } from 'vue'
export function install (app: App) {
app.directive('placeholder', {
mounted (el, binding) {
const { value } = binding
const { focus, blur } = value
if (el === document.activeElement) {
el.placeholder = focus
} else {
el.placeholder = blur
}
el.addEventListener('focus', () => {
el.placeholder = focus
})
el.addEventListener('blur', () => {
el.placeholder = blur
})
}
})
}
@@ -0,0 +1,72 @@
import { App } from 'vue'
export function install (app: App) {
app.directive('up-down-history', {
mounted (el, binding) {
const { value = {} } = binding
const { maxLength = 50 } = value
const history: string[] = []
let index = -1
const toggle = (e: KeyboardEvent) => {
if (history.length === 0) {
return
}
const target = e.target as HTMLInputElement
const offset = e.key === 'ArrowUp' ? -1 : 1
if (target.tagName === 'INPUT') {
e.preventDefault()
} else if (target.selectionStart === target.selectionEnd) {
const style = window.getComputedStyle(target)
const singleHeight = parseFloat(style.lineHeight) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
if (
(!target.value.includes('\n') && target.clientHeight < singleHeight + 4) ||
(offset < 0 && target.selectionStart === 0) ||
(offset > 0 && target.selectionEnd === target.value.length)
) {
e.preventDefault()
} else {
return
}
}
index += offset
if (index < 0) {
index = 0
} else if (index >= history.length) {
index = history.length - 1
}
el.value = history[index]
el.dispatchEvent(new Event('input'))
}
el.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.isComposing) {
return
}
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
toggle(e)
} else if (e.key === 'Enter') {
if (el.value && el.value !== history[index]) {
if (history.length >= maxLength) {
history.shift()
}
history.push(el.value)
index = history.length - 1
}
}
})
}
})
}
+2
View File
@@ -4,6 +4,7 @@ import { createApp } from 'vue'
import App from '@fe/App.vue'
import router from '@fe/router'
import directives from '@fe/directives'
import store from '@fe/support/store'
import toast from '@fe/support/ui/toast'
import modal from '@fe/support/ui/modal'
@@ -12,6 +13,7 @@ import quickFilter from '@fe/support/ui/quick-filter'
const app = createApp(App)
app.use(directives)
app.use(store)
app.use(router)
app.use(toast)
@@ -55,7 +55,8 @@ export default {
] : []),
{ id: 'refresh', label: t('tree.context-menu.refresh'), onClick: () => ctx.tree.refreshTree() },
...(node.type === 'dir' && !FLAG_DISABLE_XTERM ? [
{ id: 'open-in-terminal', label: t('tree.context-menu.open-in-terminal'), onClick: revealInXterminal }
{ id: 'open-in-terminal', label: t('tree.context-menu.open-in-terminal'), onClick: revealInXterminal },
{ id: 'find-in-folder', label: t('tree.context-menu.find-in-folder'), onClick: () => ctx.tree.findInFolder(node.path) },
] : []),
...(isMarkdown ? [
{ id: 'create-in-cd', label: t('tree.context-menu.create-in-cd'), onClick: () => ctx.doc.createDoc({ repo: node.repo }, node) }
@@ -108,7 +108,7 @@ export default {
},
{
type: 'btn',
icon: 'search-solid',
icon: 'bolt-solid',
flat: true,
title: ctx.i18n.t('control-center.navigation.goto', ctx.command.getKeysLabel('filter.show-quick-open')),
showInActionBar: true,
+8
View File
@@ -60,6 +60,14 @@ export function revealCurrentNode () {
getActionHandler('tree.reveal-current-node')()
}
/**
* Find in folder
* @param path
*/
export function findInFolder (path?: string) {
getActionHandler('tree.find-in-folder')(path)
}
store.watch(state => state.treeSort, async () => {
await refreshTree()
await nextTick()
+60 -6
View File
@@ -1,4 +1,6 @@
import type { IProgressMessage, ISerializedFileMatch, ISerializedSearchSuccess, ITextQuery } from 'ripgrep-wrapper'
import type { Components, Doc, ExportType, FileItem, FileSort, PathItem } from '@fe/types'
import type { SearchMessage } from '@share/typings'
import { isElectron } from '@fe/support/env'
import { JWT_TOKEN } from './args'
@@ -249,15 +251,67 @@ export async function choosePath (options: Record<string, any>): Promise<{ cance
return result.data
}
type SearchReturn = (
onResult: (result: ISerializedFileMatch[]) => void,
onMessage?: (message: IProgressMessage) => void
) => Promise<ISerializedSearchSuccess | null>
/**
* Search in a repository.
* @param repo
* @param text
* Search files.
* @param controller
* @param query
* @returns
*/
export async function search (repo: string, text: string): Promise<Pick<Doc, 'repo' | 'type' | 'path' | 'name'>> {
const result = await fetchHttp(`/api/search?repo=${encodeURIComponent(repo)}&search=${encodeURIComponent(text)}`)
return result.data
export async function search (controller: AbortController, query: ITextQuery): Promise<SearchReturn> {
const response = await fetchHttp('/api/search', {
signal: controller.signal,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
})
return async function (onResult, onMessage) {
let val = ''
let success: ISerializedSearchSuccess | null = null
const reader: ReadableStreamDefaultReader = response.body.getReader()
// read stream
while (true) {
const { done, value } = await reader.read()
if (done) {
return success
}
val += new TextDecoder().decode(value)
const idx = val.lastIndexOf('\n')
if (idx === -1) {
continue
}
const lines = val.slice(0, idx)
val = val.slice(idx + 1)
for (const line of lines.split('\n')) {
const data = JSON.parse(line)
switch (data.type) {
case 'result':
onResult((<SearchMessage<'result'>>data).payload)
break
case 'message':
onMessage?.((<SearchMessage<'message'>>data).payload)
break
case 'done':
success = (<SearchMessage<'done'>>data).payload
break
default:
throw (<SearchMessage<'error'>>data).payload
}
}
}
}
}
/**
+1
View File
@@ -254,6 +254,7 @@ export type BuildInActions = {
'control-center.refresh': () => void,
'tree.refresh': () => void,
'tree.reveal-current-node': () => void,
'tree.find-in-folder': (path?: string) => void,
'editor.toggle-wrap': () => void,
'editor.refresh-custom-editor': () => void,
'editor.trigger-save': () => void,
+3
View File
@@ -10,6 +10,7 @@
<ActionBar />
<Outline show-filter v-if="showOutline" />
<Tree v-show="!showOutline" />
<SearchPanel />
</template>
<template v-slot:terminal>
<Xterm @hide="hideXterm" />
@@ -58,6 +59,7 @@ import ControlCenter from '@fe/components/ControlCenter.vue'
import DocHistory from '@fe/components/DocHistory.vue'
import ActionBar from '@fe/components/ActionBar.vue'
import Outline from '@fe/components/Outline.vue'
import SearchPanel from '@fe/components/SearchPanel.vue'
import ExtensionManager from '@fe/components/ExtensionManager.vue'
export default defineComponent({
@@ -79,6 +81,7 @@ export default defineComponent({
DocHistory,
ActionBar,
Outline,
SearchPanel,
ExtensionManager,
},
setup () {
+11 -1
View File
@@ -290,6 +290,7 @@ const data = {
'create-in-cd': 'New File',
'copy-name': 'Copy Name',
'copy-path': 'Copy Path',
'find-in-folder': 'Find in Folder',
},
'toast': {
'moved': '[%s] Moved to [%s]',
@@ -407,7 +408,6 @@ const data = {
'input-placeholder': 'Type characters...',
'empty': 'Empty',
'files': 'Files',
'search': 'Search',
'marked': 'Marked',
},
'editor': {
@@ -596,6 +596,16 @@ const data = {
'help': 'Help',
'recent': 'Recent',
},
'search-panel': {
'search-files': 'Search Files',
'placeholder-search': 'Search',
'for-history': '(⇅ for history)',
'files-to-include': 'Files to include',
'files-to-exclude': 'Files to exclude',
'match-case': 'Match Case',
'match-whole-word': 'Match Whole Word',
'use-regex': 'Use Regular Expression',
},
}
export type BaseLanguage = typeof data
+11 -1
View File
@@ -291,6 +291,7 @@ const data: BaseLanguage = {
'create-in-cd': '当前目录创建新文件',
'copy-name': '复制名称',
'copy-path': '复制路径',
'find-in-folder': '在文件夹中查找',
},
'toast': {
'moved': '[%s] 已移动到 [%s]',
@@ -408,7 +409,6 @@ const data: BaseLanguage = {
'input-placeholder': '键入字符……',
'empty': '无结果',
'files': '快速跳转',
'search': '搜索内容',
'marked': '已标记',
},
'editor': {
@@ -597,6 +597,16 @@ const data: BaseLanguage = {
'help': '帮助',
'recent': '最近打开',
},
'search-panel': {
'search-files': '搜索文件',
'placeholder-search': '搜索',
'for-history': '(⇅ 切换历史)',
'files-to-include': '包含文件',
'files-to-exclude': '排除文件',
'match-case': '区分大小写',
'match-whole-word': '匹配整词',
'use-regex': '使用正则表达式',
},
}
export default data
+12
View File
@@ -0,0 +1,12 @@
import type { IProgressMessage, ISerializedFileMatch, ISerializedSearchSuccess } from 'ripgrep-wrapper'
export interface SearchMessage<T extends 'result' | 'message' | 'done' | 'error'> {
type: T
payload: T extends 'result'
? ISerializedFileMatch[]
: T extends 'message'
? IProgressMessage
: T extends 'done'
? ISerializedSearchSuccess
: Error
}
+31 -1
View File
@@ -1466,7 +1466,7 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*", "@types/node@^15.12.2":
"@types/node@*":
version "15.12.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26"
integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==
@@ -1481,6 +1481,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.1.tgz#459886b51f52aa923dc06b9ea81cb8b1d733e9d3"
integrity sha512-fTFWOFrgAkj737w1o0HLTIgisgYHnsZfeiqhG1Ltrf/iJjudEbUwetQAsfrtVE49JGwvpEzQR+EbMkIqG4227g==
"@types/node@^18.11.9":
version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
@@ -1764,6 +1769,14 @@
vscode-nls "^5.0.0"
vscode-uri "^2.1.2"
"@vscode/ripgrep@^1.14.2":
version "1.14.2"
resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.2.tgz#47c0eec2b64f53d8f7e1b5ffd22a62e229191c34"
integrity sha512-KDaehS8Jfdg1dqStaIPDKYh66jzKd5jy5aYEPzIv0JYFLADPsCSQPBUdsJVXnr0t72OlDcj96W05xt/rSnNFFQ==
dependencies:
https-proxy-agent "^5.0.0"
proxy-from-env "^1.1.0"
"@vue/babel-helper-vue-transform-on@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc"
@@ -7154,6 +7167,11 @@ proto-list@~1.2.1:
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
psl@^1.1.28, psl@^1.1.33:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@@ -7540,6 +7558,13 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
ripgrep-wrapper@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ripgrep-wrapper/-/ripgrep-wrapper-1.1.1.tgz#26205eefe2833123ce899cf4bcc8a5bdca42e0e3"
integrity sha512-qdXkNQ7URwnef8ymXj4Eh+Strt2qHiF5a9XxBNbiNcQRiA3zGXAQMRvuSgJoh2x/c72ZXA9MfOyNQZNI9dumew==
dependencies:
vscode-regexpp "^3.1.0"
roarr@^2.15.3:
version "2.15.4"
resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd"
@@ -8807,6 +8832,11 @@ vscode-pug-languageservice@0.29.8:
pug-parser "^6.0.0"
vscode-languageserver "^8.0.0-next.2"
vscode-regexpp@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vscode-regexpp/-/vscode-regexpp-3.1.0.tgz#42d059b6fffe99bd42939c0d013f632f0cad823f"
integrity sha512-pqtN65VC1jRLawfluX4Y80MMG0DHJydWhe5ZwMHewZD6sys4LbU6lHwFAHxeuaVE6Y6+xZOtAw+9hvq7/0ejkg==
vscode-textmate@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e"