Skip to content

Commit eef29a3

Browse files
committed
feat: add Google Analytics and multi-language support (Java, Python, Go, JavaScript)
- Add Google Analytics tracking (G-EBZYX1YBDY) - Add multi-language code editor with language switcher - Support Java, Python, Go, JavaScript with syntax highlighting - Each language has proper line mapping for debug stepping - Variable values display inline with language-appropriate comments
1 parent cb80ceb commit eef29a3

File tree

10 files changed

+695
-53
lines changed

10 files changed

+695
-53
lines changed

index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>46. 全排列 - Permutations</title>
8+
<!-- Google tag (gtag.js) -->
9+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-EBZYX1YBDY"></script>
10+
<script>
11+
window.dataLayer = window.dataLayer || [];
12+
function gtag(){dataLayer.push(arguments);}
13+
gtag('js', new Date());
14+
gtag('config', 'G-EBZYX1YBDY');
15+
</script>
816
</head>
917
<body>
1018
<div id="root"></div>

src/components/CodeEditor.css

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010
.editor-header {
1111
display: flex;
1212
align-items: center;
13-
gap: 8px;
13+
justify-content: space-between;
1414
padding: 8px 12px;
1515
background: #2d2d2d;
1616
border-bottom: 1px solid #3c3c3c;
1717
}
1818

19+
.file-info {
20+
display: flex;
21+
align-items: center;
22+
gap: 8px;
23+
}
24+
1925
.file-icon {
2026
font-size: 14px;
2127
}
@@ -25,6 +31,44 @@
2531
font-size: 12px;
2632
}
2733

