Skip to content

Commit 1e037e2

Browse files
authored
Vim UI (#1049)
* add UI for vim mode * oops * fix * man codemirror is confusing * fix * refactor * move stuff * add UI to tutorial as well * drive-by fixes * fix
1 parent 1a008bd commit 1e037e2

File tree

15 files changed

+167
-92
lines changed

15 files changed

+167
-92
lines changed

apps/svelte.dev/src/lib/components/ModalDropdown.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@
4545
}
4646

4747
// except parents of the current one
48-
const current = details.querySelector(
49-
`[href="${page.url.pathname}"]`
50-
) as HTMLAnchorElement | null;
48+
const current = details.querySelector(`[aria-current="page"]`) as HTMLAnchorElement | null;
5149
if (!current) return;
5250

5351
let node = current as Element;

apps/svelte.dev/src/routes/(authed)/playground/[id]/AppControls.svelte

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -161,20 +161,6 @@
161161
162162
saving = false;
163163
}
164-
165-
// modifying an app should reset the `<select>`, so that
166-
// the example can be reselected
167-
$effect(() => {
168-
if (modified) {
169-
// this is a little tricky, but: we need to wrap this in untrack
170-
// because otherwise we'll read `select.value` and re-run this
171-
// when we navigate, which we don't want
172-
untrack(() => {
173-
// @ts-ignore not sure why this is erroring
174-
select.value = '';
175-
});
176-
}
177-
});
178164
</script>
179165

180166
<svelte:window on:keydown={handleKeydown} />
@@ -193,7 +179,7 @@
193179
<li>
194180
<a
195181
href="/playground/{example.slug}"
196-
aria-current={page.params.id === example.slug ? 'page' : undefined}
182+
aria-current={page.params.id === example.slug && !modified ? 'page' : undefined}
197183
>
198184
{example.title}
199185
</a>

apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@
258258
toggle={() => {
259259
workspace.set(Object.values(completed ? a : b));
260260
}}
261+
{workspace}
261262
/>
262263

263264
<div class="top" class:offset={show_editor}>

apps/svelte.dev/src/routes/tutorial/[...slug]/Controls.svelte

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
import SecondaryNav from '$lib/components/SecondaryNav.svelte';
44
import ModalDropdown from '$lib/components/ModalDropdown.svelte';
55
import type { Exercise, PartStub } from '$lib/tutorial';
6-
import { Icon } from '@sveltejs/site-kit/components';
6+
import { Checkbox, Icon, Toolbox } from '@sveltejs/site-kit/components';
7+
import type { Workspace } from 'editor';
78
89
interface Props {
910
index: PartStub[];
1011
exercise: Exercise;
1112
completed: boolean;
1213
toggle: () => void;
14+
workspace: Workspace;
1315
}
1416
15-
let { index, exercise, completed, toggle }: Props = $props();
17+
let { index, exercise, completed, toggle, workspace }: Props = $props();
1618
</script>
1719

1820
<SecondaryNav>
@@ -71,6 +73,13 @@
7173
solve
7274
{/if}
7375
</button>
76+
77+
<Toolbox>
78+
<label class="option">
79+
<span>Toggle Vim mode</span>
80+
<Checkbox bind:checked={workspace.vim}></Checkbox>
81+
</label>
82+
</Toolbox>
7483
</SecondaryNav>
7584

7685
<style>

packages/editor/src/lib/Workspace.svelte.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export class Workspace {
112112
#readonly = false; // TODO do we need workspaces for readonly stuff?
113113
#files = $state.raw<Item[]>([]);
114114
#current = $state.raw() as File;
115+
#vim = $state(false);
115116

116117
#handlers = {
117118
hover: new Set<(pos: number | null) => void>(),
@@ -279,23 +280,10 @@ export class Workspace {
279280
if (this.#view) throw new Error('view is already linked');
280281
this.#view = view;
281282

282-
view.setState(this.#get_state(untrack(() => this.#current)));
283-
284-
let should_install_vim = localStorage.getItem('vim') === 'true';
285-
286-
const q = new URLSearchParams(location.search);
287-
if (q.has('vim')) {
288-
should_install_vim = q.get('vim') === 'true';
289-
localStorage.setItem('vim', should_install_vim.toString());
290-
}
291-
292-
if (should_install_vim) {
293-
const { vim } = await import('@replit/codemirror-vim');
294-
295-
this.#view?.dispatch({
296-
effects: vim_mode.reconfigure(vim())
297-
});
298-
}
283+
untrack(() => {
284+
view.setState(this.#get_state(untrack(() => this.#current)));
285+
this.vim = localStorage.getItem('vim') === 'true';
286+
});
299287
}
300288

301289
move(from: Item, to: Item) {
@@ -476,6 +464,44 @@ export class Workspace {
476464
}
477465
}
478466

467+
get vim() {
468+
return this.#vim;
469+
}
470+
471+
set vim(value) {
472+
this.#toggle_vim(value);
473+
}
474+
475+
async #toggle_vim(value: boolean) {
476+
this.#vim = value;
477+
478+
localStorage.setItem('vim', String(value));
479+
480+
// @ts-expect-error jfc CodeMirror is a struggle
481+
let vim_extension_index = default_extensions.findIndex((ext) => ext.compartment === vim_mode);
482+
483+
let extension: any = [];
484+
485+
if (value) {
486+
const { vim } = await import('@replit/codemirror-vim');
487+
extension = vim();
488+
}
489+
490+
default_extensions[vim_extension_index] = vim_mode.of(extension);
491+
492+
this.#view?.dispatch({
493+
effects: vim_mode.reconfigure(extension)
494+
});
495+
496+
// update all the other states
497+
for (const file of this.#files) {
498+
if (file.type !== 'file') continue;
499+
if (file === this.#current) continue;
500+
501+
this.states.set(file.name, this.#create_state(file));
502+
}
503+
}
504+
479505
#create_directories(item: Item) {
480506
// create intermediate directories as necessary
481507
const parts = item.name.split('/');
@@ -497,9 +523,10 @@ export class Workspace {
497523
}
498524

499525
#get_state(file: File) {
500-
let state = this.states.get(file.name);
501-
if (state) return state;
526+
return this.states.get(file.name) ?? this.#create_state(file);
527+
}
502528

