vzcode 1.13.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>
@@ -189,11 +189,11 @@ export const VZSidebar = ({
189
189
  fileId: update.start[1] as FileId,
190
190
  };
191
191
 
192
- console.log('Got presence!');
193
- // console.log({presenceId,update})
194
- console.log(
195
- JSON.stringify(presenceIndicator, null, 2),
196
- );
192
+ // console.log('Got presence!');
193
+ // // console.log({presenceId,update})
194
+ // console.log(
195
+ // JSON.stringify(presenceIndicator, null, 2),
196
+ // );
197
197
 
198
198
  updatePresenceIndicator(presenceIndicator);
199
199
  };
@@ -205,7 +205,7 @@ export const VZSidebar = ({
205
205
  }
206
206
  }, [docPresence]);
207
207
 
208
- console.log(sidebarPresenceIndicators);
208
+ // console.log(sidebarPresenceIndicators);
209
209
 
210
210
  return (
211
211
  <div
@@ -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 ? (