34+
.language-selector {
35+
display: flex;
36+
gap: 4px;
37+
}
38+
39+
.lang-btn {
40+
display: flex;
41+
align-items: center;
42+
gap: 4px;
43+
padding: 4px 8px;
44+
background: #3c3c3c;
45+
border: 1px solid #4c4c4c;
46+
border-radius: 4px;
47+
color: #aaa;
48+
font-size: 11px;
49+
cursor: pointer;
50+
transition: all 0.15s ease;
51+
}
52+
53+
.lang-btn:hover {
54+
background: #4c4c4c;
55+
color: #fff;
56+
}
57+
58+
.lang-btn.active {
59+
background: #0e639c;
60+
border-color: #1177bb;
61+
color: #fff;
62+
}
63+
64+
.lang-icon {
65+
font-size: 12px;
66+
}
67+
68+
.lang-name {
69+
font-size: 11px;
70+
}
71+
2872
.editor-content {
2973
padding: 8px 0;
3074
overflow-x: hidden;

src/components/CodeEditor.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { describe, it, expect } from 'vitest';
22
import * as fc from 'fast-check';
33
import { getHighlightedLineNumbers, getCodeLineCount } from './CodeEditor';
44
import { getJavaPermutationCode } from '../engine/javaTokenizer';
5-
import { JAVA_LINE_MAPPING, StepType } from '../types';
5+
import { StepType } from '../types';
6+
import { JAVA_LINE_MAPPING } from '../engine/codeTemplates';
7+
import { ProgrammingLanguage } from '../types/languages';
68

79
describe('CodeEditor', () => {
810
/**
@@ -107,4 +109,34 @@ describe('CodeEditor', () => {
107109
});
108110
});
109111
});
112+
113+
/**
114+
* Property: Multi-language Support
115+
* Each supported language should have valid code and line mappings.
116+
*/
117+
describe('Multi-language Support', () => {
118+
const languages: ProgrammingLanguage[] = ['java', 'python', 'golang', 'javascript'];
119+
const stepTypes: StepType[] = ['select', 'backtrack', 'complete'];
120+
121+
it('should have code for all supported languages', () => {
122+
languages.forEach(lang => {
123+
const lineCount = getCodeLineCount(lang);
124+
expect(lineCount).toBeGreaterThan(0);
125+
});
126+
});
127+
128+
it('should have valid line mappings for all languages', () => {
129+
languages.forEach(lang => {
130+
const totalLines = getCodeLineCount(lang);
131+
stepTypes.forEach(stepType => {
132+
const highlightedLines = getHighlightedLineNumbers(stepType, lang);
133+
expect(highlightedLines.length).toBeGreaterThan(0);
134+
highlightedLines.forEach(lineNum => {
135+
expect(lineNum).toBeGreaterThanOrEqual(1);
136+
expect(lineNum).toBeLessThanOrEqual(totalLines);
137+
});
138+
});
139+
});
140+
});
141+
});
110142
});

src/components/CodeEditor.tsx

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { useMemo } from 'react';
2-
import { StepType, JAVA_LINE_MAPPING } from '../types';
3-
import { getJavaPermutationCode, getCurrentExecutionLine } from '../engine/javaTokenizer';
2+
import { StepType } from '../types';
3+
import { ProgrammingLanguage, LANGUAGE_CONFIGS, getLanguageConfig } from '../types/languages';
4+
import {
5+
getCodeLines,
6+
getHighlightedLines,
7+
getCurrentExecutionLine,
8+
getLineVariableMapping
9+
} from '../engine/multiLangTokenizer';
410
import './CodeEditor.css';
511

612
interface CodeEditorProps {
@@ -9,46 +15,36 @@ interface CodeEditorProps {
915
available?: number[];
1016
inputNumbers?: number[];
1117
resultCount?: number;
18+
language: ProgrammingLanguage;
19+
onLanguageChange: (lang: ProgrammingLanguage) => void;
1220
}
1321

14-
// 定义哪些行需要显示哪些变量的值
15-
const LINE_VARIABLE_MAPPING: Record<number, string[]> = {
16-
3: ['res'], // List<List<Integer>> res = new ArrayList<>();
17-
4: ['len'], // int len = nums.length;
18-
5: ['used'], // boolean[] used = new boolean[len];
19-
6: ['path'], // Deque<Integer> path = new ArrayDeque<>();
20-
11: ['depth'], // void dfs(..., int depth, ...)
21-
14: ['depth', 'len'], // if (depth == len)
22-
18: ['i', 'len'], // for (int i = 0; i < len; i++)
23-
19: ['used[i]'], // if (used[i]) continue;
24-
20: ['nums[i]'], // path.addLast(nums[i]);
25-
21: ['used[i]'], // used[i] = true;
26-
};
27-
2822
export function CodeEditor({
2923
currentStepType,
3024
currentPath = [],
3125
available = [],
3226
inputNumbers = [],
3327
resultCount = 0,
28+
language,
29+
onLanguageChange,
3430
}: CodeEditorProps) {
35-
const codeLines = useMemo(() => getJavaPermutationCode(), []);
31+
const codeLines = useMemo(() => getCodeLines(language), [language]);
32+
const langConfig = useMemo(() => getLanguageConfig(language), [language]);
33+
const lineVariableMapping = useMemo(() => getLineVariableMapping(language), [language]);
3634

3735
const highlightedLines = useMemo(() => {
38-
if (!currentStepType) return [];
39-
return JAVA_LINE_MAPPING[currentStepType] || [];
40-
}, [currentStepType]);
36+
return getHighlightedLines(language, currentStepType);
37+
}, [language, currentStepType]);
4138

4239
const currentExecutionLine = useMemo(() => {
43-
return getCurrentExecutionLine(currentStepType);
44-
}, [currentStepType]);
40+
return getCurrentExecutionLine(language, currentStepType);
41+
}, [language, currentStepType]);
4542

