port some to svelte5

pull/12817/head
Ottomated 2 years ago
parent 9ba370f305
commit 4ff7f8cada

@ -3,43 +3,34 @@
</script>
<script>
/** @import { Lang } from './types' */
import { historyField } from '@codemirror/commands';
import { EditorState, Range, StateEffect, StateEffectType, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
import { codemirror, withCodemirrorInstance } from '@neocodemirror/svelte';
import { svelteLanguage } from '@replit/codemirror-lang-svelte';
import { javascriptLanguage } from '@codemirror/lang-javascript';
import { createEventDispatcher, tick } from 'svelte';
import { tick } from 'svelte';
import { writable } from 'svelte/store';
import { get_repl_context } from '$lib/context.js';
import Message from './Message.svelte';
import { svelteTheme } from './theme.js';
import { autocomplete } from './autocomplete.js';
/** @type {import('@codemirror/lint').LintSource | undefined} */
export let diagnostics = undefined;
/** @type {{ diagnostics: import('@codemirror/lint').LintSource | undefined, readonly?: boolean, tab?: boolean, onchange: (value: string) => void }} */
const { diagnostics = undefined, readonly = false, tab = true, onchange } = $props();
export let readonly = false;
export let tab = true;
let code = $state('');
/** @type {ReturnType<typeof createEventDispatcher<{ change: { value: string } }>>} */
const dispatch = createEventDispatcher();
/** @type {Lang} */
let lang = $state('svelte');
let code = '';
/** @type {import('./types').Lang} */
let lang = 'svelte';
/**
* @param {{ code: string; lang: import('./types').Lang }} options
*/
/** @param {{ code: string; lang: Lang }} options */
export async function set(options) {
update(options);
}
/**
* @param {{ code?: string; lang?: import('./types').Lang }} options
*/
/** @param {{ code?: string; lang?: Lang }} options */
export async function update(options) {
await isReady;
@ -64,16 +55,14 @@
}
}
/**
* @param {number} pos
*/
/** @param {number} pos */
export function setCursor(pos) {
cursor_pos = pos;
}
/** @type {(...val: any) => void} */
let fulfil_module_editor_ready;
export const isReady = new Promise((f) => (fulfil_module_editor_ready = f));
let fulfill_module_editor_ready;
export const isReady = new Promise((f) => (fulfill_module_editor_ready = f));
export function resize() {
$cmInstance.view?.requestMeasure();
@ -87,9 +76,7 @@
return $cmInstance.view?.state.toJSON({ history: historyField });
}
/**
* @param {any} state
*/
/** @param {any} state */
export function setEditorState(state) {
if (!$cmInstance.view) return;
@ -174,18 +161,24 @@
/** @type {import('@codemirror/state').Extension[]} */
let extensions = [];
let cursor_pos = 0;
let cursor_pos = $state(0);
$: if ($cmInstance.view) {
fulfil_module_editor_ready();
}
$effect(() => {
if ($cmInstance.view) {
fulfill_module_editor_ready();
}
});
$: if ($cmInstance.view && w && h) resize();
$effect(() => {
if ($cmInstance.view && w && h) resize();
});
$: if (marked) {
unmarkText();
marked = false;
}
$effect(() => {
if (marked) {
unmarkText();
marked = false;
}
});
const watcher = EditorView.updateListener.of((viewUpdate) => {
if (viewUpdate.selectionSet) {
@ -206,6 +199,7 @@
});
</script>
<!-- svelte-ignore attribute_illegal_colon - codemirror's custom events contain colons -->
<div
class="codemirror-container"
use:codemirror={{
@ -230,9 +224,9 @@
extensions: [svelte_rune_completions, js_rune_completions, watcher],
instanceStore: cmInstance
}}
on:codemirror:textChange={({ detail: value }) => {
code = value;
dispatch('change', { value: code });
oncodemirror:textChange={(/** @type {{ detail: string; }} */ event) => {
code = event.detail;
onchange?.(code);
}}
>
{#if !$cmInstance.view}

@ -1,21 +1,15 @@
<script>
/** @import { File } from '$lib/types' */
import { get_repl_context } from '$lib/context.js';
import { get_full_filename } from '$lib/utils.js';
import { createEventDispatcher, tick } from 'svelte';
import { tick } from 'svelte';
import RunesInfo from './RunesInfo.svelte';
import Migrate from './Migrate.svelte';
/** @type {boolean} */
export let show_modified;
/** @typedef {{ files: File[]; diff: File }} ChangeEvent */
/** @type {boolean} */
export let runes;
/** @type {ReturnType<typeof createEventDispatcher<{
* remove: { files: import('$lib/types').File[]; diff: import('$lib/types').File },
* add: { files: import('$lib/types').File[]; diff: import('$lib/types').File },
* }>>} */
const dispatch = createEventDispatcher();
/** @type {{ show_modified: boolean; runes: boolean, onremove?: (ev: ChangeEvent) => void, onadd?: (ev: ChangeEvent) => void }} */
const { show_modified, runes, onremove, onadd } = $props();
const {
files,
@ -28,9 +22,9 @@
} = get_repl_context();
/** @type {string | null} */
let editing_name = null;
let editing_name = $state(null);
let input_value = '';
let input_value = $state('');
/** @param {string} filename */
function select_file(filename) {
@ -40,7 +34,7 @@
}
}
/** @param {import('$lib/types').File} file */
/** @param {File} file */
function edit_tab(file) {
if ($selected_name === get_full_filename(file)) {
editing_name = get_full_filename(file);
@ -125,7 +119,7 @@
$files = $files.filter((file) => get_full_filename(file) !== filename);
dispatch('remove', { files: $files, diff: file });
onremove?.({ files: $files, diff: file });
EDITOR_STATE_MAP.delete(get_full_filename(file));
@ -160,12 +154,12 @@
rebundle();
dispatch('add', { files: $files, diff: file });
onadd?.({ files: $files, diff: file });
$files = $files;
}
/** @param {import('$lib/types').File} editing */
/** @param {File} editing */
function is_file_name_used(editing) {
return $files.find(
(file) => JSON.stringify(file) !== JSON.stringify($selected) && file.name === editing.name
@ -177,7 +171,7 @@
let from = null;
/** @type {string | null} */
let over = null;
let over = $state(null);
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragStart(event) {
@ -190,10 +184,13 @@
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragOver(event) {
event.preventDefault();
over = event.currentTarget.id;
}
function dragEnd() {
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragEnd(event) {
event.preventDefault();
if (from && over) {
const from_index = $files.findIndex((file) => file.name === from);
const to_index = $files.findIndex((file) => file.name === over);
@ -210,8 +207,8 @@
</script>
<div class="component-selector">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="file-tabs" on:dblclick={add_new}>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="file-tabs" ondblclick={add_new}>
{#each $files as file, index (file.name)}
{@const filename = get_full_filename(file)}
<div
@ -222,14 +219,14 @@
class:active={filename === $selected_name}
class:draggable={filename !== editing_name && index !== 0}
class:drag-over={over === file.name}
on:click={() => select_file(filename)}
on:keyup={(e) => e.key === ' ' && select_file(filename)}
on:dblclick|stopPropagation={() => {}}
onclick={() => select_file(filename)}
onkeyup={(e) => e.key === ' ' && select_file(filename)}
ondblclick={(ev) => ev.stopPropagation()}
draggable={filename !== editing_name}
on:dragstart={dragStart}
on:dragover|preventDefault={dragOver}
on:dragleave={dragLeave}
on:drop|preventDefault={dragEnd}
ondragstart={dragStart}
ondragover={dragOver}
ondragleave={dragLeave}
ondrop={dragEnd}
>
<i class="drag-handle"></i>
{#if file.name === 'App' && filename !== editing_name}
@ -244,14 +241,14 @@
{input_value + (/\./.test(input_value) ? '' : `.${editing_file.type}`)}
</span>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus
spellcheck={false}
bind:value={input_value}
on:focus={select_input}
on:blur={close_edit}
on:keydown={(e) => {
onfocus={select_input}
onblur={close_edit}
onkeydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (!is_file_name_used(editing_file)) {
@ -263,21 +260,21 @@
/>
{/if}
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="editable"
title="edit component name"
on:click={() => edit_tab(file)}
on:keyup={(e) => e.key === ' ' && edit_tab(file)}
onclick={() => edit_tab(file)}
onkeyup={(e) => e.key === ' ' && edit_tab(file)}
>
{file.name}.{file.type}{#if show_modified && file.modified}*{/if}
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="remove"
on:click={() => remove(filename)}
on:keyup={(e) => e.key === ' ' && remove(filename)}
onclick={() => remove(filename)}
onkeyup={(e) => e.key === ' ' && remove(filename)}
>
<svg width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="18" y1="6" x2="6" y2="18" />
@ -289,7 +286,7 @@
{/each}
</div>
<button class="add-new" on:click={add_new} title="add new component">
<button class="add-new" onclick={add_new} title="add new component">
<svg width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="12" y1="5" x2="12" y2="19" />
<line stroke="#999" x1="5" y1="12" x2="19" y2="12" />

@ -5,7 +5,7 @@
</script>
<div class="container">
<button on:click={migrate} title="Migrate this component towards the new syntax">migrate</button>
<button onclick={migrate} title="Migrate this component towards the new syntax">migrate</button>
</div>
<style>

@ -2,14 +2,8 @@
import { get_repl_context } from '$lib/context.js';
import CodeMirror from '../CodeMirror.svelte';
/** @type {boolean} */
export let autocomplete;
/** @type {any} */ // TODO
export let error;
/** @type {any[]} */ // TODO
export let warnings;
/** @type {{ error: any; warnings: any[] }} */
const { error, warnings } = $props();
export function focus() {
$module_editor?.focus();
@ -62,7 +56,7 @@
return [];
}}
on:change={handle_change}
onchange={handle_change}
/>
</div>
</div>

@ -1,22 +1,22 @@
<script>
import { get_repl_context } from '$lib/context.js';
/** @type {boolean} */
export let runes;
/** @type {{ runes: boolean }} */
const { runes } = $props();
let open = false;
let open = $state(false);
const { selected_name } = get_repl_context();
</script>
<svelte:window
on:keydown={(e) => {
onkeydown={(e) => {
if (e.key === 'Escape') open = false;
}}
/>
<div class="container">
<button class:active={runes} class:open on:click={() => (open = !open)}>
<button class:active={runes} class:open onclick={() => (open = !open)}>
<svg viewBox="0 0 24 24">
<path d="M9.4,1H19l-5.9,7.7h8L8.3,23L11,12.6H3.5L9.4,1z" />
</svg>
@ -25,9 +25,10 @@
</button>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions
(This is taken care of by the <svelte:window> above) -->
<div class="modal-backdrop" on:click={() => (open = false)}></div>
<!-- a11y is handled by the <svelte:window> above -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="modal-backdrop" onclick={() => (open = false)}></div>
<div class="popup">
{#if $selected_name.endsWith('.svelte.js')}
<p>

@ -1,10 +1,10 @@
<script>
export let checked = false;
/** @type {{ checked: boolean }} */
let { checked = $bindable(false) } = $props();
import Checkbox from './Checkbox.svelte';
</script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="input-output-toggle">
<span class:active={!checked} style="text-align: right">input</span>
<span style="display:grid; place-items: center">

@ -2,16 +2,14 @@
import { slide } from 'svelte/transition';
import { get_repl_context } from './context.js';
/** @type {'info' | 'warning' | 'error'} */
export let kind = 'info';
/** @type {import('./types').MessageDetails | undefined} */
export let details = undefined;
/** @type {string | undefined} */
export let filename = undefined;
export let truncate = false;
/** @type {{
* kind: 'info' | 'warning' | 'error',
* details?: import('./types').MessageDetails | undefined,
* filename?: string | undefined,
* truncate?: boolean,
* children: import('svelte').Snippet,
* }} */
const { kind = 'info', details = undefined, filename = undefined, truncate = false, children } = $props();
const { go_to_warning_pos } = get_repl_context();
@ -33,13 +31,13 @@
{#if details}
<button
class:navigable={details.filename}
on:click={() => go_to_warning_pos(details)}
on:keyup={(e) => e.key === ' ' && go_to_warning_pos(details)}
onclick={() => go_to_warning_pos(details)}
onkeyup={(e) => e.key === ' ' && go_to_warning_pos(details)}
>
{message(details)}
</button>
{:else}
<slot />
{@render children()}
{/if}
</div>

@ -1,9 +1,9 @@
<script>
/** @import { File, ReplContext } from './types' */
import { EditorState } from '@codemirror/state';
import { SplitPane } from '@rich_harris/svelte-split-pane';
import { BROWSER } from 'esm-env';
import { createEventDispatcher } from 'svelte';
import { derived, writable } from 'svelte/store';
import { derived as derivedStore, writable } from 'svelte/store';
import Bundler from './Bundler.js';
import ComponentSelector from './Input/ComponentSelector.svelte';
import ModuleEditor from './Input/ModuleEditor.svelte';
@ -13,23 +13,44 @@
import { get_full_filename } from './utils.js';
import Compiler from './Output/Compiler.js';
export let packagesUrl = 'https://unpkg.com';
export let svelteUrl = `${BROWSER ? location.origin : ''}/svelte`;
export let embedded = false;
/** @type {'columns' | 'rows'} */
export let orientation = 'columns';
export let relaxed = false;
export let fixed = false;
export let fixedPos = 50;
export let injectedJS = '';
export let injectedCSS = '';
/** @type {'light' | 'dark'} */
export let previewTheme = 'light';
export let showModified = false;
export let showAst = false;
export let autocomplete = true;
let runes = false;
/** @type {{
* packagesUrl?: string;
* svelteUrl?: string;
* embedded?: boolean;
* orientation?: 'columns' | 'rows';
* relaxed?: boolean;
* fixed?: boolean;
* fixedPos?: number;
* injectedJS?: string;
* injectedCSS?: string;
* previewTheme?: 'light' | 'dark';
* showModified?: boolean;
* showAst?: boolean;
* autocomplete?: boolean;
* onchange?: (files: File[]) => void;
* onadd?: (event: { files: File[]; diff: File }) => void;
* onremove?: (event: { files: File[]; diff: File }) => void;
* }} */
let {
packagesUrl = 'https://unpkg.com',
svelteUrl = `${BROWSER ? location.origin : ''}/svelte`,
embedded = false,
orientation = 'columns',
relaxed = false,
fixed = false,
fixedPos = 50,
injectedJS = '',
injectedCSS = '',
previewTheme = 'light',
showModified = false,
showAst = false,
autocomplete = true,
onchange,
onadd,
onremove
} = $props();
let runes = $state(false);
export function toJSON() {
return {
@ -39,7 +60,7 @@
}
/**
* @param {{ files: import('./types').File[], css?: string }} data
* @param {{ files: File[], css?: string }} data
*/
export async function set(data) {
$files = data.files;
@ -60,20 +81,13 @@
// after having loaded the files externally
populate_editor_state();
dispatch('change', { files: $files });
onchange?.($files);
}
export function markSaved() {
$files = $files.map((val) => ({ ...val, modified: false }));
}
/** @type {ReturnType<typeof createEventDispatcher<{ change: { files: import('./types').File[] } }>>} */
const dispatch = createEventDispatcher();
/**
* @typedef {import('./types').ReplContext} ReplContext
*/
/** @type {import('svelte/compiler').CompileOptions} */
const DEFAULT_COMPILE_OPTIONS = {
generate: 'client',
@ -92,7 +106,7 @@
const selected_name = writable('App.svelte');
/** @type {ReplContext['selected']} */
const selected = derived([files, selected_name], ([$files, $selected_name]) => {
const selected = derivedStore([files, selected_name], ([$files, $selected_name]) => {
return (
$files.find((val) => get_full_filename(val) === $selected_name) ?? {
name: '',
@ -154,7 +168,7 @@
$bundling = new Promise((resolve) => {
resolver = resolve;
});
const result = await $bundler?.bundle($files);
const result = await $bundler?.bundle($state.snapshot($files));
if (result && token === current_token) $bundle = result;
resolver();
}
@ -162,7 +176,7 @@
async function migrate() {
if (!compiler || $selected?.type !== 'svelte') return;
const result = await compiler.migrate($selected);
const result = await compiler.migrate($state.snapshot($selected));
if (result.error) {
// TODO show somehow
return;
@ -182,9 +196,7 @@
let is_select_changing = false;
/**
* @param {string} filename
*/
/** @param {string} filename */
async function handle_select(filename) {
is_select_changing = true;
@ -203,16 +215,14 @@
is_select_changing = false;
}
/**
* @param {CustomEvent<{ value: string }>} event
*/
async function handle_change(event) {
/** @param {string} value */
async function handle_change(value) {
if (is_select_changing) return;
files.update(($files) => {
const file = { ...$selected };
file.source = event.detail.value;
file.source = value;
file.modified = true;
const idx = $files.findIndex((val) => get_full_filename(val) === $selected_name);
@ -227,9 +237,7 @@
EDITOR_STATE_MAP.set(get_full_filename($selected), $module_editor?.getEditorState());
dispatch('change', {
files: $files
});
onchange?.($files);
rebundle();
}
@ -268,35 +276,42 @@
const compiler = BROWSER ? new Compiler(svelteUrl) : null;
/** @type {import('./workers/workers').CompileMessageData | null} */
let compiled = null;
let compiled = $state(null);
$inspect(compiled);
/**
* @param {import('./types').File | null} $selected
* @param {File | null} $selected
* @param {import('svelte/compiler').CompileOptions} $compile_options
*/
async function recompile($selected, $compile_options) {
if (!compiler || !$selected) return;
if ($selected.type === 'svelte' || $selected.type === 'js') {
compiled = await compiler.compile($selected, $compile_options, true);
compiled = await compiler.compile($state.snapshot($selected), $compile_options, true);
runes = compiled.result.metadata?.runes ?? false;
} else {
runes = false;
}
}
$: recompile($selected, $compile_options);
$effect(() => {
recompile($selected, $compile_options);
});
$: mobile = width < 540;
let width = $state(0);
$: $toggleable = mobile && orientation === 'columns';
const mobile = $derived(width < 540);
let width = 0;
let show_output = false;
$effect(() => {
toggleable.set(mobile && orientation === 'columns');
});
let show_output = $state(false);
/** @type {string | null} */
let status = null;
let status_visible = false;
let status = $state(null);
let status_visible = $state(false);
/** @type {NodeJS.Timeout | undefined} */
let status_timeout = undefined;
@ -336,7 +351,7 @@
}
</script>
<svelte:window on:beforeunload={before_unload} />
<svelte:window onbeforeunload={before_unload} />
<div class="container" class:toggleable={$toggleable} bind:clientWidth={width}>
<div class="viewport" class:output={show_output}>
@ -349,12 +364,8 @@
max="-4.1rem"
>
<section slot="a">
<ComponentSelector show_modified={showModified} {runes} on:add on:remove />
<ModuleEditor
{autocomplete}
error={compiled?.result.error}
warnings={compiled?.result.warnings ?? []}
/>
<ComponentSelector show_modified={showModified} {runes} {onadd} {onremove} />
<ModuleEditor error={compiled?.result.error} warnings={compiled?.result.warnings ?? []} />
</section>
<section slot="b" style="height: 100%;">

@ -134,10 +134,11 @@ const options = runes.map(({ snippet, test }, i) => ({
/**
* @param {import('@codemirror/autocomplete').CompletionContext} context
* @param {import('./types.js').File} selected
* @param {import('./types.js').File | null} selected
* @param {import('./types.js').File[]} files
*/
export function autocomplete(context, selected, files) {
if (!selected) return false;
let node = syntaxTree(context.state).resolveInner(context.pos, -1);
if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') {

@ -2,12 +2,12 @@ import { getContext, setContext } from 'svelte';
const key = Symbol('repl');
/** @returns {import("./types").ReplContext} */
/** @returns {import('./types').ReplContext} */
export function get_repl_context() {
return getContext(key);
}
/** @param {import("./types").ReplContext} value */
/** @param {import('./types').ReplContext} value */
export function set_repl_context(value) {
setContext(key, value);
}

@ -67,11 +67,7 @@ export type ReplContext = {
rebundle(): Promise<void>;
migrate(): Promise<void>;
handle_select(filename: string): Promise<void>;
handle_change(
event: CustomEvent<{
value: string;
}>
): Promise<void>;
handle_change(value: string): Promise<void>;
go_to_warning_pos(item?: MessageDetails): Promise<void>;
clear_state(): void;
};

@ -19,7 +19,7 @@ const ready = new Promise((f) => {
self.addEventListener(
'message',
/** @param {MessageEvent<import("../workers").CompileMessageData>} event */
/** @param {MessageEvent<import('../workers').CompileMessageData>} event */
async (event) => {
switch (event.data.type) {
case 'init':
@ -55,7 +55,7 @@ const common_options = {
css: false
};
/** @param {import("../workers").CompileMessageData} param0 */
/** @param {import('../workers').CompileMessageData} param0 */
function compile({ id, source, options, return_ast }) {
try {
const css = `/* Select a component to see compiled CSS */`;
@ -132,7 +132,7 @@ function compile({ id, source, options, return_ast }) {
}
}
/** @param {import("../workers").MigrateMessageData} param0 */
/** @param {import('../workers').MigrateMessageData} param0 */
function migrate({ id, source }) {
try {
const result = svelte.migrate(source);

@ -1,4 +1,4 @@
import type { CompileOptions, File } from '../types';
import type { CompileOptions, File, Warning } from '../types';
export type CompileMessageData = {
id: number;
@ -15,6 +15,11 @@ export type CompileMessageData = {
metadata?: {
runes: boolean;
};
error: {
message: string;
position: [number, number];
} | null;
warnings: Warning[];
};
};

Loading…
Cancel
Save