Skip to content
Open
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
48 changes: 47 additions & 1 deletion src/lib/MarkdownViewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,39 @@ import { t } from './utils/i18n.js';
});
}

// window.prompt() is a silent no-op inside the webview (wry does not
// implement the native JS dialogs), so text input goes through our own
// modal. Resolves with the entered string, or null on cancel.
let promptModal = $state<{
show: boolean;
title: string;
message: string;
value: string;
resolve: ((v: string | null) => void) | null;
}>({ show: false, title: '', message: '', value: '', resolve: null });

function promptCustom(message: string, options: { title: string; initial?: string }): Promise<string | null> {
return new Promise((resolve) => {
promptModal = {
show: true,
title: options.title,
message,
value: options.initial ?? '',
resolve,
};
});
}

function handlePromptConfirm() {
if (promptModal.resolve) promptModal.resolve(promptModal.value);
promptModal.show = false;
}

function handlePromptCancel() {
if (promptModal.resolve) promptModal.resolve(null);
promptModal.show = false;
}

function handleModalSave() {
if (modalState.resolve) modalState.resolve('save');
modalState.show = false;
Expand Down Expand Up @@ -2724,7 +2757,10 @@ import { t } from './utils/i18n.js';
const tab = tabManager.tabs.find((t) => t.id === tabId);
if (!tab || !tab.path) return;

const newName = window.prompt(t('menu.renameFile', settings.language), tab.title);
const newName = await promptCustom(t('menu.renameFile', settings.language), {
title: t('menu.rename', settings.language),
initial: tab.title,
});
if (newName && newName !== tab.title) {
const oldPath = tab.path;
const newPath = oldPath.replace(/[/\\][^/\\]+$/, (m) => m.charAt(0) + newName);
Expand Down Expand Up @@ -3387,6 +3423,16 @@ import { t } from './utils/i18n.js';
onsave={handleModalSave}
oncancel={handleModalCancel} />

<Modal
show={promptModal.show}
title={promptModal.title}
message={promptModal.message}
kind="info"
showInput={true}
bind:inputValue={promptModal.value}
onconfirm={handlePromptConfirm}
oncancel={handlePromptCancel} />

<UpdateDialog />

<div class="toast-container">
Expand Down
38 changes: 36 additions & 2 deletions src/lib/components/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
message,
kind = 'info',
showSave = false,
showInput = false,
inputValue = $bindable(''),
onconfirm,
onsave,
oncancel,
Expand All @@ -18,18 +20,26 @@
message: string;
kind?: 'info' | 'warning' | 'error';
showSave?: boolean;
showInput?: boolean;
inputValue?: string;
onconfirm: () => void;
onsave?: () => void;
oncancel: () => void;
}>();

let modalContent = $state<HTMLDivElement>();
let inputElement = $state<HTMLInputElement>();
let previousActiveElement: HTMLElement | null = null;

$effect(() => {
if (show) {
previousActiveElement = document.activeElement as HTMLElement;
setTimeout(() => {
if (showInput && inputElement) {
inputElement.focus();
inputElement.select();
return;
}
const focusable = modalContent?.querySelector('button.primary') as HTMLElement;
if (focusable) {
focusable.focus();
Expand All @@ -43,6 +53,9 @@
});

function handleKeydown(e: KeyboardEvent) {
// While the user is typing in the input, plain letters are text —
// the y/n confirm/cancel shortcuts must not fire.
const typingInInput = showInput && e.target === inputElement;
if (e.key === 'Escape') {
e.preventDefault();
oncancel();
Expand All @@ -56,12 +69,12 @@
}
}
// Y for Yes/Confirm
if (e.key.toLowerCase() === 'y' && !e.ctrlKey && !e.altKey && !e.metaKey) {
if (!typingInInput && e.key.toLowerCase() === 'y' && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
onconfirm();
}
// N for No/Cancel
if (e.key.toLowerCase() === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) {
if (!typingInInput && e.key.toLowerCase() === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
oncancel();
}
Expand Down Expand Up @@ -112,6 +125,9 @@
</div>
<div class="modal-body">
<p>{message}</p>
{#if showInput}
<input class="modal-input" type="text" bind:this={inputElement} bind:value={inputValue} spellcheck="false" />
{/if}
</div>
<div class="modal-footer">
<button class="modal-btn secondary" onclick={oncancel}>{t('settings.cancel', settings.language)}</button>
Expand Down Expand Up @@ -174,6 +190,24 @@
color: var(--color-fg-muted);
}

.modal-input {
margin-top: 12px;
width: 100%;
box-sizing: border-box;
padding: 6px 10px;
font-size: 14px;
font-family: inherit;
color: var(--color-fg-default);
background: var(--color-canvas-default);
border: 1px solid var(--color-border-default);
border-radius: 6px;
outline: none;
}

.modal-input:focus {
border-color: var(--color-accent-fg);
}

.modal-footer {
padding: 16px 24px;
background: var(--color-canvas-subtle);
Expand Down
8 changes: 7 additions & 1 deletion src/lib/components/Tab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@
items: [
{ label: t('menu.newFile', currentLang), shortcut: 'Ctrl+T', onClick: () => emit('menu-tab-new') },
{ label: t('menu.undoCloseTab', currentLang), shortcut: 'Ctrl+Shift+T', onClick: () => emit('menu-tab-undo') },
{ label: t('menu.rename', currentLang), onClick: () => emit('menu-tab-rename', tab.id) },
{
label: t('menu.rename', currentLang),
// Rename renames the file on disk; untitled/HOME tabs have
// no file, and the handler previously no-oped silently.
disabled: !hasRealFilePath(tab.path),
onClick: () => emit('menu-tab-rename', tab.id),
},
{ separator: true },
...fileActionItems,
{ separator: true },
Expand Down
Loading