Skip to content

Commit

Permalink
Merge pull request #1 from pydantic/tabs
Browse files Browse the repository at this point in the history
add tabs for different files
  • Loading branch information
samuelcolvin authored Jan 18, 2025
2 parents 9ed659f + 74e0dd6 commit 49d8f9d
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- uses: actions/setup-node@v4

- run: npm install
- run: num run build
- run: npm run build

check:
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
# - id: no-commit-to-branch
- id: no-commit-to-branch
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
Expand Down
55 changes: 46 additions & 9 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Convert from 'ansi-to-html'
import Editor from './editor'
import Worker from './worker?worker'
import defaultPythonCode from './default_code.py?raw'
import type { WorkerResponse, RunCode } from './types'
import type { WorkerResponse, RunCode, File } from './types'

const decoder = new TextDecoder()
const ansiConverter = new Convert()
Expand All @@ -18,19 +18,55 @@ export default function () {
let worker: Worker
let outputRef!: HTMLPreElement

function workerMessage(data: RunCode) {
const [files, setFiles] = createSignal<File[]>(getFiles())

function workerMessage(warmup: boolean = false) {
const data: RunCode = { files: files(), warmup }
worker!.postMessage(data)
}

function runCode(user_code: string) {
function runCode(newContent: string) {
setFiles((prev) =>
prev.map(({ name, content, active }) => {
if (active) {
return { name, content: newContent, active }
} else {
return { name, content, active }
}
}),
)
setStatus('Launching Python...')
setInstalled('')
setOutputHtml('')
terminalOutput = ''
workerMessage({ user_code })
workerMessage()
}

const initialCode = getInitialCode()
// function changeTab(updateContent: string, newName: string) {
// setFiles((prev) =>
// prev.map(({ name, content, active }) => {
// if (name == newName) {
// return { name, content, active: true }
// } else if (active) {
// return { name, content: updateContent, active: false }
// } else {
// return { name, content, active }
// }
// }),
// )
// }
//
// function newTab() {
// const newFileName = getNewName(files())
// if (newFileName) {
// const file: File = { name: newFileName, content: '', active: true }
// setFiles((prev) => [...prev.map(({ name, content }) => ({ name, content, active: false })), file])
// }
// }
//
// function closeTab(name: string) {
//
// }

onMount(() => {
worker = new Worker()
Expand All @@ -51,7 +87,7 @@ export default function () {
// scrolls to the bottom of the div
outputRef.scrollTop = outputRef.scrollHeight
}
workerMessage({ user_code: initialCode, warmup: true })
workerMessage(true)
})

return (
Expand All @@ -64,7 +100,7 @@ export default function () {
<div id="counter"></div>
</header>
<section>
<Editor runCode={runCode} initialCode={initialCode} />
<Editor runCode={runCode} files={files} setFiles={setFiles} />
<div class="col">
<div class="status">{status()}</div>
<div class="installed">{installed()}</div>
Expand All @@ -81,8 +117,9 @@ function escapeHTML(html: string): string {
return escapeEl.innerHTML
}

function getInitialCode(): string {
function getFiles(): File[] {
const url = new URL(window.location.href)
const base64Code = url.searchParams.get('code')
return base64Code ? atob(base64Code) : defaultPythonCode
const content = base64Code ? atob(base64Code) : defaultPythonCode
return [{ name: 'main.py', content, active: true }]
}
107 changes: 101 additions & 6 deletions src/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { createSignal, onMount, Show } from 'solid-js'
import { createSignal, onMount, Show, createEffect, Accessor, Setter } from 'solid-js'

import type * as monaco from 'monaco-editor'
import type { File } from './types'

interface EditorProps {
runCode: (user_code: string) => void
initialCode: string
runCode: (code: string) => void
files: Accessor<File[]>
setFiles: Setter<File[]>
}

export default function ({ runCode, initialCode }: EditorProps) {
export default function (props: EditorProps) {
const { runCode, files } = props
let [loading, setLoading] = createSignal(true)
let editor: monaco.editor.IStandaloneCodeEditor
let editor: monaco.editor.IStandaloneCodeEditor | null = null
let editorRef!: HTMLDivElement

function run() {
Expand All @@ -32,7 +35,7 @@ export default function ({ runCode, initialCode }: EditorProps) {
setLoading(false)

editor = monaco.editor.create(editorRef, {
value: initialCode,
value: getContent(files()),
language: 'python',
theme: 'custom-dark',
automaticLayout: true,
Expand All @@ -44,8 +47,20 @@ export default function ({ runCode, initialCode }: EditorProps) {
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, run)
})

createEffect(() => {
const newContent = getContent(files())
if (editor) {
editor.setValue(newContent)
}
})

function getActiveContent(): string {
return editor!.getValue()
}

return (
<div class="col">
<Tabs getActiveContent={getActiveContent} {...props} />
<div class="editor" ref={editorRef}>
<Show when={loading()}>
<div class="loading">loading...</div>
Expand All @@ -58,3 +73,83 @@ export default function ({ runCode, initialCode }: EditorProps) {
</div>
)
}

interface TabProps extends EditorProps {
getActiveContent: () => string
}

function Tabs({ files, setFiles, getActiveContent }: TabProps) {
function changeTab(updateContent: string, newName: string) {
setFiles((prev) =>
prev.map(({ name, content, active }) => {
if (name == newName) {
return { name, content, active: true }
} else if (active) {
return { name, content: updateContent, active: false }
} else {
return { name, content, active }
}
}),
)
}

function newTab() {
const activeContent = getActiveContent()
const newFileName = getNewName(files())
if (newFileName) {
const file: File = { name: newFileName, content: '', active: false }
setFiles((prev) => [...prev, file])
changeTab(activeContent, newFileName)
}
}

function closeTab(name: string) {
setFiles((prev) => {
if (prev.length === 1) {
return prev
}
const files = prev.filter((f) => f.name !== name)
if (!files.find((f) => f.active)) {
files[0].active = true
}
console.log(files)
return files
})
}

return (
<div class="tabs">
{files().map(({ name, active }) => (
<div class={active ? 'tab active' : 'tab'} onClick={() => changeTab(getActiveContent(), name)}>
{name}
<span class="close" onClick={() => closeTab(name)}>
</span>
</div>
))}
<div class="tab new" onClick={newTab}>
+
</div>
</div>
)
}

function getContent(files: File[]) {
const file = files.find((f) => f.active)
return file ? file.content : ''
}

function getNewName(files: File[]): string | null {
let defaultName: string = 'new.py'
let num = 1
while (files.find((f) => f.name === defaultName)) {
defaultName = `new-${num}.py`
num++
}

let name = prompt('File name?', defaultName)
while (name !== null && files.find((f) => f.name === name)) {
name = prompt(`File name ${name} already exists. Try another name?`, defaultName)
}
return name
}
46 changes: 32 additions & 14 deletions src/main.py → src/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import sys
import traceback
from pathlib import Path
from typing import Any
from typing import Any, TypedDict
import importlib.util

import micropip # noqa
Expand All @@ -25,10 +25,26 @@
_logfire_configured = False


async def install_deps(source_code: str) -> str | None:
dependencies = _find_pep723_dependencies(source_code)
class File(TypedDict):
name: str
content: str
active: bool


async def install_deps(files: list[File]) -> str | None:
cwd = Path.cwd()
for file in cwd.iterdir():
if file.name != 'run.py' and file.is_file():
file.unlink()
for file in files:
(cwd / file['name']).write_text(file['content'])

dependencies: set[str] = set()
active = next((file for file in files if file['active']), None)
if active:
dependencies = _find_pep723_dependencies(active['content'])
if dependencies is None:
dependencies = await _find_import_dependencies(source_code)
dependencies = await _find_import_dependencies(files)
new_dependencies = dependencies - _already_installed
if new_dependencies:
await micropip.install(new_dependencies)
Expand All @@ -40,10 +56,9 @@ async def install_deps(source_code: str) -> str | None:
return json.dumps(list(_already_installed))


def run_code(user_code: str) -> None:
def run_code(file: str) -> None:
try:
file_path = Path('user_code.py')
file_path.write_text(user_code)
file_path = Path(file)
spec = importlib.util.spec_from_file_location('__main__', file_path)
module = importlib.util.module_from_spec(spec)
# sys.modules['__main__'] = module
Expand Down Expand Up @@ -137,14 +152,17 @@ def _read_pep723_metadata(script: str) -> dict[str, Any]:
return {}


async def _find_import_dependencies(source_code: str) -> set[str]:
async def _find_import_dependencies(files: list[File]) -> set[str]:
"""Find dependencies in imports."""
try:
imports: list[str] = find_imports(source_code)
except SyntaxError:
return set()
else:
return _find_imports_to_install(imports)
deps: set[str] = set()
for file in files:
try:
imports: list[str] = find_imports(file['content'])
except SyntaxError:
pass
else:
deps.update(_find_imports_to_install(imports))
return deps


def _find_imports_to_install(imports: list[str]) -> set[str]:
Expand Down
42 changes: 40 additions & 2 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ aside {
}
div.col {
flex: 1;
padding: 10px 5px;
padding: 5px 5px 10px;
overflow-y: scroll;
overflow-x: hidden;
border: 1px solid #aaa;
Expand Down Expand Up @@ -75,7 +75,7 @@ button:hover {
}
.status {
color: #2490b5;
margin-bottom: 5px;
margin: 5px 0;
}
.installed {
color: #777;
Expand All @@ -86,3 +86,41 @@ button:hover {
.output {
overflow: auto;
}
.tabs {
display: flex;
margin: 0;
padding: 0;
flex-wrap: wrap;
border-bottom: 1px solid #34364f;
}
.tab {
padding: 6px 8px 6px 15px;
cursor: pointer;
color: white;
border-top: 1px solid #34364f;
border-right: 1px solid #34364f;
}
.tab:hover {
background: #27293c;
}
.active {
background: #34364f !important;
}
.new {
padding: 8px 15px;
font-size: 1.4rem;
border: 1px solid transparent;
}
.new:hover {
border: 1px solid #705326;
}
.close {
display: inline-block;
margin-left: 4px;
padding: 2px 6px;
font-size: 1.2rem;
border: 1px solid transparent;
}
.close:hover {
border: 1px solid #705326;
}
Loading

0 comments on commit 49d8f9d

Please sign in to comment.