vzcode 1.3.0 → 1.5.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.
- package/dist/assets/{index-BnoPNwcv.js → index-D6aZiqme.js} +50 -50
- package/dist/assets/{worker-CuCF2V0j.js → worker-DqNV0IYq.js} +83 -83
- package/dist/index.html +1 -1
- package/package.json +5 -6
- package/src/client/CodeEditor/getOrCreateEditor.ts +1 -1
- package/src/client/CodeEditor/index.tsx +9 -2
- package/src/client/CodeEditor/json1PresenceDisplay.ts +44 -0
- package/src/client/CodeEditor/rainbowBrackets.ts +110 -0
- package/src/client/VZCodeContext.tsx +6 -0
- package/src/client/themes/themes.ts +19 -1
- package/src/client/themes/vizhubTheme/index.ts +0 -27
- package/src/client/useKeyboardShortcuts.ts +168 -47
package/dist/index.html
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-D6aZiqme.js"></script>
|
|
24
24
|
<link rel="stylesheet" crossorigin href="/assets/index-ClPo_hzs.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"@codemirror/lang-markdown": "^6.2.5",
|
|
108
108
|
"@codemirror/state": "^6.4.1",
|
|
109
109
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
110
|
-
"@codemirror/view": "^6.28.
|
|
110
|
+
"@codemirror/view": "^6.28.1",
|
|
111
111
|
"@lezer/highlight": "^1.2.0",
|
|
112
112
|
"@replit/codemirror-indentation-markers": "^6.5.2",
|
|
113
113
|
"@replit/codemirror-interact": "^6.3.1",
|
|
@@ -136,9 +136,8 @@
|
|
|
136
136
|
"json0-ot-diff": "^1.1.2",
|
|
137
137
|
"ngrok": "^4.3.3",
|
|
138
138
|
"open": "^10.1.0",
|
|
139
|
-
"openai": "^4.
|
|
139
|
+
"openai": "^4.51.0",
|
|
140
140
|
"prettier-plugin-svelte": "^3.2.4",
|
|
141
|
-
"rainbowbrackets": "^2.0.2",
|
|
142
141
|
"react": "^18.3.1",
|
|
143
142
|
"react-bootstrap": "^2.10.2",
|
|
144
143
|
"react-dom": "^18.3.1",
|
|
@@ -154,9 +153,9 @@
|
|
|
154
153
|
"@vitejs/plugin-react": "^4.3.1",
|
|
155
154
|
"concurrently": "^8.2.2",
|
|
156
155
|
"prettier": "^3.3.2",
|
|
157
|
-
"sass": "^1.77.
|
|
156
|
+
"sass": "^1.77.5",
|
|
158
157
|
"typescript": "^5.4.5",
|
|
159
|
-
"vite": "^5.
|
|
158
|
+
"vite": "^5.3.1",
|
|
160
159
|
"vitest": "^1.6.0"
|
|
161
160
|
},
|
|
162
161
|
"optionalDependencies": {
|
|
@@ -39,7 +39,7 @@ import { typeScriptLinter } from './typeScriptLinter';
|
|
|
39
39
|
import { keymap } from '@codemirror/view';
|
|
40
40
|
import { basicSetup } from './basicSetup';
|
|
41
41
|
import { InteractRule } from '@replit/codemirror-interact';
|
|
42
|
-
import rainbowBrackets from '
|
|
42
|
+
import rainbowBrackets from '../CodeEditor/rainbowBrackets';
|
|
43
43
|
|
|
44
44
|
// Feature flag to enable TypeScript completions & TypeScript Linter.
|
|
45
45
|
const enableTypeScriptCompletions = true;
|
|
@@ -35,9 +35,10 @@ export const CodeEditor = ({
|
|
|
35
35
|
editorNoLongerWantsFocus,
|
|
36
36
|
typeScriptWorker,
|
|
37
37
|
theme,
|
|
38
|
+
codeEditorRef,
|
|
38
39
|
} = useContext(VZCodeContext);
|
|
39
40
|
|
|
40
|
-
const ref =
|
|
41
|
+
const ref = codeEditorRef;
|
|
41
42
|
|
|
42
43
|
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
43
44
|
// via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
|
|
@@ -138,5 +139,11 @@ export const CodeEditor = ({
|
|
|
138
139
|
editorNoLongerWantsFocus,
|
|
139
140
|
]);
|
|
140
141
|
|
|
141
|
-
return
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
className="vz-code-editor"
|
|
145
|
+
ref={ref}
|
|
146
|
+
tabIndex={-1}
|
|
147
|
+
></div>
|
|
148
|
+
);
|
|
142
149
|
};
|
|
@@ -8,6 +8,12 @@ import { Annotation, RangeSet } from '@codemirror/state';
|
|
|
8
8
|
import ColorHash from 'color-hash';
|
|
9
9
|
import { Username } from '../../types';
|
|
10
10
|
|
|
11
|
+
const debug = false;
|
|
12
|
+
|
|
13
|
+
// TODO: Make this a setting in the UI.
|
|
14
|
+
// See https://github.com/vizhub-core/vzcode/issues/739
|
|
15
|
+
const enableAutoFollow = false;
|
|
16
|
+
|
|
11
17
|
// Deals with receiving the broadcasted presence cursor locations
|
|
12
18
|
// from other clients and displaying them.
|
|
13
19
|
//
|
|
@@ -27,6 +33,9 @@ export const json1PresenceDisplay = ({
|
|
|
27
33
|
// See https://codemirror.net/6/docs/ref/#view.Decoration
|
|
28
34
|
decorations: RangeSet<Decoration>;
|
|
29
35
|
|
|
36
|
+
//Added variable for cursor position
|
|
37
|
+
cursorPosition = {};
|
|
38
|
+
|
|
30
39
|
constructor(view: EditorView) {
|
|
31
40
|
// Initialize decorations to empty array so CodeMirror doesn't crash.
|
|
32
41
|
this.decorations = RangeSet.of([]);
|
|
@@ -36,8 +45,19 @@ export const json1PresenceDisplay = ({
|
|
|
36
45
|
// * Values are presence objects as defined by ot-json1-presence
|
|
37
46
|
const presenceState = {};
|
|
38
47
|
|
|
48
|
+
// Add the scroll event listener
|
|
49
|
+
//This runs for the arrow key scrolling, it should result in the users scrolling to eachother's location.
|
|
50
|
+
view.dom.addEventListener('scroll', () => {
|
|
51
|
+
this.scrollToCursor(view);
|
|
52
|
+
});
|
|
39
53
|
// Receive remote presence changes.
|
|
40
54
|
docPresence.on('receive', (id, presence) => {
|
|
55
|
+
if (debug) {
|
|
56
|
+
console.log(
|
|
57
|
+
`Received presence for id ${id}`,
|
|
58
|
+
presence,
|
|
59
|
+
); // Debug statement
|
|
60
|
+
}
|
|
41
61
|
// If presence === null, the user has disconnected / exited
|
|
42
62
|
// We also check if the presence is for the current file or not.
|
|
43
63
|
if (presence && pathMatches(path, presence)) {
|
|
@@ -97,6 +117,13 @@ export const json1PresenceDisplay = ({
|
|
|
97
117
|
}),
|
|
98
118
|
});
|
|
99
119
|
}
|
|
120
|
+
if (view.state.doc.length >= from) {
|
|
121
|
+
// Ensure position is valid
|
|
122
|
+
this.cursorPosition[id] = from; // Store the cursor position, important to run if we cant get the regular scroll to work
|
|
123
|
+
// console.log(`Stored cursor position for id ${id}: ${from}`); // Debug statement
|
|
124
|
+
} else {
|
|
125
|
+
// console.warn(`Invalid cursor position for id ${id}: ${from}`); // Debug statement
|
|
126
|
+
}
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
this.decorations = Decoration.set(
|
|
@@ -116,8 +143,25 @@ export const json1PresenceDisplay = ({
|
|
|
116
143
|
annotations: [presenceAnnotation.of(true)],
|
|
117
144
|
});
|
|
118
145
|
}, 0);
|
|
146
|
+
|
|
147
|
+
// Auto-follow all users when their presence is broadcast
|
|
148
|
+
// by scrolling them into view.
|
|
149
|
+
if (enableAutoFollow) {
|
|
150
|
+
this.scrollToCursor(view);
|
|
151
|
+
}
|
|
119
152
|
});
|
|
120
153
|
}
|
|
154
|
+
// Method to scroll the view to keep the cursor in view
|
|
155
|
+
scrollToCursor(view) {
|
|
156
|
+
for (const id in this.cursorPosition) {
|
|
157
|
+
//getting the cursor position of the other cursor
|
|
158
|
+
const cursorPos = this.cursorPosition[id];
|
|
159
|
+
view.dispatch({
|
|
160
|
+
//if the other person's cursor has jumped off screen, we will follow it by scrolling there directly.
|
|
161
|
+
effects: EditorView.scrollIntoView(cursorPos),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
121
165
|
},
|
|
122
166
|
{
|
|
123
167
|
decorations: (v) => v.decorations,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditorView,
|
|
3
|
+
Decoration,
|
|
4
|
+
ViewPlugin,
|
|
5
|
+
} from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
function generateColors() {
|
|
8
|
+
return ['red', 'orange', 'yellow', 'green'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const rainbowBracketsPlugin = ViewPlugin.fromClass(
|
|
12
|
+
class {
|
|
13
|
+
decorations;
|
|
14
|
+
|
|
15
|
+
constructor(view) {
|
|
16
|
+
this.decorations = this.getBracketDecorations(view);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
update(update) {
|
|
20
|
+
if (
|
|
21
|
+
update.docChanged ||
|
|
22
|
+
update.selectionSet ||
|
|
23
|
+
update.viewportChanged
|
|
24
|
+
) {
|
|
25
|
+
this.decorations = this.getBracketDecorations(
|
|
26
|
+
update.view,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getBracketDecorations(view) {
|
|
32
|
+
const { doc } = view.state;
|
|
33
|
+
const decorations = [];
|
|
34
|
+
const stack = [];
|
|
35
|
+
const colors = generateColors();
|
|
36
|
+
|
|
37
|
+
for (let pos = 0; pos < doc.length; pos += 1) {
|
|
38
|
+
const char = doc.sliceString(pos, pos + 1);
|
|
39
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
40
|
+
stack.push({ type: char, from: pos });
|
|
41
|
+
} else if (
|
|
42
|
+
char === ')' ||
|
|
43
|
+
char === ']' ||
|
|
44
|
+
char === '}'
|
|
45
|
+
) {
|
|
46
|
+
const open = stack.pop();
|
|
47
|
+
if (
|
|
48
|
+
open &&
|
|
49
|
+
open.type === this.getMatchingBracket(char)
|
|
50
|
+
) {
|
|
51
|
+
const color =
|
|
52
|
+
colors[stack.length % colors.length];
|
|
53
|
+
decorations.push(
|
|
54
|
+
Decoration.mark({
|
|
55
|
+
class: `rainbow-bracket-${color}`,
|
|
56
|
+
}).range(open.from, open.from + 1),
|
|
57
|
+
Decoration.mark({
|
|
58
|
+
class: `rainbow-bracket-${color}`,
|
|
59
|
+
}).range(pos, pos + 1),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
decorations.sort(
|
|
66
|
+
(a, b) =>
|
|
67
|
+
a.from - b.from || a.startSide - b.startSide,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return Decoration.set(decorations);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line class-methods-use-this
|
|
74
|
+
getMatchingBracket(closingBracket) {
|
|
75
|
+
switch (closingBracket) {
|
|
76
|
+
case ')':
|
|
77
|
+
return '(';
|
|
78
|
+
case ']':
|
|
79
|
+
return '[';
|
|
80
|
+
case '}':
|
|
81
|
+
return '{';
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
decorations: (v) => v.decorations,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export default function rainbowBrackets() {
|
|
93
|
+
return [
|
|
94
|
+
rainbowBracketsPlugin,
|
|
95
|
+
EditorView.baseTheme({
|
|
96
|
+
'.rainbow-bracket-red': { color: 'white' },
|
|
97
|
+
'.rainbow-bracket-red > span': { color: 'white' },
|
|
98
|
+
'.rainbow-bracket-orange': { color: '#FFFFB2' },
|
|
99
|
+
'.rainbow-bracket-orange > span': {
|
|
100
|
+
color: '#FFFFB2',
|
|
101
|
+
},
|
|
102
|
+
'.rainbow-bracket-yellow': { color: '#FFB2DA' },
|
|
103
|
+
'.rainbow-bracket-yellow > span': {
|
|
104
|
+
color: '#FFB2DA',
|
|
105
|
+
},
|
|
106
|
+
'.rainbow-bracket-green': { color: '#B2FFB2' },
|
|
107
|
+
'.rainbow-bracket-green > span': { color: '#B2FFB2' },
|
|
108
|
+
}),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
@@ -129,6 +129,8 @@ export type VZCodeContextValue = {
|
|
|
129
129
|
|
|
130
130
|
sidebarRef: React.RefObject<HTMLDivElement>;
|
|
131
131
|
|
|
132
|
+
codeEditorRef: React.RefObject<HTMLDivElement>;
|
|
133
|
+
|
|
132
134
|
connected: boolean;
|
|
133
135
|
|
|
134
136
|
hoveredItemId: ItemId | null;
|
|
@@ -179,6 +181,8 @@ export const VZCodeProvider = ({
|
|
|
179
181
|
|
|
180
182
|
const sidebarRef = useRef(null);
|
|
181
183
|
|
|
184
|
+
const codeEditorRef = useRef(null);
|
|
185
|
+
|
|
182
186
|
// The error message shows either:
|
|
183
187
|
// * `prettierError` - errors from Prettier, client-side only
|
|
184
188
|
// * `codeError` - errors from an external source, such as
|
|
@@ -323,6 +327,7 @@ export const VZCodeProvider = ({
|
|
|
323
327
|
runCodeRef,
|
|
324
328
|
sidebarRef,
|
|
325
329
|
editorCache,
|
|
330
|
+
codeEditorRef,
|
|
326
331
|
});
|
|
327
332
|
|
|
328
333
|
// Track the currently hovered file id.
|
|
@@ -390,6 +395,7 @@ export const VZCodeProvider = ({
|
|
|
390
395
|
runPrettierRef,
|
|
391
396
|
runCodeRef,
|
|
392
397
|
sidebarRef,
|
|
398
|
+
codeEditorRef,
|
|
393
399
|
|
|
394
400
|
connected,
|
|
395
401
|
|
|
@@ -26,7 +26,25 @@ export type ThemeOption = {
|
|
|
26
26
|
value: any;
|
|
27
27
|
label: ThemeLabel;
|
|
28
28
|
};
|
|
29
|
-
|
|
29
|
+
// add light theme brackets to eclipse theme
|
|
30
|
+
// const modifiedEclipse = {
|
|
31
|
+
// ...eclipse,
|
|
32
|
+
// '.rainbow-bracket-red > span': {
|
|
33
|
+
// color: 'white',
|
|
34
|
+
// },
|
|
35
|
+
// '.rainbow-bracket-orange > span': {
|
|
36
|
+
// //yellow
|
|
37
|
+
// color: '#FFFFB2',
|
|
38
|
+
// },
|
|
39
|
+
// '.rainbow-bracket-yellow > span': {
|
|
40
|
+
// // orange
|
|
41
|
+
// color: '#FFB2DA',
|
|
42
|
+
// },
|
|
43
|
+
// '.rainbow-bracket-green > span': {
|
|
44
|
+
// // green
|
|
45
|
+
// color: '#B2FFB2',
|
|
46
|
+
// }
|
|
47
|
+
// };
|
|
30
48
|
export const themes: Array<ThemeOption> = [
|
|
31
49
|
{ value: abcdef, label: 'abcdef' },
|
|
32
50
|
{ value: dracula, label: 'dracula' },
|
|
@@ -190,33 +190,6 @@ function vizhubThemeInit(
|
|
|
190
190
|
backgroundColor: 'transparent',
|
|
191
191
|
outline: '1px solid #ff2222',
|
|
192
192
|
},
|
|
193
|
-
'.rainbow-bracket-red > span': {
|
|
194
|
-
color: 'white',
|
|
195
|
-
},
|
|
196
|
-
'.rainbow-bracket-orange > span': {
|
|
197
|
-
// red
|
|
198
|
-
color: '#FFB2B2',
|
|
199
|
-
},
|
|
200
|
-
'.rainbow-bracket-yellow > span': {
|
|
201
|
-
// orange
|
|
202
|
-
color: '#FFB2DA',
|
|
203
|
-
},
|
|
204
|
-
'.rainbow-bracket-green > span': {
|
|
205
|
-
// yellow
|
|
206
|
-
color: '#FFFFB2',
|
|
207
|
-
},
|
|
208
|
-
'.rainbow-bracket-blue > span': {
|
|
209
|
-
// green
|
|
210
|
-
color: '#B2FFB2',
|
|
211
|
-
},
|
|
212
|
-
'.rainbow-bracket-indigo > span': {
|
|
213
|
-
// blue
|
|
214
|
-
color: '#B2FFFF',
|
|
215
|
-
},
|
|
216
|
-
'.rainbow-bracket-violet > span': {
|
|
217
|
-
// purple
|
|
218
|
-
color: '#DAB2FF',
|
|
219
|
-
}
|
|
220
193
|
},
|
|
221
194
|
{ dark: true },
|
|
222
195
|
),
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { shouldTriggerRun } from './shouldTriggerRun';
|
|
3
|
-
import { EditorView } from 'codemirror';
|
|
4
3
|
import { syntaxTree } from '@codemirror/language';
|
|
5
|
-
import { SyntaxNode } from '@lezer/common';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
SelectionRange,
|
|
9
|
-
} from '@codemirror/state';
|
|
4
|
+
import { SyntaxNode, SyntaxNodeRef } from '@lezer/common';
|
|
5
|
+
import { EditorView } from '@codemirror/view';
|
|
6
|
+
import { EditorState } from '@codemirror/state';
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
/*
|
|
9
|
+
The following is a helpful resource for the following Code Mirror Syntax Tree methods below
|
|
10
|
+
https://lezer.codemirror.net/docs/ref/#common
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Store the element in the current editor DOM to highlight, indicating a potential jump to definition is possible
|
|
14
|
+
let activeJumpingElement: HTMLSpanElement = null;
|
|
15
|
+
|
|
16
|
+
// Store the syntax node representing the destination within the syntax tree
|
|
17
|
+
let definingNode: SyntaxNode = null;
|
|
18
|
+
|
|
19
|
+
// Example nesting types for the specific language to find level in the syntax tree
|
|
12
20
|
const nestingTypes = new Set<string>([
|
|
13
21
|
'FunctionDeclaration',
|
|
14
22
|
'ClassDeclaration',
|
|
@@ -22,9 +30,10 @@ const nestingTypes = new Set<string>([
|
|
|
22
30
|
'CatchClause',
|
|
23
31
|
'WithStatement',
|
|
24
32
|
'ArrowFunction',
|
|
33
|
+
'ImportGroup',
|
|
25
34
|
]);
|
|
26
35
|
|
|
27
|
-
//
|
|
36
|
+
// Example declaration types for the specific language to find definitions in the syntax tree
|
|
28
37
|
const declarationTypes = new Set<string>([
|
|
29
38
|
'VariableDeclaration',
|
|
30
39
|
'FunctionDeclaration',
|
|
@@ -46,10 +55,10 @@ const declarationTypes = new Set<string>([
|
|
|
46
55
|
function getIdentifierContext(
|
|
47
56
|
identifier: SyntaxNode,
|
|
48
57
|
): number {
|
|
49
|
-
let levels = 0;
|
|
50
58
|
let current: SyntaxNode = identifier;
|
|
59
|
+
let levels: number = 0;
|
|
51
60
|
|
|
52
|
-
// Traverse up the tree to find the total depth
|
|
61
|
+
// Traverse up the tree to find the total depth, only counting valid nesting types
|
|
53
62
|
while (current && current.type) {
|
|
54
63
|
const parentType: string = current.type.name;
|
|
55
64
|
|
|
@@ -63,19 +72,18 @@ function getIdentifierContext(
|
|
|
63
72
|
return levels;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
function jumpToDefinition(
|
|
67
|
-
|
|
75
|
+
function jumpToDefinition(
|
|
76
|
+
editor: EditorView,
|
|
77
|
+
node: SyntaxNode,
|
|
78
|
+
): SyntaxNode {
|
|
68
79
|
const state: EditorState = editor.state;
|
|
69
|
-
const location: SelectionRange = state.selection.main;
|
|
70
80
|
const definitions: Array<{
|
|
71
81
|
identifier: SyntaxNode;
|
|
72
82
|
context: number;
|
|
73
83
|
}> = [];
|
|
74
84
|
|
|
75
|
-
//
|
|
76
|
-
const identifier: SyntaxNode =
|
|
77
|
-
state,
|
|
78
|
-
).resolveInner(location.from, -1);
|
|
85
|
+
// From an identifier in the syntax tree, fetch the name and context to find closest defining syntax node
|
|
86
|
+
const identifier: SyntaxNode = node;
|
|
79
87
|
const identifierName: string = state.doc.sliceString(
|
|
80
88
|
identifier.from,
|
|
81
89
|
identifier.to,
|
|
@@ -84,13 +92,15 @@ function jumpToDefinition(editor: EditorView) {
|
|
|
84
92
|
|
|
85
93
|
if (identifier) {
|
|
86
94
|
syntaxTree(state).iterate({
|
|
87
|
-
enter(tree) {
|
|
95
|
+
enter(tree: SyntaxNodeRef) {
|
|
88
96
|
// Traverse syntax tree to find positions of respective identifier definitions within context
|
|
89
97
|
if (
|
|
90
98
|
declarationTypes.has(tree.name) ||
|
|
91
99
|
nestingTypes.has(tree.name)
|
|
92
100
|
) {
|
|
93
101
|
const parent: SyntaxNode = tree.node;
|
|
102
|
+
|
|
103
|
+
// Fetch a host of potential identifiers in an attempt to find the defining syntax node
|
|
94
104
|
const children: Array<SyntaxNode> = [
|
|
95
105
|
...parent.getChildren('VariableDefinition'),
|
|
96
106
|
...parent.getChildren('PropertyDefinition'),
|
|
@@ -104,9 +114,10 @@ function jumpToDefinition(editor: EditorView) {
|
|
|
104
114
|
child.to,
|
|
105
115
|
);
|
|
106
116
|
|
|
117
|
+
// Ensure no jump to self by comparing syntax node id's
|
|
107
118
|
if (
|
|
108
119
|
name === identifierName &&
|
|
109
|
-
child !== identifier
|
|
120
|
+
child.type.id !== identifier.type.id
|
|
110
121
|
) {
|
|
111
122
|
definitions.push({
|
|
112
123
|
identifier: child,
|
|
@@ -132,21 +143,10 @@ function jumpToDefinition(editor: EditorView) {
|
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
|
|
136
|
-
editor.dispatch({
|
|
137
|
-
selection: {
|
|
138
|
-
anchor: closestDefinition.from,
|
|
139
|
-
head: closestDefinition.to,
|
|
140
|
-
},
|
|
141
|
-
scrollIntoView: true,
|
|
142
|
-
effects: EditorView.scrollIntoView(
|
|
143
|
-
closestDefinition.from,
|
|
144
|
-
{
|
|
145
|
-
y: 'center',
|
|
146
|
-
},
|
|
147
|
-
),
|
|
148
|
-
});
|
|
146
|
+
return closestDefinition;
|
|
149
147
|
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -158,6 +158,7 @@ function jumpToDefinition(editor: EditorView) {
|
|
|
158
158
|
// * Alt-PageUp: Change the active tab to the previous one
|
|
159
159
|
// * Alt-PageDown: Change the active tab to the next one
|
|
160
160
|
// * Ctrl-s or Shift-Enter: Run the code and format it with Prettier
|
|
161
|
+
// * Ctrl-Click: Jump to closest definition for a potential identifier
|
|
161
162
|
export const useKeyboardShortcuts = ({
|
|
162
163
|
closeTabs,
|
|
163
164
|
activeFileId,
|
|
@@ -168,6 +169,7 @@ export const useKeyboardShortcuts = ({
|
|
|
168
169
|
runCodeRef,
|
|
169
170
|
sidebarRef,
|
|
170
171
|
editorCache,
|
|
172
|
+
codeEditorRef,
|
|
171
173
|
}) => {
|
|
172
174
|
useEffect(() => {
|
|
173
175
|
const handleKeyPress = (event: KeyboardEvent) => {
|
|
@@ -189,18 +191,11 @@ export const useKeyboardShortcuts = ({
|
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
if (event.ctrlKey) {
|
|
193
|
-
|
|
194
|
-
editorCache.get(activeFileId).editor;
|
|
195
|
-
const jumpToDefinitionHandler = () => {
|
|
196
|
-
jumpToDefinition(editor);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// CTRL + Click: Jump to relative definition
|
|
194
|
+
if (event.ctrlKey === true) {
|
|
195
|
+
// On holding CTRL key, search for a potential definition jump using mouse location
|
|
200
196
|
document.addEventListener(
|
|
201
|
-
'
|
|
202
|
-
|
|
203
|
-
{ once: true },
|
|
197
|
+
'mouseover',
|
|
198
|
+
handleMouseOver,
|
|
204
199
|
);
|
|
205
200
|
}
|
|
206
201
|
|
|
@@ -231,23 +226,149 @@ export const useKeyboardShortcuts = ({
|
|
|
231
226
|
setActiveFileRight();
|
|
232
227
|
return;
|
|
233
228
|
}
|
|
234
|
-
}
|
|
235
229
|
|
|
236
|
-
|
|
237
|
-
if (event.key === '0') {
|
|
230
|
+
if (event.key === '1') {
|
|
238
231
|
if (sidebarRef.current) {
|
|
239
232
|
sidebarRef.current.focus();
|
|
240
233
|
}
|
|
241
234
|
}
|
|
235
|
+
|
|
236
|
+
if (event.key === '2') {
|
|
237
|
+
if (codeEditorRef.current) {
|
|
238
|
+
codeEditorRef.current.focus();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const resetActiveJumpingElement = (): void => {
|
|
245
|
+
if (activeJumpingElement) {
|
|
246
|
+
activeJumpingElement.style.cursor = 'initial';
|
|
247
|
+
activeJumpingElement.style.textDecoration = 'none';
|
|
248
|
+
activeJumpingElement = definingNode = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
document.removeEventListener(
|
|
252
|
+
'mouseover',
|
|
253
|
+
handleMouseOver,
|
|
254
|
+
);
|
|
255
|
+
document.removeEventListener(
|
|
256
|
+
'mousedown',
|
|
257
|
+
jumpToDefinitionHandler,
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const jumpToDefinitionHandler = (
|
|
262
|
+
event: MouseEvent,
|
|
263
|
+
): void => {
|
|
264
|
+
// Ensure the current destination node is defined and current cursor position matches highlighted element
|
|
265
|
+
if (
|
|
266
|
+
!definingNode ||
|
|
267
|
+
(event.target as HTMLSpanElement) !==
|
|
268
|
+
activeJumpingElement
|
|
269
|
+
) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Move current cursor and center view in the editor to destination node
|
|
274
|
+
const editor: EditorView =
|
|
275
|
+
editorCache.get(activeFileId).editor;
|
|
276
|
+
const closestDefinition: SyntaxNode = definingNode;
|
|
277
|
+
|
|
278
|
+
editor.dispatch({
|
|
279
|
+
selection: {
|
|
280
|
+
anchor: closestDefinition.from,
|
|
281
|
+
head: closestDefinition.to,
|
|
282
|
+
},
|
|
283
|
+
scrollIntoView: true,
|
|
284
|
+
effects: EditorView.scrollIntoView(
|
|
285
|
+
closestDefinition.from,
|
|
286
|
+
{
|
|
287
|
+
y: 'center',
|
|
288
|
+
},
|
|
289
|
+
),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
resetActiveJumpingElement();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleKeyRelease = (event: KeyboardEvent) => {
|
|
296
|
+
// On releasing CTRL key, reset all active definition jumping elements and listeners
|
|
297
|
+
if (!event.ctrlKey) {
|
|
298
|
+
resetActiveJumpingElement();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleMouseOver = (event: MouseEvent) => {
|
|
303
|
+
const editor: EditorView =
|
|
304
|
+
editorCache.get(activeFileId).editor;
|
|
305
|
+
const tree = syntaxTree(editor.state);
|
|
306
|
+
const element = event.target as HTMLSpanElement;
|
|
307
|
+
|
|
308
|
+
// Ensure the identifier element can be found and is within the current editor DOM
|
|
309
|
+
if (
|
|
310
|
+
element == null ||
|
|
311
|
+
!editor.dom.contains(element)
|
|
312
|
+
) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const position: number = editor.posAtDOM(element);
|
|
317
|
+
const identifier: SyntaxNode = tree.resolveInner(
|
|
318
|
+
position,
|
|
319
|
+
1,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// All valid identifiers must be span elements to find a potential defining jump in editor
|
|
323
|
+
if (
|
|
324
|
+
identifier &&
|
|
325
|
+
element instanceof HTMLSpanElement
|
|
326
|
+
) {
|
|
327
|
+
const potentialJump: SyntaxNode = jumpToDefinition(
|
|
328
|
+
editor,
|
|
329
|
+
identifier,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Only allowing to jump to other definition nodes
|
|
333
|
+
if (
|
|
334
|
+
potentialJump &&
|
|
335
|
+
identifier.type.id !== potentialJump.type.id
|
|
336
|
+
) {
|
|
337
|
+
if (activeJumpingElement) {
|
|
338
|
+
activeJumpingElement.style.cursor = 'initial';
|
|
339
|
+
activeJumpingElement.style.textDecoration =
|
|
340
|
+
'none';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
activeJumpingElement = element;
|
|
344
|
+
definingNode = potentialJump;
|
|
345
|
+
|
|
346
|
+
activeJumpingElement.style.cursor = 'pointer';
|
|
347
|
+
activeJumpingElement.style.textDecoration =
|
|
348
|
+
'underline';
|
|
349
|
+
|
|
350
|
+
// CTRL + Click: Jump to relative definition, which is removed on CTRL key release
|
|
351
|
+
document.addEventListener(
|
|
352
|
+
'mousedown',
|
|
353
|
+
jumpToDefinitionHandler,
|
|
354
|
+
{ once: true },
|
|
355
|
+
);
|
|
356
|
+
}
|
|
242
357
|
}
|
|
243
358
|
};
|
|
244
359
|
|
|
245
360
|
document.addEventListener('keydown', handleKeyPress);
|
|
361
|
+
document.addEventListener('keyup', handleKeyRelease);
|
|
362
|
+
|
|
246
363
|
return () => {
|
|
247
364
|
document.removeEventListener(
|
|
248
365
|
'keydown',
|
|
249
366
|
handleKeyPress,
|
|
250
367
|
);
|
|
368
|
+
document.removeEventListener(
|
|
369
|
+
'keyup',
|
|
370
|
+
handleKeyRelease,
|
|
371
|
+
);
|
|
251
372
|
};
|
|
252
373
|
}, [
|
|
253
374
|
handleOpenCreateFileModal,
|