Skip to content

Commit

Permalink
Extract editor panel tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
x0k committed Jun 6, 2024
1 parent f798982 commit a819356
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 219 deletions.
3 changes: 2 additions & 1 deletion src/components/editor/editor-surface.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
}
const PANEL_BORDER_HEIGHT = 1
const MIN_PANEL_HEIGHT = 32 + PANEL_BORDER_HEIGHT
const PANEL_HEADER_VPADDING = 4 * 2
const MIN_PANEL_HEIGHT = 32 + PANEL_HEADER_VPADDING + PANEL_BORDER_HEIGHT
function normalizeHeight(height: number) {
return Math.min(Math.max(height, 0), window.innerHeight - MIN_PANEL_HEIGHT);
Expand Down
6 changes: 3 additions & 3 deletions src/components/editor/editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import EditorSurface from "./editor-surface.svelte";
import LangSelect from "./lang-select.svelte";
import TestingPanel from "./testing-panel.svelte";
import Panel from "./panel/panel.svelte"
import VimMode from './vim-mode.svelte';
interface Props<L extends Language, I, O> {
Expand Down Expand Up @@ -68,7 +68,7 @@

<EditorSurface {model} {widthStorage}>
{#snippet panel({ resizer, api })}
<TestingPanel
<Panel
{api}
{model}
{testsData}
Expand All @@ -79,6 +79,6 @@
<VimMode {api} />
<LangSelect bind:lang {languages} />
{/snippet}
</TestingPanel>
</Panel>
{/snippet}
</EditorSurface>
97 changes: 97 additions & 0 deletions src/components/editor/panel/header.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import Icon from '@iconify/svelte';
import { type SurfaceApi } from '../model';
import { Tab, TAB_TITLES } from './model';
interface Props {
isRunning: boolean;
testsCount: number;
lastTestId: number;
selectedTab: Tab;
api: SurfaceApi;
onRun: () => void;
append: Snippet;
}
let {
selectedTab = $bindable(),
isRunning,
testsCount,
lastTestId,
api,
append,
onRun
}: Props = $props();
interface TabButtonProps { tab: Tab, append?: Snippet }
</script>

<div class="flex items-center gap-3 p-1">
<button
class="btn btn-sm btn-primary"
onclick={onRun}
>
{#if isRunning}
<span class="loading loading-spinner"></span>
{:else}
<Icon class="w-6" icon="lucide:play" />
{/if}
</button>
<div role="tablist" class="tabs panel-tabs">
{#snippet tabButton({ tab, append }: TabButtonProps)}
<a
href="#top"
role="tab"
class="tab"
class:tab-with-badge={append}
class:tab-active={selectedTab === tab}
onclick={() => {
selectedTab = tab
api.showPanel(window.innerHeight/3)
}}
>
{TAB_TITLES[tab]}
{#if append}
{@render append()}
{/if}
</a>
{/snippet}
{#snippet testBadge()}
<div
class="badge"
class:hidden={lastTestId < 0}
class:badge-success={lastTestId === testsCount}
class:badge-error={lastTestId < testsCount && lastTestId >= 0}
>
{lastTestId}/{testsCount}
</div>
{/snippet}
{@render tabButton({ tab: Tab.Tests, append: testBadge })}
{@render tabButton({ tab: Tab.Output })}
{@render tabButton({ tab: Tab.Settings })}
</div>
<div class="grow" ></div>
{@render append()}
<button
class="btn btn-sm btn-ghost"
onclick={() => {
api.togglePanel(window.innerHeight/3)
}}
>
<Icon icon={api.isPanelCollapsed ? "lucide:chevron-up" : "lucide:chevron-down"} />
</button>
</div>

<style>
.tab-with-badge {
@apply flex gap-2 items-center;
}
.panel-tabs {
@apply uppercase;
.tab:not(.tab-active) {
--tab-color: oklch(var(--bc) / 0.5);
}
}
</style>
11 changes: 11 additions & 0 deletions src/components/editor/panel/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum Tab {
Tests = "tests",
Output = "output",
Settings = "settings",
}

export const TAB_TITLES: Record<Tab, string> = {
[Tab.Tests]: "Tests",
[Tab.Output]: "Output",
[Tab.Settings]: "Settings",
};
114 changes: 114 additions & 0 deletions src/components/editor/panel/panel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script lang="ts" generics="Input, Output">
import type { Snippet } from 'svelte';
import type { editor } from "monaco-editor";
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
import { createLogger } from '@/lib/logger';
import {
runTests,
type TestData,
type TestRunnerFactory,
} from "@/lib/testing";
import { type SurfaceApi } from '../model';
import { Tab } from './model';
import { makeTheme } from './terminal'
import TestsTab from './tests.svelte';
import TerminalTab from './terminal.svelte';
import SettingsTab from './settings.svelte';
import TabsHeader from './header.svelte';
interface Props<I, O> {
api: SurfaceApi
model: editor.IModel;
testsData: TestData<I, O>[];
testRunnerFactory: TestRunnerFactory<I, O>;
children: Snippet
header: Snippet
}
let { api, model, testsData, testRunnerFactory, children, header }: Props<Input, Output> = $props();
let isRunning = $state(false);
let lastTestId = $state(-1);
$effect(() => {
testsData;
testRunnerFactory;
isRunning = false;
lastTestId = -1;
});
let selectedTab = $state<Tab>(Tab.Tests)
const term = new Terminal({
theme: makeTheme("business"),
fontFamily: "monospace",
convertEol: true,
rows: 1,
})
const fitAddon = new FitAddon()
term.loadAddon(fitAddon)
$effect(() => () => {
term.dispose()
})
let resizeFrameId: number
$effect(() => {
api.panelHeight;
api.width;
if (selectedTab !== Tab.Output) {
return
}
cancelAnimationFrame(resizeFrameId)
resizeFrameId = requestAnimationFrame(() => {
fitAddon.fit()
})
return () => {
cancelAnimationFrame(resizeFrameId)
}
})
const logger = createLogger(term)
async function handleRun (){
if (isRunning) {
return;
}
isRunning = true;
term.clear();
const runner = await testRunnerFactory({
code: model.getValue(),
out: term,
});
try {
lastTestId = await runTests(logger, runner, testsData);
} finally {
runner[Symbol.dispose]();
isRunning = false;
}
}
</script>

<div class="grow border-t border-base-100 relative flex flex-col bg-base-300 overflow-hidden">
<TabsHeader
bind:selectedTab
{api}
{isRunning}
{lastTestId}
testsCount={testsData.length}
onRun={handleRun}
append={header}
/>
{#if selectedTab === Tab.Tests}
<TestsTab {testsData} {lastTestId} />
{:else if selectedTab === Tab.Settings}
<SettingsTab />
{/if}
<!-- This Tab should't be unmounted -->
<TerminalTab terminal={term} class={selectedTab !== Tab.Output ? "hidden" : ""} />
{@render children()}
</div>
14 changes: 14 additions & 0 deletions src/components/editor/panel/settings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import { vimState } from "../model";
</script>

<div class="grow overflow-auto">
<div class="flex flex-col gap-4 p-4">
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Vim mode</span>
<input type="checkbox" bind:checked={vimState.value} class="checkbox" />
</label>
</div>
</div>
</div>
20 changes: 20 additions & 0 deletions src/components/editor/panel/terminal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import type { Terminal } from "@xterm/xterm";
interface Props {
class: string;
terminal: Terminal;
}
const { terminal, class: className }: Props = $props();
let termElement: HTMLDivElement;
// NOTE: no `dispose` here
// https://github.com/xtermjs/xterm.js/issues/3939#issuecomment-1195377718
$effect(() => {
terminal.open(termElement);
});
</script>

<div bind:this={termElement} class="grow ml-4 mt-4 {className}"></div>
File renamed without changes.
34 changes: 34 additions & 0 deletions src/components/editor/panel/tests.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts" generics="Input, Output">
import Icon from "@iconify/svelte";
import type { TestData } from "@/lib/testing";
interface Props<I, O> {
testsData: TestData<I, O>[];
lastTestId: number;
}
const { testsData, lastTestId }: Props<Input, Output> = $props();
</script>

<div class="overflow-auto grow">
<div class="flex flex-col gap-4 p-4">
{#each testsData as testData, i}
<div>
<div class="flex items-center gap-2 pb-2">
{#if lastTestId === i}
<Icon icon="lucide:circle-x" class="text-error" />
{:else if i < lastTestId}
<Icon icon="lucide:circle-check" class="text-success" />
{:else}
<Icon icon="lucide:circle-dashed" />
{/if}
Case {i + 1}
</div>
<pre class="p-2 rounded bg-base-100"><code
>{JSON.stringify(testData.input, null, 2)}</code
></pre>
</div>
{/each}
</div>
</div>
Loading

0 comments on commit a819356

Please sign in to comment.