Skip to content

Commit 108ee49

Browse files
authored
playground: print text for node (#353)
1 parent 80d056b commit 108ee49

File tree

3 files changed

+122
-9
lines changed

3 files changed

+122
-9
lines changed

website/theme/components/Playground/Editor.tsx

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,55 @@ export const Editor = ({
3434
const divEl = useRef<HTMLDivElement>(null);
3535
const editorRef =
3636
useRef<import('monaco-editor').editor.IStandaloneCodeEditor>(null);
37+
const urlUpdateTimer = useRef<number | null>(null);
38+
39+
function decodeParam(value: string | null): string | null {
40+
if (!value) return null;
41+
try {
42+
return decodeURIComponent(value);
43+
} catch {
44+
return value;
45+
}
46+
}
47+
48+
function getInitialCode(): string {
49+
if (typeof window === 'undefined') {
50+
return ['let a: any;', 'a.b = 10;'].join('\n');
51+
}
52+
const { search, hash } = window.location;
53+
const searchParams = new URLSearchParams(search);
54+
// URLSearchParams.get already decodes percent-encoding
55+
const fromSearch = searchParams.get('code');
56+
if (fromSearch != null) return fromSearch;
57+
// Also support hash like #code=...
58+
if (hash && hash.startsWith('#')) {
59+
const hashParams = new URLSearchParams(hash.slice(1));
60+
const fromHash = hashParams.get('code');
61+
if (fromHash != null) return fromHash;
62+
}
63+
return ['let a: any;', 'a.b = 10;'].join('\n');
64+
}
65+
66+
function scheduleSerializeToUrl(value: string) {
67+
if (typeof window === 'undefined') return;
68+
if (urlUpdateTimer.current) {
69+
window.clearTimeout(urlUpdateTimer.current);
70+
urlUpdateTimer.current = null;
71+
}
72+
urlUpdateTimer.current = window.setTimeout(() => {
73+
try {
74+
const url = new URL(window.location.href);
75+
url.searchParams.set('code', value);
76+
// Remove any code=... from hash if present to avoid ambiguity
77+
if (url.hash && url.hash.includes('code=')) {
78+
url.hash = '';
79+
}
80+
window.history.replaceState(null, '', url.toString());
81+
} catch {
82+
// ignore URL update errors
83+
}
84+
}, 300);
85+
}
3786
// get value from editor using forwardRef
3887
React.useImperativeHandle(ref, () => ({
3988
getValue: () => editorRef.current?.getValue(),
@@ -68,15 +117,24 @@ export const Editor = ({
68117
}
69118

70119
const editor = monaco.editor.create(divEl.current, {
71-
value: ['let a: any;', 'a.b = 10;'].join('\n'),
120+
value: getInitialCode(),
72121
language: 'typescript',
73122
automaticLayout: true,
74123
scrollBeyondLastLine: false,
75124
});
125+
// Ensure ref is set before first onChange so parent can read value
126+
editorRef.current = editor;
127+
// Trigger initial onChange + URL sync with initial value
128+
{
129+
const initialVal = editor.getValue() || '';
130+
onChange(initialVal);
131+
scheduleSerializeToUrl(initialVal);
132+
}
76133
editor.onDidChangeModelContent(() => {
77-
onChange(editor.getValue() || '');
134+
const val = editor.getValue() || '';
135+
onChange(val);
136+
scheduleSerializeToUrl(val);
78137
});
79-
editorRef.current = editor;
80138

81139
return () => {
82140
editor.dispose();

website/theme/components/Playground/ResultPanel.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import './ResultPanel.css';
33

44
export interface Diagnostic {
@@ -25,7 +25,61 @@ type TabType = 'lint' | 'fixed' | 'ast' | 'type';
2525
export const ResultPanel: React.FC<ResultPanelProps> = props => {
2626
const { diagnostics, ast, error, initialized, fixedCode, typeInfo, loading } =
2727
props;
28-
const [activeTab, setActiveTab] = useState<TabType>('lint');
28+
const [activeTab, setActiveTab] = useState<TabType>(() => {
29+
if (typeof window === 'undefined') return 'lint';
30+
const params = new URLSearchParams(window.location.search);
31+
let tab = params.get('tab');
32+
if (!tab && window.location.hash) {
33+
const hashParams = new URLSearchParams(window.location.hash.slice(1));
34+
tab = hashParams.get('tab');
35+
}
36+
if (tab === 'lint' || tab === 'ast' || tab === 'fixed' || tab === 'type') {
37+
return tab as TabType;
38+
}
39+
return 'lint';
40+
});
41+
42+
// Keep the URL in sync with the selected tab
43+
useEffect(() => {
44+
if (typeof window === 'undefined') return;
45+
try {
46+
const url = new URL(window.location.href);
47+
url.searchParams.set('tab', activeTab);
48+
if (url.hash && new URLSearchParams(url.hash.slice(1)).has('tab')) {
49+
url.hash = '';
50+
}
51+
window.history.replaceState(null, '', url.toString());
52+
} catch {
53+
// ignore URL update errors
54+
}
55+
}, [activeTab]);
56+
57+
// Respond to browser navigation updating the tab
58+
useEffect(() => {
59+
if (typeof window === 'undefined') return;
60+
const handler = () => {
61+
try {
62+
const params = new URLSearchParams(window.location.search);
63+
let tab = params.get('tab');
64+
if (!tab && window.location.hash) {
65+
const hashParams = new URLSearchParams(window.location.hash.slice(1));
66+
tab = hashParams.get('tab');
67+
}
68+
if (
69+
tab === 'lint' ||
70+
tab === 'ast' ||
71+
tab === 'fixed' ||
72+
tab === 'type'
73+
) {
74+
setActiveTab(tab as TabType);
75+
}
76+
} catch {
77+
// ignore
78+
}
79+
};
80+
window.addEventListener('popstate', handler);
81+
return () => window.removeEventListener('popstate', handler);
82+
}, []);
2983

3084
return (
3185
<div className="result-panel">

website/theme/components/Playground/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { use, useEffect, useRef, useState } from 'react';
1+
import React, { useRef, useState } from 'react';
22
import * as Rslint from '@rslint/wasm';
33
import { Editor, EditorRef } from './Editor';
44
import { ResultPanel, Diagnostic } from './ResultPanel';
@@ -67,6 +67,7 @@ const Playground: React.FC = () => {
6767
end: number;
6868
name?: string;
6969
children?: ASTNode[];
70+
text?: string;
7071
}
7172

7273
// Generate AST
@@ -75,11 +76,13 @@ const Playground: React.FC = () => {
7576
const buffer = Uint8Array.from(atob(astBuffer), c => c.charCodeAt(0));
7677
const source = new RemoteSourceFile(buffer, new TextDecoder());
7778
// Convert a RemoteNode (from tsgo/rslint-api) to a minimal ESTree node
79+
7880
function RemoteNodeToEstree(node: Node): ASTNode {
7981
return {
8082
type: SyntaxKind[node.kind],
8183
start: node.pos,
8284
end: node.end,
85+
text: node.text,
8386
children: node.forEachChild((child: Node) => {
8487
return RemoteNodeToEstree(child);
8588
}),
@@ -100,9 +103,7 @@ const Playground: React.FC = () => {
100103
}
101104
}
102105

103-
useEffect(() => {
104-
runLint();
105-
}, []);
106+
// Initial lint is triggered by Editor's initial onChange
106107

107108
return (
108109
<div className="playground-container">

0 commit comments

Comments
 (0)