Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/components/editor/CodeEditor.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
interface Props {
slug: string
initialCode?: string
}

const { slug, initialCode = 'fn main() {\n println!("Hola, mundo!");\n}\n' } = Astro.props
---

<div
id="editor"
data-slug={slug}
data-initial-code={initialCode}
class="flex-1 min-w-0 min-h-0 overflow-hidden"
role="textbox"
aria-label="Editor de código"
aria-multiline="true"
>
</div>

<script>
import { EditorView, keymap } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { basicSetup } from "codemirror";
import { indentWithTab } from "@codemirror/commands";
import { gruvbox } from "~/components/editor/theme";
import { db, editorCode } from "~/lib/stores/editor-store";
import { rust } from "~/components/editor/config";

let editorView: EditorView | null = null;
let saveTimeout: number | undefined;

async function initEditor() {
const editorElement = document.getElementById("editor");
if (!editorElement) return;

if (editorView) {
editorView.destroy();
editorView = null;
}

const { slug, initialCode } = editorElement.dataset as {
slug: string;
initialCode: string;
};
const initialDoc = (await db.getCode(slug)) ?? initialCode;

editorView = new EditorView({
state: EditorState.create({
doc: initialDoc,
extensions: [
basicSetup,
gruvbox,
rust(),
keymap.of([indentWithTab]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const value = update.state.doc.toString();
editorCode.set(value);
clearTimeout(saveTimeout);
saveTimeout = window.setTimeout(
() => db.saveCode(slug, value),
500,
);
}
}),
],
}),
parent: editorElement,
});

editorView.contentDOM.setAttribute("aria-label", "Editor de código Rust");
editorView.contentDOM.setAttribute("role", "textbox");
editorCode.set(initialDoc);
}

document.addEventListener("astro:page-load", initEditor);

document.addEventListener("astro:before-swap", () => {
if (editorView) {
editorView.destroy();
editorView = null;
}

clearTimeout(saveTimeout);
});
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
LRLanguage,
} from "@codemirror/language"
import { parser } from "@lezer/rust"
import { rustCompletions } from "~/components/shared/editor/keywords"
import { rustCompletions } from "~/components/editor/keywords"

const rustLanguage = LRLanguage.define({
name: "rust",
Expand Down
186 changes: 186 additions & 0 deletions src/components/editor/output/Terminal.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
import IconLoader2 from "~icons/tabler/loader-2"
import IconPlayerPlay from "~icons/tabler/player-play"
import IconTerminal from "~icons/tabler/terminal"
---

<terminal-output
class="h-full flex flex-col bg-dark-fg/60 border-t border-stroke-color text-sm font-mono rounded-b-2xl"
role="region"
aria-label="Terminal output"
>
<header
class="flex items-center justify-between border-b border-stroke-color bg-light-bg px-3 h-8"
>
<div class="flex items-center gap-2">
<IconTerminal font-size={18} aria-hidden="true" />
<h2 class="text-secondary font-medium text-sm">Output</h2>
</div>
<button
type="button"
aria-label="Run code"
class="flex items-center gap-1 px-2 py-1 text-xs text-secondary hover:text-green-500 transition-colors bg-transparent border-none cursor-pointer"
>
<IconPlayerPlay font-size={14} class="play-icon" aria-hidden="true" />
<IconLoader2
font-size={14}
class="loader-icon hidden animate-spin"
aria-hidden="true"
/>
<span class="button-text">Ejecutar</span>
</button>
</header>
<div
class="flex-1 overflow-auto p-3 text-secondary outline-none"
role="log"
aria-live="polite"
aria-atomic="false"
tabindex="0"
>
<div class="command-line text-gray-400 text-sm" role="status">
<span class="text-green-400">$</span>
<span class="text-gray-300">cargo run</span>
</div>
</div>
</terminal-output>

<script>
import { rustPlayground } from "~/lib/rust-playground";
import { editorCode } from "~/lib/stores/editor-store";
import { getStderrHTML } from "~/components/editor/output/renderer";

class RustOutput extends HTMLElement {
private output!: HTMLElement;
private stderrSection!: HTMLElement;
private stdoutSection!: HTMLElement;
private separator!: HTMLElement;
private inputLine: HTMLElement | null = null;
private input: HTMLInputElement | null = null;
private lastStderr = "";
private stdoutBuffer = "";
private pendingRender = false;

connectedCallback() {
this.output = this.querySelector("[role='log']")!;
this.querySelector("button")!.addEventListener("click", () => this.run());
}

private sendInput() {
if (!this.input?.value) return;
rustPlayground.sendStdin(`${this.input.value}\n`);
this.input.value = "";
this.output.scrollTop = this.output.scrollHeight;
}

private run() {
this.toggleRunning(true);
this.reset();
this.toggleInputLine(true);
this.output.focus();

rustPlayground.execute(editorCode.get(), (stderr, stdout, done) => {
if (stderr !== this.lastStderr) {
this.lastStderr = stderr;
this.stderrSection.innerHTML = getStderrHTML(stderr);
}

if (stdout) {
this.stdoutBuffer = stdout;
this.scheduleRender();
}

this.separator.hidden = !stderr || !stdout;

if (done) {
this.toggleRunning(false);
this.toggleInputLine(false);
this.flushRender();
}
});
}

private toggleInputLine(show: boolean) {
this.inputLine?.remove();
this.inputLine = null;
this.input = null;

if (show) {
this.inputLine = document.createElement("div");
this.inputLine.className =
"text-gray-300 text-sm flex items-center bg-dark-fg/40 -mx-3 px-3 py-1 border-l-2 border-gray-600";

this.input = document.createElement("input");
this.input.type = "text";
this.input.className =
"flex-1 bg-transparent outline-none text-gray-300 font-mono text-sm";
this.input.placeholder = "";
this.input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
this.sendInput();
}
});

this.inputLine.innerHTML = `<span class="text-green-400 mr-1">›</span>`;
this.inputLine.appendChild(this.input);
this.output.appendChild(this.inputLine);
this.input.focus();
}
}

private scheduleRender() {
if (this.pendingRender) return;

this.pendingRender = true;
requestAnimationFrame(() => this.flushRender());
}

private flushRender() {
this.pendingRender = false;
const atBottom =
this.output.scrollHeight -
this.output.scrollTop -
this.output.clientHeight <
5;

this.stdoutSection.textContent = this.stdoutBuffer;

if (atBottom) this.output.scrollTop = this.output.scrollHeight;
}

private toggleRunning(running: boolean) {
this.querySelector(".play-icon")!.classList.toggle("hidden", running);
this.querySelector(".loader-icon")!.classList.toggle("hidden", !running);
this.querySelector(".button-text")!.textContent = running
? "Ejecutando..."
: "Ejecutar";
}

private reset() {
const commandLine = this.output.querySelector(".command-line")!;
this.output.textContent = "";
this.output.appendChild(commandLine);

this.stderrSection = document.createElement("pre");
this.stderrSection.className = "text-gray-400 text-sm whitespace-pre";

this.separator = document.createElement("hr");
this.separator.className = "border-t border-gray-700 my-2";
this.separator.hidden = true;

this.stdoutSection = document.createElement("pre");
this.stdoutSection.className = "text-gray-300 text-sm whitespace-pre";

this.output.append(
this.stderrSection,
this.separator,
this.stdoutSection,
);
this.lastStderr = "";
this.stdoutBuffer = "";
this.pendingRender = false;
}
}

customElements.define("terminal-output", RustOutput);
</script>
File renamed without changes.
80 changes: 0 additions & 80 deletions src/components/shared/editor/CodeEditor.astro

This file was deleted.

Loading
Loading