529+
#create_state(file: File) {
503530
const extensions = [
504531
...default_extensions,
505532
EditorState.readOnly.of(this.#readonly),
@@ -573,7 +600,7 @@ export class Workspace {
573600
break;
574601
}
575602

576-
state = EditorState.create({
603+
const state = EditorState.create({
577604
doc: file.contents,
578605
extensions
579606
});

packages/repl/src/lib/Input/ComponentSelector.svelte

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
<script lang="ts">
22
import RunesInfo from './RunesInfo.svelte';
3-
import Migrate from './Migrate.svelte';
43
import type { Workspace, File } from 'editor';
54
import { tick } from 'svelte';
5+
import { Checkbox, Toolbox } from '@sveltejs/site-kit/components';
66
77
interface Props {
88
runes: boolean;
99
onchange: () => void;
1010
workspace: Workspace;
1111
can_migrate: boolean;
12+
migrate: () => void;
1213
}
1314
14-
let { runes, onchange, workspace, can_migrate }: Props = $props();
15+
let { runes, onchange, workspace, can_migrate, migrate }: Props = $props();
1516
1617
let input = $state() as HTMLInputElement;
1718
let input_value = $state(workspace.current.name);
@@ -162,7 +163,14 @@
162163

163164
<div class="runes">
164165
<RunesInfo {runes} />
165-
<Migrate {can_migrate} />
166+
<Toolbox>
167+
<label class="option">
168+
<span>Toggle Vim mode</span>
169+
<Checkbox bind:checked={workspace.vim}></Checkbox>
170+
</label>
171+
172+
<button disabled={!can_migrate} onclick={migrate}>Migrate to Svelte 5, if possible</button>
173+
</Toolbox>
166174
</div>
167175
</div>
168176

packages/repl/src/lib/Input/Migrate.svelte

Lines changed: 0 additions & 28 deletions
This file was deleted.

packages/repl/src/lib/Input/RunesInfo.svelte

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,9 @@
44
55
let { runes }: { runes: boolean } = $props();
66
7-
let open = $state(false);
8-
97
const { workspace } = get_repl_context();
108
</script>
119

12-
<svelte:window
13-
onkeydown={(e) => {
14-
if (e.key === 'Escape') open = false;
15-
}}
16-
/>
17-
1810
<Dropdown align="right">
1911
<div class="target">
2012
<span class="lightning" class:active={runes} role="presentation"></span>
@@ -106,12 +98,13 @@
10698
10799
.popup {
108100
position: absolute;
109-
right: -6rem;
101+
right: -4rem;
110102
width: 100vw;
111103
max-width: 320px;
112104
z-index: 9999;
113105
background: var(--sk-bg-3);
114106
padding: 1em;
107+
border-radius: var(--sk-border-radius);
115108
116109
p {
117110
font: var(--sk-font-ui-medium);

packages/repl/src/lib/Repl.svelte

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,6 @@
8585
set_repl_context({
8686
bundle,
8787
toggleable,
88-
89-
migrate,
90-
9188
workspace
9289
});
9390
@@ -177,7 +174,7 @@
177174
>
178175
{#snippet a()}
179176
<section>
180-
<ComponentSelector {runes} {onchange} {workspace} {can_migrate} />
177+
<ComponentSelector {runes} {onchange} {workspace} {can_migrate} {migrate} />
181178

182179
<Editor {workspace} />
183180
</section>

packages/repl/src/lib/types.d.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,5 @@ export type ReplState = {
4646
export type ReplContext = {
4747
bundle: Writable<ReplState['bundle']>;
4848
toggleable: Writable<ReplState['toggleable']>;
49-
5049
workspace: Workspace;
51-
52-
// Methods
53-
migrate(): Promise<void>;
5450
};

0 commit comments

Comments
 (0)