| | |
| | |
| | |
| | |
| | <script lang="ts"> |
| | import type { EditorView } from '@codemirror/view'; |
| | |
| | import { onMount, createEventDispatcher } from 'svelte'; |
| | import { |
| | SearchQuery, |
| | findPrevious, |
| | findNext, |
| | setSearchQuery, |
| | replaceNext, |
| | replaceAll |
| | } from '@codemirror/search'; |
| | |
| | import IconCaretV2 from '../Icons/IconCaretV2.svelte'; |
| | import IconArrowLeft from '../Icons/IconArrowLeft.svelte'; |
| | import IconCross from '../Icons/IconCross.svelte'; |
| | import IconReplace from '../Icons/IconReplace.svelte'; |
| | import IconReplaceAll from '../Icons/IconReplaceAll.svelte'; |
| | |
| | export let view: EditorView; |
| | |
| | let el: HTMLDivElement; |
| | let searchTxtEl: HTMLInputElement; |
| | let searchTxt = ''; |
| | let replaceTxt = ''; |
| | let isCaseSensitive = false; |
| | let isRegexp = false; |
| | let isWholeWord = false; |
| | let isReplacePanelOpen = false; |
| | |
| | const dispatch = createEventDispatcher<{ close: void }>(); |
| | |
| | $: query = new SearchQuery({ |
| | search: searchTxt, |
| | caseSensitive: isCaseSensitive, |
| | wholeWord: isWholeWord, |
| | regexp: isRegexp, |
| | replace: replaceTxt |
| | }); |
| | |
| | $: query, search(); |
| | |
| | |
| | |
| | function destroyDefaultPanel() { |
| | |
| | |
| | const el = document.querySelector('.codemirror-wrapper .cm-search'); |
| | el?.parentElement?.removeChild(el); |
| | } |
| | |
| | function getSelectedText(editorView: EditorView) { |
| | const state = editorView.state; |
| | const selection = state.selection; |
| | const selectedText = selection.ranges |
| | .map((range) => state.doc.sliceString(range.from, range.to)) |
| | .join('\n'); |
| | return selectedText; |
| | } |
| | |
| | function search() { |
| | destroyDefaultPanel(); |
| | view?.dispatch({ effects: setSearchQuery.of(query) }); |
| | if (searchTxt && view) { |
| | findPrevious(view); |
| | findNext(view); |
| | } else { |
| | reset(); |
| | } |
| | } |
| | |
| | function reset() { |
| | |
| | view?.dispatch({ |
| | effects: setSearchQuery.of( |
| | new SearchQuery({ |
| | search: '' |
| | }) |
| | ) |
| | }); |
| | } |
| | |
| | function onKeyDownWindow(e: KeyboardEvent) { |
| | const { ctrlKey, metaKey, key, shiftKey } = e; |
| | const cmdKey = metaKey || ctrlKey; |
| | const isOpenShortcut = key === 'f3' || (cmdKey && key === 'f'); |
| | const isNextOrPrevShortcut = cmdKey && key === 'g'; |
| | const isCloseShortcut = key === 'Escape' || key === 'Esc'; |
| | if (isOpenShortcut) { |
| | e.preventDefault(); |
| | searchTxt = getSelectedText(view); |
| | searchTxtEl.focus(); |
| | } else if (isNextOrPrevShortcut) { |
| | e.preventDefault(); |
| | shiftKey ? findPrevious(view) : findNext(view); |
| | } else if (isCloseShortcut) { |
| | dispatch('close'); |
| | } |
| | } |
| | |
| | function onKeyDownEl(e: KeyboardEvent) { |
| | const { key } = e; |
| | const isNextShortcut = key === 'Enter'; |
| | if (isNextShortcut) { |
| | e.preventDefault(); |
| | findNext(view); |
| | } |
| | } |
| | |
| | onMount(() => { |
| | searchTxt = getSelectedText(view); |
| | searchTxtEl.focus(); |
| | |
| | |
| | if (el) { |
| | const rect = el.getBoundingClientRect(); |
| | el.style.position = 'fixed'; |
| | el.style.top = rect.top + 'px'; |
| | el.style.left = rect.left + 'px'; |
| | el.style.right = 'auto'; |
| | el.classList.remove('absolute'); |
| | el.classList.add('fixed'); |
| | } |
| | |
| | return reset; |
| | }); |
| | </script> |
| |
|
| | <svelte:window on:keydown={onKeyDownWindow} /> |
| |
|
| | <div |
| | bind:this={el} |
| | class="absolute top-0 right-0 z-20 rounded-sm border border-gray-500 bg-white dark:bg-gray-900 dark:text-white" |
| | on:keydown={onKeyDownEl} |
| | > |
| | <div class="flex border-b border-gray-500"> |
| | <button |
| | type="button" |
| | class="border-r border-gray-500 px-0.5" |
| | on:click={() => (isReplacePanelOpen = !isReplacePanelOpen)} |
| | > |
| | <IconCaretV2 classNames="h-full {isReplacePanelOpen ? '' : '-rotate-90'}" /> |
| | </button> |
| | <div class="my-1"> |
| | <div class="flex items-center"> |
| | <div class="flex w-[250px] items-center bg-gray-100 dark:bg-gray-800"> |
| | <input |
| | type="text" |
| | class="w-full border-0 bg-transparent py-1 pr-[4.3rem] pl-2 text-sm" |
| | bind:value={searchTxt} |
| | bind:this={searchTxtEl} |
| | /> |
| | |
| | <div |
| | class="-ml-[4.3rem] flex w-16 items-center justify-between gap-0.5 font-mono select-none" |
| | > |
| | <button |
| | type="button" |
| | title="Match Case" |
| | class="rounded-sm px-0.5 text-sm {isCaseSensitive ? 'bg-black text-white' : ''}" |
| | on:click={() => (isCaseSensitive = !isCaseSensitive)} |
| | > |
| | Aa |
| | </button> |
| | <button |
| | type="button" |
| | title="Match Whole Word" |
| | class="rounded-sm px-0.5 text-sm underline {isWholeWord ? 'bg-black text-white' : ''}" |
| | on:click={() => (isWholeWord = !isWholeWord)} |
| | > |
| | ab |
| | </button> |
| | <button |
| | type="button" |
| | title="Use Regular Expression" |
| | class="rounded-sm px-0.5 text-sm {isRegexp ? 'bg-black text-white' : ''}" |
| | on:click={() => (isRegexp = !isRegexp)} |
| | > |
| | re |
| | </button> |
| | </div> |
| | </div> |
| | |
| | <div class="mx-2 flex items-center gap-0.5 select-none"> |
| | <button type="button" title="Next Match" on:click={() => findNext(view)}> |
| | <IconArrowLeft classNames="-rotate-90" /> |
| | </button> |
| | <button type="button" title="Previous Match" on:click={() => findPrevious(view)}> |
| | <IconArrowLeft classNames="rotate-90" /> |
| | </button> |
| | <button type="button" title="Close" on:click={() => dispatch('close')}> |
| | <IconCross /> |
| | </button> |
| | </div> |
| | </div> |
| | {#if isReplacePanelOpen} |
| | <div class="mt-1 flex items-center"> |
| | <input |
| | type="text" |
| | bind:value={replaceTxt} |
| | class="w-[250px] border-0 bg-gray-100 py-1 pl-2 text-sm dark:bg-gray-800" |
| | /> |
| | <div class="ml-1 flex items-center gap-0.5 select-none"> |
| | <button type="button" title="Replace" on:click={() => replaceNext(view)}> |
| | <IconReplace classNames="text-base" /> |
| | </button> |
| | <button type="button" title="Replace All" on:click={() => replaceAll(view)}> |
| | <IconReplaceAll classNames="text-base" /> |
| | </button> |
| | </div> |
| | </div> |
| | {/if} |
| | </div> |
| | </div> |
| | </div> |
| |
|