diff --git a/docs/.vitepress/theme/components/playground/MagoPlayground.vue b/docs/.vitepress/theme/components/playground/MagoPlayground.vue index 1da79c79..15996338 100644 --- a/docs/.vitepress/theme/components/playground/MagoPlayground.vue +++ b/docs/.vitepress/theme/components/playground/MagoPlayground.vue @@ -3,6 +3,7 @@ import { onMounted, onUnmounted, watch, ref } from 'vue'; import { useMagoWasm } from '../../composables/useMagoWasm.js'; import { createPlaygroundState } from '../../composables/usePlaygroundState.js'; import { useUrlState } from '../../composables/useUrlState.js'; +import { useMagoTasks } from '../../composables/useMagoTasks.js'; import PlaygroundToolbar from './PlaygroundToolbar.vue'; import PlaygroundSettings from './PlaygroundSettings.vue'; import PlaygroundEditor from './PlaygroundEditor.vue'; @@ -11,7 +12,8 @@ import PlaygroundOutput from './PlaygroundOutput.vue'; const store = createPlaygroundState(); const { state } = store; -const { isLoading: wasmLoading, isReady: wasmReady, analyze, format, loadWasm, getRules } = useMagoWasm(); +const { isLoading: wasmLoading, isReady: wasmReady, loadWasm, getRules } = useMagoWasm(); +const { analyze, format } = useMagoTasks(); const { shareError, shareSuccess, isSharing, shareUrl, generateShareUrl, loadFromUrl, copyToClipboard, clearShareUrl, shouldClearShareUrl } = useUrlState(); const hasRunOnce = ref(false); @@ -26,6 +28,12 @@ onMounted(async () => { store.restoreState(urlState); } + const params = new URLSearchParams(window.location.search); + const tabParam = params.get('tab'); + if (tabParam === 'issues' || tabParam === 'formatter') { + store.setActiveTab(tabParam); + } + try { await loadWasm(); store.setWasmReady(true); @@ -51,6 +59,7 @@ watch(wasmReady, async (ready) => { console.error('Failed to load linter rules:', e); } runAnalysis(); + updateFormattedPreview(); } }); @@ -89,12 +98,12 @@ watch( ); async function runAnalysis() { - if (!wasmReady.value) return; + if (!wasmReady.value || state.activeTab !== 'issues') return; store.setLoading(true); try { const results = await analyze(state.code, state.settings); - store.setResults(results); + store.setAnalyzerResults(results); } catch (e) { console.error('Analysis failed:', e); store.setResults({ @@ -107,13 +116,23 @@ async function runAnalysis() { } } +async function updateFormattedPreview() { + if (!wasmReady.value || state.activeTab !== 'formatter') return; + try { + const res = await format(state.code, state.settings.phpVersion); + store.setFormatterResults(res); + } catch (e) { + store.setFormatterResults({ code: state.code || '', timeMs: null, error: e?.message || 'Format failed' }); + } +} + async function handleFormat() { if (!wasmReady.value || state.isLoading) return; store.setLoading(true); try { const formatted = await format(state.code, state.settings.phpVersion); - store.setCode(formatted); + store.setCode(formatted.code); } catch (e) { console.error('Format failed:', e); } finally { @@ -162,6 +181,31 @@ function handleTabChange(tab) { store.setActiveTab(tab); } +watch( + () => state.activeTab, + (tab) => { + const params = new URLSearchParams(window.location.search); + params.set('tab', tab); + const query = `?${params.toString()}`; + const url = `${window.location.pathname}${query}${window.location.hash || ''}`; + window.history.replaceState(null, '', url); + + if (tab === 'formatter') { + return updateFormattedPreview(); + } else if (tab === 'issues') { + return runAnalysis(); + } + } +); + +watch( + () => [state.code, state.settings.phpVersion], + () => { + updateFormattedPreview(); + }, + { deep: true } +); + function handleHighlightLine(range) { highlightedRange.value = range; } @@ -237,10 +281,13 @@ onUnmounted(() => { /> diff --git a/docs/.vitepress/theme/components/playground/PlaygroundOutput.vue b/docs/.vitepress/theme/components/playground/PlaygroundOutput.vue index b2be1960..9f4d0c02 100644 --- a/docs/.vitepress/theme/components/playground/PlaygroundOutput.vue +++ b/docs/.vitepress/theme/components/playground/PlaygroundOutput.vue @@ -1,8 +1,12 @@ @@ -237,6 +319,41 @@ function handleIssueLeave() { background: var(--vp-c-bg-soft); } +.tabs { + display: flex; + gap: 8px; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-secondary { + background: var(--vp-c-bg); + color: var(--vp-c-text-1); + border: 1px solid var(--vp-c-divider); +} + +.tab-btn:hover { + background: var(--vp-c-bg-soft); + border-color: #10b981; +} + +.tab-btn.active { + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand-1); + border-color: var(--vp-c-brand-1); +} + .header-title { font-size: 14px; font-weight: 600; @@ -249,7 +366,7 @@ function handleIssueLeave() { gap: 12px; } -.analysis-time { +.execution-time { font-size: 12px; font-weight: 500; color: var(--vp-c-text-2); @@ -311,12 +428,85 @@ function handleIssueLeave() { background: rgba(168, 85, 247, 0.05); } +.error-badge { + background: rgba(220, 38, 38, 0.1); + color: #dc2626; +} + .output-content { flex: 1; overflow: auto; padding: 0; } +.formatted-view { + padding: 0; + height: 100%; + overflow: auto; +} + +.formatted-pre { + margin: 0; + padding: 16px; + height: 100%; + background: var(--vp-c-bg); +} + +.formatted-pre :deep(.token.comment), +.formatted-pre :deep(.token.prolog), +.formatted-pre :deep(.token.doctype), +.formatted-pre :deep(.token.cdata) { + color: var(--vp-c-text-3); +} + +.formatted-pre :deep(.token.punctuation) { + color: var(--vp-c-text-2); +} + +.formatted-pre :deep(.token.property), +.formatted-pre :deep(.token.tag), +.formatted-pre :deep(.token.boolean), +.formatted-pre :deep(.token.number), +.formatted-pre :deep(.token.constant), +.formatted-pre :deep(.token.symbol), +.formatted-pre :deep(.token.deleted) { + color: #e06c75; +} + +.formatted-pre :deep(.token.selector), +.formatted-pre :deep(.token.attr-name), +.formatted-pre :deep(.token.string), +.formatted-pre :deep(.token.char), +.formatted-pre :deep(.token.builtin), +.formatted-pre :deep(.token.inserted) { + color: #98c379; +} + +.formatted-pre :deep(.token.operator), +.formatted-pre :deep(.token.entity), +.formatted-pre :deep(.token.url), +.formatted-pre :deep(.language-css .token.string), +.formatted-pre :deep(.style .token.string) { + color: #56b6c2; +} + +.formatted-pre :deep(.token.atrule), +.formatted-pre :deep(.token.attr-value), +.formatted-pre :deep(.token.keyword) { + color: #c678dd; +} + +.formatted-pre :deep(.token.function), +.formatted-pre :deep(.token.class-name) { + color: #61afef; +} + +.formatted-pre :deep(.token.regex), +.formatted-pre :deep(.token.important), +.formatted-pre :deep(.token.variable) { + color: #c678dd; +} + .loading, .placeholder, .no-issues { diff --git a/docs/.vitepress/theme/composables/useMagoTasks.js b/docs/.vitepress/theme/composables/useMagoTasks.js new file mode 100644 index 00000000..59ef9b66 --- /dev/null +++ b/docs/.vitepress/theme/composables/useMagoTasks.js @@ -0,0 +1,49 @@ +import { useMagoWasm } from './useMagoWasm.js'; + +export function useMagoTasks() { + const { analyze: doAnalyze, format: doFormat, loadWasm, isReady } = useMagoWasm(); + + async function ensureReady() { + await loadWasm(); + return isReady.value; + } + + async function withTiming(fn) { + const start = performance.now(); + try { + const result = await fn(); + const end = performance.now(); + return { result, timeMs: end - start, error: null }; + } catch (e) { + const end = performance.now(); + return { result: null, timeMs: end - start, error: e?.message || 'Task failed' }; + } + } + + async function analyze(code, settings) { + await ensureReady(); + const { result, timeMs, error } = await withTiming(() => doAnalyze(code, settings)); + if (error) { + return { issues: [], analysisTimeMs: timeMs, error }; + } + return { + issues: result?.issues || [], + analysisTimeMs: timeMs ?? null, + error: null, + }; + } + + async function format(code, phpVersion) { + await ensureReady(); + const { result, timeMs, error } = await withTiming(() => doFormat(code, phpVersion)); + if (error) { + return { code, timeMs, error }; + } + return { code: result, timeMs, error: null }; + } + + return { + analyze, + format, + }; +} diff --git a/docs/.vitepress/theme/composables/usePlaygroundState.js b/docs/.vitepress/theme/composables/usePlaygroundState.js index c333d827..b6ee6a84 100644 --- a/docs/.vitepress/theme/composables/usePlaygroundState.js +++ b/docs/.vitepress/theme/composables/usePlaygroundState.js @@ -54,10 +54,11 @@ export function createPlaygroundState(initialCode = DEFAULT_CODE) { disabledRules: [], }, }, - results: null, + analyzerResults: null, + formatterResults: { code: "", timeMs: null, error: null }, isLoading: false, wasmReady: false, - activeTab: "linter", + activeTab: "issues", settingsOpen: false, availableRules: [], }); @@ -97,8 +98,12 @@ export function createPlaygroundState(initialCode = DEFAULT_CODE) { state.availableRules = rules; }, - setResults(results) { - state.results = results; + setAnalyzerResults(results) { + state.analyzerResults = results; + }, + + setFormatterResults(result) { + state.formatterResults = result; }, setLoading(loading) { diff --git a/docs/.vitepress/theme/composables/useUrlState.js b/docs/.vitepress/theme/composables/useUrlState.js index fbb9d763..0d59278c 100644 --- a/docs/.vitepress/theme/composables/useUrlState.js +++ b/docs/.vitepress/theme/composables/useUrlState.js @@ -54,10 +54,14 @@ export function useUrlState() { } const { uuid } = await response.json(); - shareUrl.value = `${window.location.origin}${window.location.pathname}#${uuid}`; + const params = new URLSearchParams(window.location.search); + const tab = state.t || params.get('tab') || 'issues'; + params.set('tab', tab); + const query = `?${params.toString()}`; + shareUrl.value = `${window.location.origin}${window.location.pathname}${query}#${uuid}`; lastStateHash = stateHash; - window.history.replaceState(null, '', `#${uuid}`); + window.history.replaceState(null, '', `${query}#${uuid}`); return shareUrl.value; } catch (e) { diff --git a/flake.nix b/flake.nix index 0c11b380..130c54c7 100644 --- a/flake.nix +++ b/flake.nix @@ -25,6 +25,7 @@ pkgs.openssl pkgs.just pkgs.wasm-pack + pkgs.lld php composer ] ++ pkgs.lib.optionals isDarwin [