24
24
</div >
25
25
26
26
<div :class =" $style.playgroundEditorRoot" >
27
- <div :class =" $style.playgroundEditorScroller" :inert =" editorLoading" >
28
- <div :class =" [$style.highlight, $style.playgroundEditorHighlight]" v-html =" editorHtml" ></div >
29
- <textarea
30
- ref =" inputEl"
31
- v-model =" code"
32
- @input =" onInput"
33
- @keydown =" onKeydown"
34
- autocomplete =" off"
35
- wrap =" off"
36
- spellcheck =" false"
37
- :class =" $style.playgroundEditorTextarea"
38
- ></textarea >
27
+ <div :class =" $style.playgroundEditorScRoot" >
28
+ <div :class =" $style.playgroundEditorScroller" :inert =" editorLoading" >
29
+ <div :class =" [$style.highlight, $style.playgroundEditorHighlight]" v-html =" editorHtml" ></div >
30
+ <textarea
31
+ ref =" inputEl"
32
+ v-model =" code"
33
+ @input =" onInput"
34
+ @keydown =" onKeydown"
35
+ @selectionchange.passive =" setEditorCursorPosition"
36
+ autocomplete =" off"
37
+ wrap =" off"
38
+ spellcheck =" false"
39
+ :class =" $style.playgroundEditorTextarea"
40
+ ></textarea >
41
+ </div >
42
+ </div >
43
+ <div :class =" $style.playgroundInfoFooter" >
44
+ <div >Row: {{ currentCursor.row + 1 }}, Column: {{ currentCursor.column + 1 }}</div >
39
45
</div >
40
46
<div v-if =" editorLoading" :class =" $style.playgroundEditorLoading" >
41
47
<div >Loading...</div >
92
98
import { inBrowser } from ' vitepress' ;
93
99
import { ref , computed , useTemplateRef , nextTick , onMounted , watch , onUnmounted } from ' vue' ;
94
100
import { createHighlighterCore } from ' shiki/core' ;
95
- import type { HighlighterCore , LanguageRegistration } from ' shiki/core' ;
101
+ import type { HighlighterCore , LanguageRegistration , ShikiTransformerContext } from ' shiki/core' ;
96
102
import { createJavaScriptRegexEngine } from ' shiki/engine/javascript' ;
97
103
import lzString from ' lz-string' ;
98
104
import { useThrottle } from ' ../scripts/throttle' ;
@@ -116,6 +122,13 @@ const editorLoading = ref(true);
116
122
const inputEl = useTemplateRef (' inputEl' );
117
123
const code = ref (fizzbuzz );
118
124
const editorHtml = ref (' ' );
125
+ const currentCursor = ref <{
126
+ row: number ;
127
+ column: number ;
128
+ }>({
129
+ row: 0 ,
130
+ column: 0 ,
131
+ });
119
132
120
133
let highlighter: HighlighterCore | null = null ;
121
134
@@ -135,6 +148,20 @@ async function init() {
135
148
});
136
149
}
137
150
151
+ function setEditorCursorPosition() {
152
+ if (inputEl .value == null ) return ;
153
+
154
+ const pos = inputEl .value .selectionDirection === ' backward'
155
+ ? inputEl .value .selectionStart
156
+ : inputEl .value .selectionEnd ;
157
+ const lines = code .value .slice (0 , pos ).split (' \n ' );
158
+
159
+ currentCursor .value = {
160
+ row: lines .length - 1 ,
161
+ column: lines [lines .length - 1 ].length ,
162
+ };
163
+ }
164
+
138
165
function onInput(ev : Event ) {
139
166
code .value = (ev .target as HTMLTextAreaElement )?.value ?? code .value ;
140
167
}
@@ -189,6 +216,7 @@ const logs = ref<{
189
216
const logEl = useTemplateRef (' logEl' );
190
217
191
218
const isSyntaxError = ref (false );
219
+ const errorLine = ref <number | null >(null );
192
220
193
221
const ast = ref <unknown >(null );
194
222
const astHtml = ref (' ' );
@@ -198,6 +226,7 @@ const metadataHtml = ref('');
198
226
199
227
function parse() {
200
228
isSyntaxError .value = false ;
229
+ errorLine .value = null ;
201
230
202
231
if (runner .value == null ) {
203
232
ast .value = null ;
@@ -214,6 +243,13 @@ function parse() {
214
243
type: ' error' ,
215
244
}];
216
245
isSyntaxError .value = true ;
246
+
247
+ const lineRes = / \( Line\s * (:\s * )? (\d + )/ .exec (err .message );
248
+ if (lineRes != null ) {
249
+ errorLine .value = parseInt (lineRes [2 ], 10 ); // Convert to zero-based index
250
+ } else {
251
+ errorLine .value = null ;
252
+ }
217
253
}
218
254
ast .value = null ;
219
255
metadata .value = null ;
@@ -356,6 +392,13 @@ onMounted(async () => {
356
392
dark: ' github-dark' ,
357
393
},
358
394
defaultColor: false ,
395
+ transformers: [{
396
+ line(node , lineIndex ) {
397
+ if (isSyntaxError .value && errorLine .value === lineIndex ) {
398
+ this .addClassToHast (node , ' error' );
399
+ }
400
+ },
401
+ }],
359
402
});
360
403
}
361
404
}, { immediate: true });
@@ -451,7 +494,24 @@ onUnmounted(() => {
451
494
452
495
.playgroundEditorRoot {
453
496
position : relative ;
497
+ min-height : 0 ;
498
+ min-width : 0 ;
499
+ display : flex ;
500
+ flex-direction : column ;
501
+ }
502
+
503
+ .playgroundEditorScRoot {
454
504
overflow : scroll ;
505
+ flex-grow : 1 ;
506
+ }
507
+
508
+ .playgroundInfoFooter {
509
+ border-top : 1px solid var (--vp-c-divider );
510
+ flex-shrink : 0 ;
511
+ padding : 0 24px ;
512
+ font-size : 12px ;
513
+ line-height : 24px ;
514
+ color : var (--vp-c-text-2 );
455
515
}
456
516
457
517
.playgroundEditorLoading {
@@ -518,6 +578,11 @@ onUnmounted(() => {
518
578
.highlight span :global(.line ) {
519
579
display : inline-block ;
520
580
min-height : 1em ;
581
+ min-width : 100% ;
582
+ }
583
+
584
+ .highlight span :global(.line.error ) {
585
+ background-color : var (--vp-c-danger-soft );
521
586
}
522
587
523
588
:global(html .dark ) .highlight span {
0 commit comments