4643
// 计算变量值
4744
const getVariableValue = useMemo(() => {
4845
const depth = currentPath.length;
4946
const len = inputNumbers.length;
5047
const used = inputNumbers.map(n => !available.includes(n));
51-
// 当前正在处理的索引 (最后选择的数字在原数组中的位置)
5248
const currentI = currentPath.length > 0
5349
? inputNumbers.indexOf(currentPath[currentPath.length - 1])
5450
: 0;
@@ -58,6 +54,7 @@ export function CodeEditor({
5854
case 'res':
5955
return `size=${resultCount}`;
6056
case 'len':
57+
case 'n':
6158
return `${len}`;
6259
case 'used':
6360
return `[${used.map(u => u ? 'T' : 'F').join(', ')}]`;
@@ -79,7 +76,7 @@ export function CodeEditor({
7976

8077
// 获取某行的变量值显示
8178
const getLineVariableDisplay = (lineNumber: number): string | null => {
82-
const variables = LINE_VARIABLE_MAPPING[lineNumber];
79+
const variables = lineVariableMapping[lineNumber];
8380
if (!variables || inputNumbers.length === 0) return null;
8481

8582
const values = variables
@@ -95,8 +92,23 @@ export function CodeEditor({
9592
return (
9693
<div className="code-editor">
9794
<div className="editor-header">
98-
<span className="file-icon"></span>
99-
<span className="file-name">Solution.java</span>
95+
<div className="file-info">
96+
<span className="file-icon">{langConfig.icon}</span>
97+
<span className="file-name">{langConfig.fileName}</span>
98+
</div>
99+
<div className="language-selector">
100+
{LANGUAGE_CONFIGS.map((config) => (
101+
<button
102+
key={config.id}
103+
className={`lang-btn ${language === config.id ? 'active' : ''}`}
104+
onClick={() => onLanguageChange(config.id)}
105+
title={config.name}
106+
>
107+
<span className="lang-icon">{config.icon}</span>
108+
<span className="lang-name">{config.name}</span>
109+
</button>
110+
))}
111+
</div>
100112
</div>
101113
<div className="editor-content">
102114
{codeLines.map((line) => {
@@ -120,7 +132,10 @@ export function CodeEditor({
120132
</span>
121133
))}
122134
{variableDisplay && (
123-
<span className="inline-variable-value">// {variableDisplay}</span>
135+
<span className="inline-variable-value">
136+
{language === 'python' ? ' # ' : ' // '}
137+
{variableDisplay}
138+
</span>
124139
)}
125140
</div>
126141
</div>
@@ -134,14 +149,13 @@ export function CodeEditor({
134149
/**
135150
* 用于测试的辅助函数:获取高亮行号
136151
*/
137-
export function getHighlightedLineNumbers(stepType: StepType | null): number[] {
138-
if (!stepType) return [];
139-
return JAVA_LINE_MAPPING[stepType] || [];
152+
export function getHighlightedLineNumbers(stepType: StepType | null, lang: ProgrammingLanguage = 'java'): number[] {
153+
return getHighlightedLines(lang, stepType);
140154
}
141155

142156
/**
143157
* 用于测试的辅助函数:获取代码行数
144158
*/
145-
export function getCodeLineCount(): number {
146-
return getJavaPermutationCode().length;
159+
export function getCodeLineCount(lang: ProgrammingLanguage = 'java'): number {
160+
return getCodeLines(lang).length;
147161
}

src/components/JavaDebuggerPanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState } from 'react';
22
import { StepType } from '../types';
3+
import { ProgrammingLanguage } from '../types/languages';
34
import { CodeEditor } from './CodeEditor';
45
import { MemoryPanel } from './MemoryPanel';
56
import './JavaDebuggerPanel.css';
@@ -26,6 +27,7 @@ export function JavaDebuggerPanel({
2627
isExpanded: initialExpanded = true,
2728
}: JavaDebuggerPanelProps) {
2829
const [isExpanded, setIsExpanded] = useState(initialExpanded);
30+
const [language, setLanguage] = useState<ProgrammingLanguage>('java');
2931

3032
return (
3133
<div className={`java-debugger-panel ${isExpanded ? 'expanded' : 'collapsed'}`}>
@@ -43,6 +45,8 @@ export function JavaDebuggerPanel({
4345
available={available}
4446
inputNumbers={inputNumbers}
4547
resultCount={resultCount}
48+
language={language}
49+
onLanguageChange={setLanguage}
4650
/>
4751
<MemoryPanel
4852
currentPath={currentPath}

0 commit comments

Comments
 (0)