vzcode 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,13 +3,15 @@ import {
3
3
  useEffect,
4
4
  useContext,
5
5
  useState,
6
+ useCallback,
6
7
  } from 'react';
7
8
  import { Form } from '../bootstrap';
8
9
  import { VZCodeContext } from '../VZCodeContext';
9
- import { SearchFile } from '../../types';
10
+ import { FileId, SearchFile } from '../../types';
10
11
  import { EditorView } from 'codemirror';
11
- import { getExtensionIcon } from './FileListing';
12
12
  import { CloseSVG, DirectoryArrowSVG } from '../Icons';
13
+ import { FileTypeIcon } from './FileTypeIcon';
14
+ import { editorCacheKey } from '../useEditorCache';
13
15
 
14
16
  function jumpToPattern(
15
17
  editor: EditorView,
@@ -32,21 +34,47 @@ function jumpToPattern(
32
34
  });
33
35
  }
34
36
 
37
+ function isResultElementWithinView(container, element) {
38
+ const containerTop = container.scrollTop;
39
+ const containerBottom =
40
+ containerTop + container.clientHeight;
41
+
42
+ const elementTop =
43
+ element.offsetTop - container.offsetTop;
44
+ const elementBottom = elementTop + element.clientHeight;
45
+
46
+ return (
47
+ elementTop >= containerTop + 100 &&
48
+ elementBottom <= containerBottom - 100
49
+ );
50
+ }
51
+
35
52
  export const Search = () => {
36
- const inputRef = useRef(null);
37
53
  const [isMounted, setIsMounted] = useState(false);
38
54
  const [isSearching, setIsSearching] = useState(false);
39
55
  const {
40
56
  search,
41
57
  setSearch,
58
+ activePaneId,
42
59
  setActiveFileId,
43
- openTab,
44
60
  setSearchResults,
45
61
  setSearchFileVisibility,
62
+ setSearchLineVisibility,
63
+ setSearchFocusedIndex,
46
64
  shareDBDoc,
47
65
  editorCache,
48
66
  } = useContext(VZCodeContext);
49
- const { pattern, results, focused } = search;
67
+ const {
68
+ pattern,
69
+ results,
70
+ focusedIndex,
71
+ focusedChildIndex,
72
+ focused,
73
+ } = search;
74
+ const inputRef = useRef(null);
75
+ const files: [string, SearchFile][] = Object.entries(
76
+ results,
77
+ ).filter(([_, file]) => file.visibility !== 'closed');
50
78
 
51
79
  useEffect(() => {
52
80
  if (isMounted) {
@@ -70,7 +98,199 @@ export const Search = () => {
70
98
  }
71
99
  }, [pattern]);
72
100
 
101
+ const flattenResult = useCallback(
102
+ (fileId: string, file: SearchFile) => {
103
+ setSearchFileVisibility(
104
+ shareDBDoc,
105
+ fileId,
106
+ file.visibility === 'open' &&
107
+ focusedChildIndex === null
108
+ ? 'flattened'
109
+ : 'open',
110
+ );
111
+ },
112
+ [focusedIndex, focusedChildIndex],
113
+ );
114
+
115
+ const closeResult = useCallback((fileId: FileId) => {
116
+ setSearchFileVisibility(shareDBDoc, fileId, 'closed');
117
+ }, []);
118
+
119
+ const focusFileElement = useCallback(
120
+ (fileId: FileId, index: number) => {
121
+ setActiveFileId(fileId);
122
+ setSearchFocusedIndex(index, null);
123
+ },
124
+ [],
125
+ );
126
+
127
+ const handleKeyDown = (event) => {
128
+ event.preventDefault();
129
+
130
+ if (files.length === 0) {
131
+ return;
132
+ }
133
+
134
+ const matchingLines: number =
135
+ files[focusedIndex][1].matches.length;
136
+
137
+ switch (event.key) {
138
+ case 'Tab':
139
+ // Focus the file heading
140
+ setSearchFocusedIndex(focusedIndex, null);
141
+ break;
142
+ case 'ArrowUp':
143
+ if (
144
+ focusedIndex == 0 &&
145
+ focusedChildIndex == null
146
+ ) {
147
+ // No effect on first search listing
148
+ break;
149
+ } else if (
150
+ focusedChildIndex === null ||
151
+ matchingLines == 0
152
+ ) {
153
+ // Toggle the previous file last child, if any
154
+ const previousMatchingLines: number =
155
+ files[focusedIndex - 1][1].matches.length;
156
+ setSearchFocusedIndex(
157
+ focusedIndex - 1,
158
+ previousMatchingLines > 0
159
+ ? previousMatchingLines - 1
160
+ : null,
161
+ );
162
+ } else if (focusedChildIndex === 0) {
163
+ // Toggle the file
164
+ setSearchFocusedIndex(focusedIndex, null);
165
+ } else {
166
+ // Toggle the previous matching line
167
+ setSearchFocusedIndex(
168
+ focusedIndex,
169
+ focusedChildIndex - 1,
170
+ );
171
+ }
172
+
173
+ break;
174
+ case 'ArrowDown':
175
+ if (
176
+ focusedIndex == files.length - 1 &&
177
+ focusedChildIndex == matchingLines - 1
178
+ ) {
179
+ // Last matching line should have no effect
180
+ break;
181
+ } else if (
182
+ focusedChildIndex === null &&
183
+ matchingLines > 0
184
+ ) {
185
+ // Toggle the first matching line
186
+ setSearchFocusedIndex(focusedIndex, 0);
187
+ } else if (
188
+ focusedChildIndex == matchingLines - 1 ||
189
+ matchingLines == 0
190
+ ) {
191
+ // Toggle the next file
192
+ setSearchFocusedIndex(focusedIndex + 1, null);
193
+ } else {
194
+ // Toggle the next matching line
195
+ setSearchFocusedIndex(
196
+ focusedIndex,
197
+ focusedChildIndex + 1,
198
+ );
199
+ }
200
+
201
+ break;
202
+ case 'ArrowLeft':
203
+ if (focusedChildIndex !== null) {
204
+ setSearchFocusedIndex(focusedIndex, null);
205
+ } else {
206
+ flattenResult(
207
+ files[focusedIndex][0],
208
+ files[focusedIndex][1],
209
+ );
210
+ }
211
+ break;
212
+ case 'ArrowRight':
213
+ if (files[focusedIndex][1].visibility !== 'open') {
214
+ flattenResult(
215
+ files[focusedIndex][0],
216
+ files[focusedIndex][1],
217
+ );
218
+ } else if (
219
+ focusedChildIndex === null &&
220
+ matchingLines !== 0
221
+ ) {
222
+ setSearchFocusedIndex(focusedIndex, 0);
223
+ }
224
+
225
+ break;
226
+ case 'Enter':
227
+ case ' ':
228
+ const fileId: string = files[focusedIndex][0];
229
+ setActiveFileId(fileId);
230
+
231
+ if (focusedChildIndex !== null) {
232
+ // Jump to matching line
233
+ const line: number =
234
+ files[focusedIndex][1].matches[
235
+ focusedChildIndex
236
+ ].line;
237
+ const index: number =
238
+ files[focusedIndex][1].matches[
239
+ focusedChildIndex
240
+ ].index;
241
+
242
+ const cacheKey = editorCacheKey(
243
+ fileId,
244
+ activePaneId,
245
+ );
246
+ if (editorCache.has(cacheKey)) {
247
+ jumpToPattern(
248
+ editorCache.get(cacheKey).editor,
249
+ pattern,
250
+ line,
251
+ index,
252
+ );
253
+ }
254
+ }
255
+ break;
256
+ default:
257
+ break;
258
+ }
259
+
260
+ // Ensure keyboard navigation keeps results within the current view
261
+ const file: string = files[focusedIndex][0];
262
+ const container = document.getElementById(
263
+ 'sidebar-view-container',
264
+ );
265
+
266
+ if (container) {
267
+ if (focusedChildIndex === null) {
268
+ const fileElement = document.getElementById(file);
269
+
270
+ if (
271
+ !isResultElementWithinView(container, fileElement)
272
+ ) {
273
+ fileElement.scrollIntoView({ block: 'center' });
274
+ }
275
+ } else {
276
+ const line =
277
+ files[focusedIndex][1].matches[focusedChildIndex]
278
+ .line;
279
+ const lineElement = document.getElementById(
280
+ file + '-' + line,
281
+ );
282
+
283
+ if (
284
+ !isResultElementWithinView(container, lineElement)
285
+ ) {
286
+ lineElement.scrollIntoView({ block: 'center' });
287
+ }
288
+ }
289
+ }
290
+ };
291
+
73
292
  useEffect(() => {
293
+ // Focus the search input on mount and keyboard shortcut invocation
74
294
  if (inputRef.current) {
75
295
  inputRef.current.focus();
76
296
  }
@@ -85,122 +305,206 @@ export const Search = () => {
85
305
  <Form.Control
86
306
  type="text"
87
307
  value={pattern}
88
- onChange={(e) => setSearch(e.target.value)}
308
+ onChange={(event) =>
309
+ setSearch(event.target.value)
310
+ }
89
311
  ref={inputRef}
90
312
  spellCheck="false"
91
313
  />
92
314
  </Form.Group>
93
315
  {Object.keys(results).length >= 1 &&
94
316
  pattern.trim().length >= 1 ? (
95
- <div className="search-results">
96
- {Object.entries(results)
97
- .filter(
98
- ([_, file]) => file.visibility !== 'closed',
99
- )
100
- .map(([fileId, file]: [string, SearchFile]) => (
101
- <div
102
- className="search-result"
103
- key={file.name}
104
- >
105
- <div className="search-file-heading">
106
- <div className="search-file-title">
107
- <div
108
- className="arrow-wrapper"
109
- style={{
110
- transform: `rotate(${file.visibility === 'open' ? 90 : 0}deg)`,
111
- }}
112
- onClick={() => {
113
- setSearchFileVisibility(
114
- shareDBDoc,
115
- fileId,
116
- file.visibility === 'open'
117
- ? 'flattened'
118
- : 'open',
119
- );
120
- }}
121
- >
122
- <DirectoryArrowSVG />
317
+ <div
318
+ onKeyDown={handleKeyDown}
319
+ tabIndex={0}
320
+ className="search-results"
321
+ >
322
+ {files.map(
323
+ (
324
+ [fileId, file]: [string, SearchFile],
325
+ index,
326
+ ) => {
327
+ const matches = file.matches;
328
+ return (
329
+ <div
330
+ className="search-result"
331
+ key={file.name}
332
+ >
333
+ <div
334
+ onClick={() =>
335
+ focusFileElement(fileId, index)
336
+ }
337
+ id={fileId}
338
+ className={`search-file-heading
339
+ ${
340
+ focusedIndex == index &&
341
+ focusedChildIndex == null
342
+ ? 'active'
343
+ : ''
344
+ }`}
345
+ >
346
+ <div className="search-file-title">
347
+ <div
348
+ className="arrow-wrapper"
349
+ onClick={() =>
350
+ flattenResult(fileId, file)
351
+ }
352
+ style={{
353
+ transform: `rotate(${file.visibility === 'open' ? 90 : 0}deg)`,
354
+ }}
355
+ >
356
+ <DirectoryArrowSVG />
357
+ </div>
358
+ <div className="search-file-name">
359
+ <FileTypeIcon name={file.name} />
360
+ <h5>{file.name}</h5>
361
+ </div>
123
362
  </div>
124
- <div className="search-file-name">
125
- {getExtensionIcon(file.name)}
126
- <h5>{file.name}</h5>
363
+ <div className="search-file-info">
364
+ {index == focusedIndex &&
365
+ focusedChildIndex == null ? (
366
+ <span
367
+ className="search-file-close"
368
+ onClick={(event) => {
369
+ event.stopPropagation();
370
+ closeResult(fileId);
371
+ // Focus the previous search file, if possible
372
+ setSearchFocusedIndex(
373
+ Math.max(0, index - 1),
374
+ null,
375
+ );
376
+ }}
377
+ >
378
+ <CloseSVG />
379
+ </span>
380
+ ) : (
381
+ <h6 className="search-file-count">
382
+ {matches.length}
383
+ </h6>
384
+ )}
127
385
  </div>
128
386
  </div>
129
- <div className="search-file-info">
130
- <h6 className="search-file-count">
131
- {file.matches.length}
132
- </h6>
133
- <div
134
- className="search-file-close"
135
- onClick={() => {
136
- setSearchFileVisibility(
137
- shareDBDoc,
138
- fileId,
139
- 'closed',
140
- );
141
- }}
142
- >
143
- <CloseSVG />
144
- </div>
387
+ <div className="search-file-lines">
388
+ {(file.visibility === 'open' ||
389
+ (file.visibility === 'flattened' &&
390
+ index == focusedIndex &&
391
+ focusedChildIndex !== null)) &&
392
+ file.matches.map(
393
+ (match, childIndex) => {
394
+ const before =
395
+ match.text.substring(
396
+ 0,
397
+ match.index,
398
+ );
399
+ const hit = match.text.substring(
400
+ match.index,
401
+ match.index + pattern.length,
402
+ );
403
+ const after =
404
+ match.text.substring(
405
+ match.index + pattern.length,
406
+ );
407
+
408
+ const identifier =
409
+ file.name + '-' + match.line;
410
+
411
+ return (
412
+ <div
413
+ key={identifier}
414
+ tabIndex={
415
+ index == focusedIndex
416
+ ? 0
417
+ : -1
418
+ }
419
+ id={fileId + '-' + match.line}
420
+ className={`search-line
421
+ ${
422
+ focusedIndex == index &&
423
+ focusedChildIndex ==
424
+ childIndex
425
+ ? 'active'
426
+ : ''
427
+ }`}
428
+ >
429
+ <p
430
+ key={
431
+ file.name +
432
+ ' - ' +
433
+ match.line +
434
+ ' - ' +
435
+ match.index
436
+ }
437
+ onClick={() => {
438
+ setSearchFocusedIndex(
439
+ index,
440
+ childIndex,
441
+ );
442
+
443
+ const cacheKey =
444
+ editorCacheKey(
445
+ fileId,
446
+ activePaneId,
447
+ );
448
+
449
+ if (
450
+ editorCache.has(
451
+ cacheKey,
452
+ )
453
+ ) {
454
+ jumpToPattern(
455
+ editorCache.get(
456
+ cacheKey,
457
+ ).editor,
458
+ pattern,
459
+ match.line,
460
+ match.index,
461
+ );
462
+ }
463
+ }}
464
+ >
465
+ {before}
466
+ <span className="search-pattern">
467
+ {hit}
468
+ </span>
469
+ {after}
470
+ </p>
471
+ {focusedIndex == index &&
472
+ focusedChildIndex ===
473
+ childIndex && (
474
+ <span
475
+ className="search-file-close"
476
+ onClick={(event) => {
477
+ event.stopPropagation();
478
+ setSearchLineVisibility(
479
+ shareDBDoc,
480
+ fileId,
481
+ match.line,
482
+ );
483
+
484
+ if (childIndex == 0) {
485
+ // Removing remaining single line
486
+ setSearchFocusedIndex(
487
+ index,
488
+ null,
489
+ );
490
+ }
491
+ }}
492
+ >
493
+ <CloseSVG />
494
+ </span>
495
+ )}
496
+ </div>
497
+ );
498
+ },
499
+ )}
145
500
  </div>
146
501
  </div>
147
- <div className="search-file-lines">
148
- {file.visibility != 'flattened' &&
149
- file.matches.map((match) => {
150
- const before = match.text.substring(
151
- 0,
152
- match.index,
153
- );
154
- const hit = match.text.substring(
155
- match.index,
156
- match.index + pattern.length,
157
- );
158
- const after = match.text.substring(
159
- match.index + pattern.length,
160
- );
161
-
162
- return (
163
- <p
164
- className="search-line"
165
- key={
166
- file.name +
167
- ' - ' +
168
- match.line +
169
- ' - ' +
170
- match.index
171
- }
172
- onClick={() => {
173
- setActiveFileId(fileId);
174
- openTab({
175
- fileId: fileId,
176
- isTransient: false,
177
- });
178
-
179
- if (editorCache.get(fileId)) {
180
- jumpToPattern(
181
- editorCache.get(fileId)
182
- .editor,
183
- pattern,
184
- match.line,
185
- match.index,
186
- );
187
- }
188
- }}
189
- >
190
- {before}
191
- <span className="search-pattern">
192
- {hit}
193
- </span>
194
- {after}
195
- </p>
196
- );
197
- })}
198
- </div>
199
- </div>
200
- ))}
502
+ );
503
+ },
504
+ )}
201
505
  </div>
202
506
  ) : (
203
- <div className="search-results">
507
+ <div className="search-state">
204
508
  <h6>
205
509
  {isSearching ? 'Searching...' : 'No Results'}
206
510
  </h6>
@@ -372,7 +372,7 @@ export const VZSidebar = ({
372
372
  </OverlayTrigger>
373
373
  </div>
374
374
 
375
- <div className="files">
375
+ <div className="files" id="sidebar-view-container">
376
376
  {!isSearchOpen ? (
377
377
  <div className="sidebar-files">
378
378
  {isDragOver ? (
@@ -71,22 +71,25 @@
71
71
  }
72
72
 
73
73
  .sidebar-search {
74
- padding: 10px;
75
-
76
74
  * {
77
75
  color: inherit;
78
76
  }
79
77
  }
80
78
 
81
- .search-result-lines {
82
- margin-top: 10px;
83
- margin-left: 20px;
79
+ .sidebar-search .arrow-wrapper {
80
+ cursor: pointer;
81
+ }
82
+
83
+ .sidebar-search-form {
84
+ padding: 10px;
84
85
  }
85
86
 
86
87
  .search-file-heading {
87
88
  display: flex;
88
89
  justify-content: space-between;
89
90
  align-items: center;
91
+ padding: 10px;
92
+ cursor: pointer;
90
93
 
91
94
  * {
92
95
  margin: 0px;
@@ -121,22 +124,32 @@
121
124
  border-radius: 100%;
122
125
  }
123
126
 
124
- .search-results {
125
- padding: 10px;
127
+ .search-state {
128
+ margin: 10px;
126
129
  }
127
130
 
128
- .search-file-lines {
129
- font-size: inherit;
130
- margin: 15px 0px 0px 30px;
131
+ .search-results .active {
132
+ border: 1px solid var(--vh-color-neutral-04);
133
+ background-color: var(--vh-color-neutral-02);
134
+ cursor: pointer;
131
135
  }
132
136
 
133
- .search-line,
134
- .arrow-wrapper,
135
- .search-file-close {
136
- &:hover {
137
- opacity: 0.7;
138
- cursor: pointer;
139
- }
137
+ .search-line {
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ padding-right: 10px;
142
+ cursor: pointer;
143
+ }
144
+
145
+ .search-line p {
146
+ padding-left: 30px;
147
+ margin: 10px 0px;
148
+ }
149
+
150
+ .search-file-lines {
151
+ font-size: inherit;
152
+ margin-top: 10px;
140
153
  }
141
154
 
142
155
  .search-file-lines .search-pattern {
@@ -259,22 +272,24 @@
259
272
  white-space: nowrap;
260
273
  overflow: hidden;
261
274
  gap: 6px;
275
+ position: relative;
262
276
  }
263
277
 
264
278
  .presence-indicators {
279
+ position: absolute;
280
+ top: 0;
265
281
  display: flex;
266
282
  gap: 4px;
267
- margin-right: 6px; /* Space between the indicators and the file name */
268
283
  }
269
284
 
270
285
  .presence-indicator {
271
- width: 16px;
272
- height: 16px;
286
+ width: 10px;
287
+ height: 10px;
273
288
  border-radius: 50%;
274
289
  display: flex;
275
290
  align-items: center;
276
291
  justify-content: center;
277
- font-size: 10px;
292
+ font-size: 8px;
278
293
  background-color: #ff5733;
279
294
  color: #ffffff;
280
295
  }