2424 </div >
2525
2626 <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 >
3945 </div >
4046 <div v-if =" editorLoading" :class =" $style.playgroundEditorLoading" >
4147 <div >Loading...</div >
9298import { inBrowser } from ' vitepress' ;
9399import { ref , computed , useTemplateRef , nextTick , onMounted , watch , onUnmounted } from ' vue' ;
94100import { createHighlighterCore } from ' shiki/core' ;
95- import type { HighlighterCore , LanguageRegistration } from ' shiki/core' ;
101+ import type { HighlighterCore , LanguageRegistration , ShikiTransformerContext } from ' shiki/core' ;
96102import { createJavaScriptRegexEngine } from ' shiki/engine/javascript' ;
97103import lzString from ' lz-string' ;
98104import { useThrottle } from ' ../scripts/throttle' ;
@@ -116,6 +122,13 @@ const editorLoading = ref(true);
116122const inputEl = useTemplateRef (' inputEl' );
117123const code = ref (fizzbuzz );
118124const editorHtml = ref (' ' );
125+ const currentCursor = ref <{
126+ row: number ;
127+ column: number ;
128+ }>({
129+ row: 0 ,
130+ column: 0 ,
131+ });
119132
120133let highlighter: HighlighterCore | null = null ;
121134
@@ -135,6 +148,20 @@ async function init() {
135148 });
136149}
137150
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+
138165function onInput(ev : Event ) {
139166 code .value = (ev .target as HTMLTextAreaElement )?.value ?? code .value ;
140167}
@@ -189,6 +216,7 @@ const logs = ref<{
189216const logEl = useTemplateRef (' logEl' );
190217
191218const isSyntaxError = ref (false );
219+ const errorLine = ref <number | null >(null );
192220
193221const ast = ref <unknown >(null );
194222const astHtml = ref (' ' );
@@ -198,6 +226,7 @@ const metadataHtml = ref('');
198226
199227function parse() {
200228 isSyntaxError .value = false ;
229+ errorLine .value = null ;
201230
202231 if (runner .value == null ) {
203232 ast .value = null ;
@@ -214,6 +243,13 @@ function parse() {
214243 type: ' error' ,
215244 }];
216245 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+ }
217253 }
218254 ast .value = null ;
219255 metadata .value = null ;
@@ -356,6 +392,13 @@ onMounted(async () => {
356392 dark: ' github-dark' ,
357393 },
358394 defaultColor: false ,
395+ transformers: [{
396+ line(node , lineIndex ) {
397+ if (isSyntaxError .value && errorLine .value === lineIndex ) {
398+ this .addClassToHast (node , ' error' );
399+ }
400+ },
401+ }],
359402 });
360403 }
361404 }, { immediate: true });
@@ -451,7 +494,24 @@ onUnmounted(() => {
451494
452495.playgroundEditorRoot {
453496 position : relative ;
497+ min-height : 0 ;
498+ min-width : 0 ;
499+ display : flex ;
500+ flex-direction : column ;
501+ }
502+
503+ .playgroundEditorScRoot {
454504 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 );
455515}
456516
457517.playgroundEditorLoading {
@@ -518,6 +578,11 @@ onUnmounted(() => {
518578.highlight span :global(.line ) {
519579 display : inline-block ;
520580 min-height : 1em ;
581+ min-width : 100% ;
582+ }
583+
584+ .highlight span :global(.line.error ) {
585+ background-color : var (--vp-c-danger-soft );
521586}
522587
523588:global(html .dark ) .highlight span {
0 commit comments