forked from quoid/userscripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEditorSearch.svelte
More file actions
199 lines (182 loc) · 6.29 KB
/
Copy pathEditorSearch.svelte
File metadata and controls
199 lines (182 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
<script>
import {tick} from "svelte";
import IconButton from "../../../shared/Components/IconButton.svelte";
import iconArrowDown from "../../../shared/img/icon-arrow-down.svg?raw";
import iconArrowUp from "../../../shared/img/icon-arrow-up.svg?raw";
import iconClose from "../../../shared/img/icon-close.svg?raw";
// determines whether or not search bar is shown
export let active;
// the codemirror instance
export let instance;
// passed from codemirror, called when close button on search bar clicked
export let closeHandler = () => {};
// bound to input element for focusing
let inp;
// bound to input value for feeding query and updating count
let inputValue;
// store the search overlay for later removal
let searchOverlay = {};
// array of selection ranges for query matches
let ranges = [];
// the currently selected range
let rangesIndex = 0;
// next/prev marks text, store those marks here for later removal
const marks = [];
// the search query
$: query = inputValue ? inputValue.trim() : "";
// focus input on show
$: if (active) focusInput();
// runs when search bar is hidden
$: if (!active) {
inputValue = undefined;
instance.removeOverlay(searchOverlay);
instance.focus();
rangesIndex = 0;
ranges = [];
marks.forEach(marker => marker.clear());
}
export async function focusInput() {
await tick();
inp.focus();
// if text entered, highlight it on focus
if (inp.value) inp.setSelectionRange(0, inp.value.length);
}
function keys(e) {
if (e.key === "Escape") closeHandler();
if (e.key === "Enter") next();
}
function highlightMatches() {
const cm = instance;
// set up regex pattern, replace certain characters
const pattern = new RegExp(
// query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"),\
query.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"),
"gi"
);
// if overlay already exists, remove if from codemirror
if (searchOverlay) cm.removeOverlay(searchOverlay);
if (query.length > 0) {
// create the overlay
searchOverlay = {
token(stream) {
pattern.lastIndex = stream.pos;
const match = pattern.exec(stream.string);
if (match && match.index === stream.pos) {
stream.pos += match[0].length || 1;
return "searching";
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
}
};
cm.addOverlay(searchOverlay);
}
}
export function getMatches() {
const cursor = instance.getSearchCursor(query, {line: 0, ch: 0}, {caseFold: true});
// reset ranges count on update
ranges = [];
rangesIndex = 0;
marks.forEach(marker => marker.clear());
while (cursor.findNext()) {
// push range to array
ranges.push({anchor: cursor.from(), head: cursor.to()});
}
}
function next() {
const cm = instance;
let i = rangesIndex;
if (ranges.length) {
// at the end of the results, reset to top
if (i === ranges.length) {
i = 0;
rangesIndex = 0;
}
cm.setSelection(ranges[i].anchor, ranges[i].head);
cm.scrollIntoView({from: ranges[i].anchor, to: ranges[i].head}, 20);
// mark currently selected element
marks.forEach(marker => marker.clear());
const m = cm.markText(ranges[i].anchor, ranges[i].head, {className: "cm-search-mark"});
marks.push(m);
// increment display index after getting data from array
// display index is always +1 compared to index within array
rangesIndex++;
}
}
function previous() {
// end of results is the first in the array
const cm = instance;
if (ranges.length) {
if (rangesIndex === 0 || rangesIndex === 1) {
rangesIndex = ranges.length;
} else {
--rangesIndex;
}
const i = rangesIndex - 1;
cm.setSelection(ranges[i].anchor, ranges[i].head);
cm.scrollIntoView({from: ranges[i].anchor, to: ranges[i].head}, 20);
// mark currently selected element
marks.forEach(marker => marker.clear());
const m = cm.markText(ranges[i].anchor, ranges[i].head, {className: "cm-search-mark"});
marks.push(m);
}
}
</script>
{#if active}
<div class="editor__search">
<input
type="text"
bind:this={inp}
bind:value={inputValue}
on:input={highlightMatches}
on:input={getMatches}
on:keydown={keys}
>
<span>{rangesIndex}/{query ? ranges.length : "?"}</span>
<IconButton icon={iconArrowDown} on:click={() => next()}/>
<IconButton icon={iconArrowUp} on:click={() => previous()}/>
<IconButton icon={iconClose} on:click={closeHandler}/>
</div>
{/if}
<style>
.editor__search {
align-items: center;
border: 1px solid black;
background-color: var(--color-black);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
display: flex;
padding: 0.25rem 0;
position: absolute;
right: 0.5rem;
top: 0;
z-index: 4;
}
input {
border: none;
color: var(--text-color-secondary);
flex-grow: 1;
font: var(--text-small);
font-family: var(--editor-font);
padding: 0.25rem;
}
span {
color: var(--text-color-disabled);
flex-shrink: 0;
font: var(--text-small);
font-family: var(--editor-font);
margin: 0 0.5rem;
min-width: 2.25em;
}
:global(.editor__search button) {
flex-shrink: 0;
}
:global(button:nth-of-type(3)) {
margin-right: 0.25rem;
}
:global(div.editor__search button svg) {
width: 45%;
}
</style>