(null)
const editorHeight = ref('300px')
+const isFindOpen = ref(false)
+const inFileFindQuery = ref('')
+const findMatchCount = ref(0)
+const currentFindMatch = ref(0)
+const findReplaceRef = ref<{ focusFindInput: () => void; openReplace: () => void } | null>(null)
+
+watch(inFileFindQuery, handleFindInput)
+
function updateEditorHeight() {
if (editorContainer.value) {
const top = editorContainer.value.getBoundingClientRect().top
@@ -126,6 +174,7 @@ watch(
() => props.file,
async (newFile) => {
if (newFile) {
+ closeFind()
await loadFileContent(newFile)
nextTick(updateEditorHeight)
} else {
@@ -180,15 +229,7 @@ function resetState() {
imagePreview.value = null
}
-function onEditorInit(editor: {
- commands: {
- addCommand: (cmd: {
- name: string
- bindKey: { win: string; mac: string }
- exec: () => void
- }) => void
- }
-}) {
+function onEditorInit(editor: AceEditorInstance) {
editorInstance.value = editor
editor.commands.addCommand({
@@ -196,6 +237,21 @@ function onEditorInit(editor: {
bindKey: { win: 'Ctrl-S', mac: 'Command-S' },
exec: () => saveFileContent(false),
})
+
+ editor.commands.addCommand({
+ name: 'find',
+ bindKey: { win: 'Ctrl-F', mac: 'Command-F' },
+ exec: () => toggleFind(),
+ })
+
+ editor.commands.addCommand({
+ name: 'replace',
+ bindKey: { win: 'Ctrl-H', mac: 'Command-Option-F' },
+ exec: () => {
+ isFindOpen.value = true
+ nextTick(() => findReplaceRef.value?.openReplace())
+ },
+ })
}
async function saveFileContent(exit: boolean = false) {
@@ -255,6 +311,93 @@ async function shareToMclogs() {
}
}
+function countOccurrences(content: string, query: string): number {
+ if (!query) return 0
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ return (content.match(new RegExp(escaped, 'gi')) ?? []).length
+}
+
+function toggleFind() {
+ if (isFindOpen.value) {
+ closeFind()
+ } else {
+ isFindOpen.value = true
+ nextTick(() => findReplaceRef.value?.focusFindInput())
+ }
+}
+
+function closeFind() {
+ isFindOpen.value = false
+ inFileFindQuery.value = ''
+ findMatchCount.value = 0
+ currentFindMatch.value = 0
+ editorInstance.value?.find('', { wrap: true })
+ editorInstance.value?.focus()
+}
+
+function replaceOne(query: string) {
+ const editor = editorInstance.value
+ if (!editor || findMatchCount.value === 0) return
+ editor.replace(query)
+ nextTick(() => {
+ const count = countOccurrences(fileContent.value, inFileFindQuery.value)
+ findMatchCount.value = count
+ currentFindMatch.value = count > 0 ? Math.min(currentFindMatch.value, count) : 0
+ })
+}
+
+function replaceAllOccurrences(query: string) {
+ const editor = editorInstance.value
+ if (!editor || findMatchCount.value === 0) return
+ editor.replaceAll(query)
+ nextTick(() => {
+ const count = countOccurrences(fileContent.value, inFileFindQuery.value)
+ findMatchCount.value = count
+ currentFindMatch.value = count > 0 ? 1 : 0
+ if (count > 0) {
+ editor.find(inFileFindQuery.value, { wrap: true, caseSensitive: false })
+ }
+ })
+}
+
+function handleFindInput() {
+ const editor = editorInstance.value
+ if (!editor) return
+
+ const query = inFileFindQuery.value
+ if (!query) {
+ findMatchCount.value = 0
+ currentFindMatch.value = 0
+ editor.find('', { wrap: true })
+ return
+ }
+
+ const count = countOccurrences(fileContent.value, query)
+ findMatchCount.value = count
+
+ if (count > 0) {
+ editor.find(query, { wrap: true, caseSensitive: false })
+ currentFindMatch.value = 1
+ } else {
+ currentFindMatch.value = 0
+ }
+}
+
+function findNext() {
+ const editor = editorInstance.value
+ if (!editor || findMatchCount.value === 0) return
+ editor.findNext()
+ currentFindMatch.value = (currentFindMatch.value % findMatchCount.value) + 1
+}
+
+function findPrevious() {
+ const editor = editorInstance.value
+ if (!editor || findMatchCount.value === 0) return
+ editor.findPrevious()
+ currentFindMatch.value =
+ ((currentFindMatch.value - 2 + findMatchCount.value) % findMatchCount.value) + 1
+}
+
function close() {
resetState()
emit('close')
@@ -271,8 +414,10 @@ defineExpose({
shareToMclogs,
close,
isEditingImage,
+ isFindOpen,
fileContent,
hasUnsavedChanges,
revertChanges,
+ toggleFind,
})
diff --git a/packages/ui/src/layouts/shared/files-tab/layout.vue b/packages/ui/src/layouts/shared/files-tab/layout.vue
index 7a5f97feed..4d94f550a1 100644
--- a/packages/ui/src/layouts/shared/files-tab/layout.vue
+++ b/packages/ui/src/layouts/shared/files-tab/layout.vue
@@ -54,6 +54,7 @@
:editing-file-name="ctx.editingFile.value?.name"
:editing-file-path="ctx.editingFile.value?.path"
:is-editing-image="fileEditorRef?.isEditingImage"
+ :is-editor-find-open="fileEditorRef?.isFindOpen"
:search-query="searchQuery"
:show-refresh-button="showRefreshButton"
:show-install-from-url="ctx.showInstallFromUrl"
@@ -70,6 +71,7 @@
@unzip-from-url="showUnzipFromUrlModal"
@refresh="ctx.refresh"
@share="() => fileEditorRef?.shareToMclogs()"
+ @find="() => fileEditorRef?.toggleFind()"
/>
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index d94e977aef..9ff0bdfd66 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -548,12 +548,42 @@
"files.editor.file-saved-title": {
"defaultMessage": "File saved"
},
+ "files.editor.find-close": {
+ "defaultMessage": "Close"
+ },
+ "files.editor.find-in-file": {
+ "defaultMessage": "Find"
+ },
+ "files.editor.find-match-count": {
+ "defaultMessage": "{current} of {total}"
+ },
+ "files.editor.find-next-match": {
+ "defaultMessage": "Next match"
+ },
+ "files.editor.find-no-results": {
+ "defaultMessage": "No results"
+ },
+ "files.editor.find-previous-match": {
+ "defaultMessage": "Previous match"
+ },
+ "files.editor.find-toggle-replace": {
+ "defaultMessage": "Toggle replace"
+ },
"files.editor.log-url-copied-text": {
"defaultMessage": "Your log file URL has been copied to your clipboard."
},
"files.editor.log-url-copied-title": {
"defaultMessage": "Log URL copied"
},
+ "files.editor.replace": {
+ "defaultMessage": "Replace"
+ },
+ "files.editor.replace-all": {
+ "defaultMessage": "Replace All"
+ },
+ "files.editor.replace-in-file": {
+ "defaultMessage": "Replace"
+ },
"files.editor.save-failed-text": {
"defaultMessage": "Could not save the file."
},
@@ -644,6 +674,9 @@
"files.navbar.file-navigation": {
"defaultMessage": "File navigation"
},
+ "files.navbar.find-in-file": {
+ "defaultMessage": "Find in file"
+ },
"files.navbar.home": {
"defaultMessage": "Home"
},