Code editor: block flip-flop colors, view option buttons, terminal error improvements, live server log bubble
- Add alternating block background colors (ff-block-even/odd) for functions/classes - Add ¶빈줄숨김 / ▓블록색상 toggle buttons to file panel sidebar (below file tree) - Fix terminal error send: filter Windows prompt+command lines, only send last Traceback block, strip pygame header noise - Show live server log bubble during AI tool calls, fade out on completion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+256
-41
@@ -127,15 +127,16 @@ function initCodeView() {
|
||||
theme: isDark ? 'vs-dark' : 'vs',
|
||||
language: 'plaintext',
|
||||
automaticLayout: true,
|
||||
fontSize: 12,
|
||||
lineHeight: 19,
|
||||
fontFamily: "'IBM Plex Mono', 'Fira Code', 'Courier New', monospace",
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
fontFamily: "'D2Coding', 'D2 Coding', monospace",
|
||||
minimap: { enabled: true },
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
renderWhitespace: 'selection',
|
||||
smoothScrolling: true,
|
||||
cursorBlinking: 'smooth',
|
||||
mouseWheelZoom: true,
|
||||
tabSize: 2,
|
||||
});
|
||||
monacoEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, codeSaveFile);
|
||||
@@ -195,6 +196,8 @@ function switchCodeFile(id) {
|
||||
}
|
||||
renderCodeFileTabs();
|
||||
codeUpdateRunButton();
|
||||
if (_hideBlankLines) setTimeout(_applyHideBlankLines, 50);
|
||||
if (_blockFlipFlop) setTimeout(_applyBlockFlipFlop, 60);
|
||||
}
|
||||
|
||||
function closeCodeFile(id, ev) {
|
||||
@@ -444,11 +447,18 @@ async function _clientLoadProjectFiles() {
|
||||
// errors after a run and offer one-click "AI에게 전송" from the terminal.
|
||||
const _termBuffers = {}; // shellId → string[] (last 300 lines)
|
||||
const _TERM_BUF_MAX = 300;
|
||||
const _ANSI_RE = /\x1b\[[0-9;]*[mGKHFABCDJnsuhl]/g;
|
||||
function _stripTerminal(raw) {
|
||||
return raw
|
||||
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC (title 등)
|
||||
.replace(/\x1b\[[\d;?]*[a-zA-Z]/g, '') // CSI (색상, 커서, private mode)
|
||||
.replace(/\x1b[^[\]]/g, '') // 기타 ESC 시퀀스
|
||||
.replace(/[\x00-\x09\x0b-\x1f\x7f]/g, '') // 제어문자 (\n 제외)
|
||||
.replace(/\r/g, '');
|
||||
}
|
||||
|
||||
function _termBufferAppend(shellId, raw) {
|
||||
if (!shellId) return;
|
||||
const text = raw.replace(_ANSI_RE, '').replace(/\r/g, '');
|
||||
const text = _stripTerminal(raw);
|
||||
if (!text) return;
|
||||
const lines = text.split('\n');
|
||||
if (!_termBuffers[shellId]) _termBuffers[shellId] = [];
|
||||
@@ -484,25 +494,61 @@ function _termDetectAndShowError(shellId) {
|
||||
|
||||
function _showTerminalErrorButton(shellId, errorText) {
|
||||
const btn = document.getElementById('code-term-ai-error-btn');
|
||||
if (btn) { btn.style.background = '#dc2626'; btn.style.color = '#fff'; btn.dataset.shellId = shellId; }
|
||||
if (btn) { btn.style.display = ''; btn.style.background = '#dc2626'; btn.style.color = '#fff'; btn.dataset.shellId = shellId; }
|
||||
}
|
||||
|
||||
function _sendTerminalErrorToAI(errorText) {
|
||||
const input = document.getElementById('code-ai-input');
|
||||
if (!input) return;
|
||||
const trimmed = errorText.split('\n').filter(l => l.trim()).slice(-25).join('\n');
|
||||
|
||||
// Filter noise lines
|
||||
const filtered = errorText.split('\n').filter(l => {
|
||||
const t = l.trim();
|
||||
if (!t) return false;
|
||||
if (/^[A-Za-z]:\\.+>/.test(t)) return false; // Windows 프롬프트 (명령어 포함)
|
||||
if (/^[~/][^\s]*[$#]\s*/.test(t)) return false; // Unix 프롬프트
|
||||
if (/^Hello from the pygame community/.test(t)) return false;
|
||||
if (/^https?:\/\/www\.pygame\.org/.test(t)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// 마지막 Traceback 블록만 추출
|
||||
let blockStart = 0;
|
||||
for (let i = filtered.length - 1; i >= 0; i--) {
|
||||
if (/Traceback \(most recent call last\)/.test(filtered[i])) {
|
||||
blockStart = i; break;
|
||||
}
|
||||
}
|
||||
const trimmed = filtered.slice(blockStart).slice(-30).join('\n');
|
||||
if (!trimmed) return;
|
||||
|
||||
input.value = '다음 에러를 고쳐줘. edit_file 도구로 파일을 직접 수정해:\n```\n' + trimmed + '\n```';
|
||||
input.style.height = '';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
// Auto-send
|
||||
codeAiSend();
|
||||
}
|
||||
|
||||
function _getXtermVisibleText(term, maxLines = 80) {
|
||||
const buf = term.buffer.active;
|
||||
const lines = [];
|
||||
const endRow = buf.baseY + buf.cursorY;
|
||||
const startRow = Math.max(0, endRow - maxLines);
|
||||
for (let i = startRow; i <= endRow; i++) {
|
||||
const line = buf.getLine(i);
|
||||
if (!line) continue;
|
||||
const t = line.translateToString(true);
|
||||
if (t.trim()) lines.push(t);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function _sendActiveTermErrorToAI() {
|
||||
const btn = document.getElementById('code-term-ai-error-btn');
|
||||
const shellId = btn?.dataset.shellId || _lastErrorShellId;
|
||||
const buf = (shellId && _termBuffers[shellId]) || Object.values(_termBuffers).at(-1);
|
||||
const text = buf ? buf.slice(-40).join('\n') : _lastErrorText;
|
||||
const shellId = btn?.dataset.shellId || _lastErrorShellId || activeCodeTermShellId;
|
||||
const shell = codeTermShells.find(s => s.id === shellId) || codeTermShells.find(s => s.id === activeCodeTermShellId);
|
||||
const text = (shell?.term ? _getXtermVisibleText(shell.term) : '')
|
||||
|| (shellId && _termBuffers[shellId] || Object.values(_termBuffers).at(-1))?.slice(-40).join('\n')
|
||||
|| _lastErrorText;
|
||||
if (text) _sendTerminalErrorToAI(text);
|
||||
if (btn) { btn.style.background = '#1a3a28'; btn.style.color = '#22c55e'; }
|
||||
}
|
||||
@@ -559,10 +605,9 @@ function handleCompanionMessage(msg) {
|
||||
if (msg.type === 'shell_ready') {
|
||||
if (msg.platform) companionPlatform = msg.platform;
|
||||
if (msg.home) companionHome = msg.home;
|
||||
s.term.write('\x1b[32m🖥️ 로컬 셸\x1b[0m\r\n');
|
||||
_companionCdProject(msg.shellId);
|
||||
}
|
||||
else if (msg.type === 'shell_output') s.term.write(msg.data);
|
||||
else if (msg.type === 'shell_output') { s.term.write(msg.data); _termBufferAppend(s.id, msg.data); }
|
||||
else if (msg.type === 'shell_exit') { s.term.write(`\r\n\x1b[33m[셸 종료 ${msg.code}]\x1b[0m\r\n`); }
|
||||
else if (msg.type === 'started') s.term.write(`\r\n\x1b[32m▶ ${msg.filename}\x1b[0m\r\n`);
|
||||
else if (msg.type === 'output') { s.term.write(msg.data.replace(/(?<!\r)\n/g, '\r\n')); _termBufferAppend(s.id, msg.data); }
|
||||
@@ -596,15 +641,10 @@ function _companionCdProject(shellId) {
|
||||
const base = (localTermPath || companionHome || '').replace(/[/\\]$/, '');
|
||||
if (!base) return;
|
||||
const target = base + sep + activeProjectName;
|
||||
const cmd = isWin ? `cd /d "${target}"\r` : `cd ${JSON.stringify(target)}\r`;
|
||||
const cmd = isWin
|
||||
? `set "PROMPT=$E[A$P$G" && cd /d "${target}" && cls\r`
|
||||
: `cd ${JSON.stringify(target)} && clear\r`;
|
||||
companionWs.send(JSON.stringify({ type: 'input', shellId: sid, data: cmd }));
|
||||
// After cd completes, clear via xterm API so the prompt is always visible
|
||||
setTimeout(() => {
|
||||
const shell = codeTermShells.find(s => s.id === sid);
|
||||
if (shell?.term) shell.term.clear();
|
||||
if (companionConnected && companionWs)
|
||||
companionWs.send(JSON.stringify({ type: 'input', shellId: sid, data: '\r' }));
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function csbLoadCodeDir() {}
|
||||
@@ -795,7 +835,7 @@ function toggleCodeTerminal() {
|
||||
if (codeTermOpen) {
|
||||
panel.style.display = 'flex';
|
||||
if (btn) btn.classList.add('active');
|
||||
if (!codeTermShells.length) { if (companionConnected) addLocalShell(); else addCodeTermShell(); }
|
||||
if (!codeTermShells.length) { requestAnimationFrame(() => { if (companionConnected) addLocalShell(); else addCodeTermShell(); }); }
|
||||
else { setTimeout(() => { const s = codeTermShells.find(s => s.id === activeCodeTermShellId); if (s?.fitAddon) s.fitAddon.fit(); if (s?.term) s.term.focus(); }, 50); }
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
@@ -2547,6 +2587,9 @@ async function _codeAiSendImpl() {
|
||||
// 파일 작업 키워드가 포함된 항목이 있으면 실행 단계 목록 → 선택지 아님
|
||||
const fileOpRe = /edit_file|create_file|move_file|delete_file|replace_lines|insert_after|\.[a-zA-Z]{2,4}(?:\s|$|—|:)/;
|
||||
if (opts.some(o => fileOpRe.test(o.label))) return [];
|
||||
if (/수정\s*계획|변경\s*계획|개선\s*계획|구현\s*계획/.test(planText)) return [];
|
||||
const firstPart = planText.slice(0, planText.length * 0.6);
|
||||
if (opts.every(o => firstPart.includes(o.label))) return [];
|
||||
return opts;
|
||||
})();
|
||||
|
||||
@@ -2708,8 +2751,9 @@ async function _codeAiSendImpl() {
|
||||
const dec = new TextDecoder();
|
||||
let buf = '', reply = '', _preambleBuf = '', _firstToolSeen = false;
|
||||
let liveTextEl = null, segReply = '';
|
||||
thinkEl.textContent = '⏳';
|
||||
thinkEl.textContent = '🤔 분석 중...';
|
||||
thinkEl.className = 'code-ai-msg thinking';
|
||||
const _statusMove = (text) => { thinkEl.textContent = text; thinkEl.style.display = ''; const msgs = document.getElementById('code-ai-msgs'); if (msgs) msgs.appendChild(thinkEl); codeAiScrollBottom(); };
|
||||
// Live streaming state (mirrored into the editor via codeLiveStreamUpdate)
|
||||
// Pin the diff target to the file that was active when the user hit send —
|
||||
// that's the file we attached as "[현재 파일: …]" context, so any returned
|
||||
@@ -2737,7 +2781,7 @@ async function _codeAiSendImpl() {
|
||||
else if (ev.type === 'done' && ev.reply) { if (!reply) reply = ev.reply; (liveTextEl || thinkEl).textContent = reply; codeAiScrollBottom(); continue; }
|
||||
// Handle tool events for live editor update + action cards
|
||||
else if (ev.type === 'tool_call') {
|
||||
if (!_firstToolSeen) { _firstToolSeen = true; thinkEl.style.display = 'none'; }
|
||||
_firstToolSeen = true;
|
||||
// Close current text bubble before showing tool card
|
||||
if (liveTextEl) {
|
||||
if (!segReply.trim()) liveTextEl.remove();
|
||||
@@ -2746,6 +2790,11 @@ async function _codeAiSendImpl() {
|
||||
}
|
||||
const toolName = ev.action || ev.name || '';
|
||||
const toolArgs = ev.args || {};
|
||||
const _fname = toolArgs.filename || toolArgs.name || toolArgs.path || '';
|
||||
const _short = _fname ? _fname.split('/').pop() : '';
|
||||
const _tIcon = _TOOL_ICONS[toolName] || '🔧';
|
||||
_statusMove(`${_tIcon} ${toolName}${_short ? ': ' + _short : ''} ...`);
|
||||
_showLogBubble();
|
||||
liveState.toolCallCount = (liveState.toolCallCount || 0) + 1;
|
||||
_codeAiToolAction(toolName, toolArgs, null, null);
|
||||
// ── Client-side file intercept: apply tool changes to local folder ──
|
||||
@@ -2765,6 +2814,7 @@ async function _codeAiSendImpl() {
|
||||
const toolResult = ev.result || '';
|
||||
const toolError = ev.error || false;
|
||||
_codeAiToolAction(toolName, null, toolResult, toolError);
|
||||
_statusMove('💭 다음 단계 분석 중...');
|
||||
// Refresh file list after successful file-modifying tools
|
||||
if (!toolError && ['create_file', 'replace_lines', 'insert_after', 'delete_lines', 'find_replace', 'edit_file', 'move_file', 'delete_file'].includes(toolName)) {
|
||||
codeSbRefresh();
|
||||
@@ -2772,7 +2822,11 @@ async function _codeAiSendImpl() {
|
||||
}
|
||||
if (token) {
|
||||
reply += token; segReply += token;
|
||||
if (!liveTextEl) { liveTextEl = codeAiAppend('assistant', ''); liveTextEl.classList.add('streaming'); }
|
||||
if (!liveTextEl) {
|
||||
liveTextEl = codeAiAppend('assistant', '');
|
||||
liveTextEl.classList.add('streaming');
|
||||
if (_firstToolSeen) _statusMove('💬 응답 작성 중...');
|
||||
}
|
||||
liveTextEl.textContent = segReply; codeAiScrollBottom();
|
||||
if (_firstToolSeen) {
|
||||
try { codeLiveStreamUpdate(segReply, liveState); } catch(e) { console.error('[codeLiveStream] error:', e); liveTextEl.textContent += '\n⚠️ 에디터 스트리밍 오류: ' + (e.message || e); }
|
||||
@@ -3138,16 +3192,20 @@ async function _codeAiSendImpl() {
|
||||
try { updateSessionPanel(); } catch (_) {}
|
||||
thinkEl.classList.remove('streaming');
|
||||
if (_firstToolSeen) {
|
||||
thinkEl.style.display = 'none';
|
||||
thinkEl.remove();
|
||||
_hideLogBubble();
|
||||
if (liveTextEl) { liveTextEl.classList.remove('streaming'); codeAiRenderMarkdown(liveTextEl, segReply); }
|
||||
const doneEl = codeAiAppend('system', '✅ 완료');
|
||||
doneEl.style.cssText = 'font-weight:600;color:#4ade80;';
|
||||
} else {
|
||||
_hideLogBubble();
|
||||
thinkEl.className = 'code-ai-msg assistant';
|
||||
if (!reply) { thinkEl.textContent = '(응답 없음)'; thinkEl.style.display = ''; }
|
||||
else { codeAiRenderMarkdown(thinkEl, reply); thinkEl.style.display = ''; }
|
||||
}
|
||||
} catch(e) {
|
||||
_hideLogBubble();
|
||||
if (!thinkEl.isConnected) { const msgs = document.getElementById('code-ai-msgs'); if (msgs) msgs.appendChild(thinkEl); }
|
||||
thinkEl.style.display = '';
|
||||
thinkEl.className = 'code-ai-msg thinking';
|
||||
const errMsg = (e.name === 'AbortError' || (e.message || '').includes('aborted'))
|
||||
@@ -3162,6 +3220,42 @@ async function _codeAiSendImpl() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Live server log bubble ────────────────────────────────────────────────
|
||||
let _logBubble = null;
|
||||
let _logPollTimer = null;
|
||||
|
||||
function _showLogBubble() {
|
||||
if (_logBubble) return;
|
||||
const msgs = document.getElementById('code-ai-messages');
|
||||
if (!msgs) return;
|
||||
_logBubble = document.createElement('div');
|
||||
_logBubble.className = 'code-ai-log-bubble';
|
||||
_logBubble.innerHTML = '<span class="log-live-dot"></span><span class="log-lines">서버 연결 중...</span>';
|
||||
msgs.appendChild(_logBubble);
|
||||
codeAiScrollBottom();
|
||||
_pollLogBubble();
|
||||
_logPollTimer = setInterval(_pollLogBubble, 800);
|
||||
}
|
||||
|
||||
async function _pollLogBubble() {
|
||||
if (!_logBubble) return;
|
||||
try {
|
||||
const r = await fetch('/api/logs', { headers: authHeaders({}) });
|
||||
if (!r.ok) return;
|
||||
const data = await r.json();
|
||||
const lines = (data.entries || []).slice(-4).map(e => e.msg.slice(0, 90));
|
||||
const linesEl = _logBubble.querySelector('.log-lines');
|
||||
if (linesEl) linesEl.textContent = lines.join('\n') || '...';
|
||||
codeAiScrollBottom();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function _hideLogBubble() {
|
||||
if (_logPollTimer) { clearInterval(_logPollTimer); _logPollTimer = null; }
|
||||
const b = _logBubble; _logBubble = null;
|
||||
if (b) { b.style.opacity = '0'; setTimeout(() => b.remove(), 400); }
|
||||
}
|
||||
|
||||
// ── Tool action cards for Cline-style visibility ──────────────────────────
|
||||
const _TOOL_ICONS = {
|
||||
read_file: '📖', list_files: '📂', search_files: '🔍',
|
||||
@@ -5226,24 +5320,126 @@ function csbWordWrap(checked) {
|
||||
localStorage.setItem('codeEditorWordWrap', checked ? '1' : '0');
|
||||
}
|
||||
|
||||
function csbFontWeight(val) {
|
||||
localStorage.setItem('codeEditorFontWeight', val);
|
||||
if (monacoEditor) monacoEditor.updateOptions({ fontWeight: val });
|
||||
}
|
||||
|
||||
let _hideBlankLines = false;
|
||||
function csbHideBlankLines(checked) {
|
||||
_hideBlankLines = !!checked;
|
||||
localStorage.setItem('codeEditorHideBlank', _hideBlankLines ? '1' : '0');
|
||||
const btn = document.getElementById('code-opt-hideblank');
|
||||
if (btn) btn.classList.toggle('active', _hideBlankLines);
|
||||
_applyHideBlankLines();
|
||||
}
|
||||
function _applyHideBlankLines() {
|
||||
if (!monacoEditor) return;
|
||||
if (!_hideBlankLines) { try { monacoEditor.setHiddenAreas([]); } catch {} return; }
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) return;
|
||||
const n = model.getLineCount();
|
||||
const ranges = [];
|
||||
let start = -1;
|
||||
let inBlock = false, blockEnd = '';
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const l = model.getLineContent(i).trim();
|
||||
let hide = false;
|
||||
if (inBlock) {
|
||||
hide = true;
|
||||
if (l.includes(blockEnd)) inBlock = false;
|
||||
} else if (l === '') {
|
||||
hide = true;
|
||||
} else if (/^(\/\/|#|--|;)/.test(l)) {
|
||||
hide = true;
|
||||
} else if (l.startsWith('/*')) {
|
||||
hide = true;
|
||||
if (!l.slice(2).includes('*/')) { inBlock = true; blockEnd = '*/'; }
|
||||
} else if (l.startsWith('<!--')) {
|
||||
hide = true;
|
||||
if (!l.slice(4).includes('-->')) { inBlock = true; blockEnd = '-->'; }
|
||||
} else if (l.startsWith('"""') || l.startsWith("'''")) {
|
||||
const d = l.slice(0, 3);
|
||||
hide = true;
|
||||
if (!l.slice(3).includes(d)) { inBlock = true; blockEnd = d; }
|
||||
}
|
||||
if (hide) { if (start < 0) start = i; }
|
||||
else { if (start >= 0) { ranges.push({ startLineNumber: start, startColumn: 1, endLineNumber: i - 1, endColumn: 1 }); start = -1; } }
|
||||
}
|
||||
if (start >= 0) ranges.push({ startLineNumber: start, startColumn: 1, endLineNumber: n, endColumn: 1 });
|
||||
try { monacoEditor.setHiddenAreas(ranges); } catch {}
|
||||
}
|
||||
|
||||
let _blockFlipFlop = false;
|
||||
let _blockFFDecoIds = [];
|
||||
|
||||
function csbBlockFlipFlop(checked) {
|
||||
_blockFlipFlop = !!checked;
|
||||
localStorage.setItem('codeEditorBlockFF', _blockFlipFlop ? '1' : '0');
|
||||
const btn = document.getElementById('code-opt-blockff');
|
||||
if (btn) btn.classList.toggle('active', _blockFlipFlop);
|
||||
_applyBlockFlipFlop();
|
||||
}
|
||||
|
||||
function _applyBlockFlipFlop() {
|
||||
if (!monacoEditor) return;
|
||||
const model = monacoEditor.getModel();
|
||||
if (!model) return;
|
||||
_blockFFDecoIds = monacoEditor.deltaDecorations(_blockFFDecoIds, []);
|
||||
if (!_blockFlipFlop) return;
|
||||
|
||||
const n = model.getLineCount();
|
||||
const decos = [];
|
||||
|
||||
// Detect top-level function/class/def declarations
|
||||
const declRe = /^(export\s+)?(default\s+)?(async\s+)?(function\b|class\b|def\b|async\s+def\b)|^(const|let|var)\s+\w+\s*=\s*(async\s+)?(function|\(|[A-Za-z_$]\w*\s*=>)/;
|
||||
const boundaries = [];
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const line = model.getLineContent(i);
|
||||
if (line.match(/^(\s*)/)[1].length === 0 && declRe.test(line.trim())) {
|
||||
boundaries.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const ffCls = (idx) => idx % 2 === 0 ? 'ff-block-even' : 'ff-block-odd';
|
||||
const ffRange = (from, to, idx) => ({ range: { startLineNumber: from, startColumn: 1, endLineNumber: to, endColumn: 1 }, options: { isWholeLine: true, className: ffCls(idx) } });
|
||||
|
||||
if (boundaries.length >= 2) {
|
||||
// Declaration-based: alternate between declaration boundaries
|
||||
for (let b = 0; b < boundaries.length; b++) {
|
||||
const from = boundaries[b];
|
||||
const to = b + 1 < boundaries.length ? boundaries[b + 1] - 1 : n;
|
||||
decos.push(ffRange(from, to, b));
|
||||
}
|
||||
} else {
|
||||
// Fallback: blank-line-separated groups
|
||||
let phase = 0, blockStart = -1;
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const blank = model.getLineContent(i).trim() === '';
|
||||
if (!blank && blockStart < 0) { blockStart = i; }
|
||||
else if (blank && blockStart >= 0) {
|
||||
decos.push(ffRange(blockStart, i - 1, phase));
|
||||
blockStart = -1; phase++;
|
||||
}
|
||||
}
|
||||
if (blockStart >= 0) decos.push(ffRange(blockStart, n, phase));
|
||||
}
|
||||
|
||||
_blockFFDecoIds = monacoEditor.deltaDecorations([], decos);
|
||||
}
|
||||
|
||||
const CODE_THEME_GROUPS = [
|
||||
{ icon: '🌙', label: '다크', themes: [
|
||||
{ v: 'vs-dark', label: 'VS Dark', bg: '#1e1e1e' },
|
||||
{ v: 'one-dark', label: 'One Dark', bg: '#282c34' },
|
||||
{ v: 'nord', label: 'Nord', bg: '#2e3440' },
|
||||
{ v: 'github-dark', label: 'GitHub Dark', bg: '#0d1117' },
|
||||
{ v: 'tomorrow-night', label: 'Tomorrow Night',bg: '#1d1f21' },
|
||||
{ v: 'hc-black', label: 'High Contrast', bg: '#000000' },
|
||||
{ v: 'github-dark', label: 'GitHub Dark', bg: '#0d1117' },
|
||||
{ v: 'one-dark', label: 'One Dark', bg: '#282c34' },
|
||||
{ v: 'dracula', label: 'Dracula', bg: '#282a36' },
|
||||
{ v: 'nord', label: 'Nord', bg: '#2e3440' },
|
||||
{ v: 'vs-dark', label: 'VS Dark', bg: '#1e1e1e' },
|
||||
{ v: 'monokai', label: 'Monokai', bg: '#272822' },
|
||||
]},
|
||||
{ icon: '☀️', label: '라이트', themes: [
|
||||
{ v: 'vs', label: 'VS Light', bg: '#ffffff' },
|
||||
{ v: 'github-light', label: 'GitHub Light',bg: '#f6f8fa' },
|
||||
]},
|
||||
{ icon: '🌿', label: '웜', themes: [
|
||||
{ v: 'monokai', label: 'Monokai', bg: '#272822' },
|
||||
{ v: 'dracula', label: 'Dracula', bg: '#282a36' },
|
||||
{ v: 'solarized-dark', label: 'Solarized Dark', bg: '#002b36' },
|
||||
{ v: 'solarized-light', label: 'Solarized Light',bg: '#fdf6e3' },
|
||||
{ v: 'github-light', label: 'GitHub Light', bg: '#f6f8fa' },
|
||||
{ v: 'vs', label: 'VS Light', bg: '#ffffff' },
|
||||
]},
|
||||
];
|
||||
const CODE_THEMES = CODE_THEME_GROUPS.flatMap(g => g.themes);
|
||||
@@ -5353,6 +5549,11 @@ function csbMinimap(checked) {
|
||||
localStorage.setItem('codeEditorMinimap', checked ? '1' : '0');
|
||||
}
|
||||
|
||||
function csbFontFamily(val) {
|
||||
localStorage.setItem('codeEditorFontFamily', val);
|
||||
if (monacoEditor) monacoEditor.updateOptions({ fontFamily: val === 'monospace' ? 'monospace' : `'${val}', monospace` });
|
||||
}
|
||||
|
||||
// Restore editor settings from localStorage
|
||||
function csbRestoreSettings() {
|
||||
const fs = localStorage.getItem('codeEditorFontSize');
|
||||
@@ -5360,16 +5561,30 @@ function csbRestoreSettings() {
|
||||
const th = localStorage.getItem('codeEditorTheme');
|
||||
const ts = localStorage.getItem('codeEditorTabSize');
|
||||
const mm = localStorage.getItem('codeEditorMinimap');
|
||||
const ff = localStorage.getItem('codeEditorFontFamily') || 'D2Coding';
|
||||
const fw = localStorage.getItem('codeEditorFontWeight') || '400';
|
||||
const hb = localStorage.getItem('codeEditorHideBlank');
|
||||
const bff = localStorage.getItem('codeEditorBlockFF');
|
||||
if (fs) { document.getElementById('csb-fontsize').value = fs; document.getElementById('csb-fontsize-label').textContent = fs; }
|
||||
if (ww !== null) document.getElementById('csb-wordwrap').checked = ww === '1';
|
||||
if (ts) document.getElementById('csb-tabsize').value = ts;
|
||||
if (mm !== null) document.getElementById('csb-minimap').checked = mm === '1';
|
||||
const ffEl = document.getElementById('csb-fontfamily'); if (ffEl) ffEl.value = ff;
|
||||
const fwEl = document.getElementById('csb-fontweight'); if (fwEl) fwEl.value = fw;
|
||||
_hideBlankLines = hb === '1';
|
||||
_blockFlipFlop = bff === '1';
|
||||
const hbBtn = document.getElementById('code-opt-hideblank'); if (hbBtn) hbBtn.classList.toggle('active', _hideBlankLines);
|
||||
const bffBtn = document.getElementById('code-opt-blockff'); if (bffBtn) bffBtn.classList.toggle('active', _blockFlipFlop);
|
||||
if (monacoEditor) monacoEditor.updateOptions({
|
||||
fontSize: fs ? parseInt(fs) : 12,
|
||||
fontSize: fs ? parseInt(fs) : 14,
|
||||
wordWrap: ww === '0' ? 'off' : 'on',
|
||||
tabSize: ts ? parseInt(ts) : 2,
|
||||
minimap: { enabled: mm !== '0' },
|
||||
fontFamily: ff === 'monospace' ? 'monospace' : `'${ff}', monospace`,
|
||||
fontWeight: fw,
|
||||
});
|
||||
if (_hideBlankLines) _applyHideBlankLines();
|
||||
if (_blockFlipFlop) _applyBlockFlipFlop();
|
||||
const activeTheme = th || (document.documentElement.getAttribute('data-theme') !== 'light' ? 'vs-dark' : 'vs');
|
||||
if (window.monaco) { defineCodeThemes(); monaco.editor.setTheme(activeTheme); }
|
||||
renderThemeGrid();
|
||||
|
||||
Reference in New Issue
Block a user