feat: add dirty-dismiss and footer hint settings
This commit is contained in:
+52
-7
@@ -30,6 +30,11 @@ import { isEditingView } from "../state/view.ts";
|
||||
import type { AskParams, AskResult, AskState } from "../types.ts";
|
||||
import { maybeAutoSubmitState } from "./auto-submit.ts";
|
||||
import { createAskAutocompleteProvider } from "./autocomplete.ts";
|
||||
import {
|
||||
DIRTY_DISMISS_NOTICE,
|
||||
shouldConfirmDirtyDismiss,
|
||||
shouldDiscardAfterConfirmation,
|
||||
} from "./dismiss-guard.ts";
|
||||
import type { AskInputCommand } from "./input.ts";
|
||||
import { getInputCommand } from "./input.ts";
|
||||
import { renderAskScreen } from "./render.ts";
|
||||
@@ -49,6 +54,7 @@ type AskFlowParams = AskParams &
|
||||
interface AskFlowController {
|
||||
config: AskConfig;
|
||||
ctx: ExtensionContext;
|
||||
dismissNotice?: string;
|
||||
done: Done;
|
||||
editor: Editor;
|
||||
settingsOpen: boolean;
|
||||
@@ -82,6 +88,7 @@ function createAskFlowController(
|
||||
const controller: AskFlowController = {
|
||||
config: params.config,
|
||||
ctx: params.ctx,
|
||||
dismissNotice: undefined,
|
||||
done,
|
||||
editor: createEditor(tui, theme, params.cwd),
|
||||
settingsOpen: false,
|
||||
@@ -128,6 +135,7 @@ function renderController(
|
||||
return renderAskScreen({
|
||||
config: controller.config,
|
||||
editor: controller.editor,
|
||||
footerNotice: getDismissNotice(controller),
|
||||
state: controller.state,
|
||||
theme: controller.theme,
|
||||
width,
|
||||
@@ -154,7 +162,7 @@ function handleEditingCommand(
|
||||
data: string
|
||||
) {
|
||||
if (command.kind === "dismiss") {
|
||||
commitState(controller, dismissFlow(controller.state), { finish: true });
|
||||
handleExitFlow(controller, dismissFlow(controller.state));
|
||||
return;
|
||||
}
|
||||
if (command.kind === "showSettings") {
|
||||
@@ -205,7 +213,7 @@ function handleNavigationCommand(
|
||||
});
|
||||
return;
|
||||
case "cancel":
|
||||
commitState(controller, cancelFlow(controller.state), { finish: true });
|
||||
handleExitFlow(controller, cancelFlow(controller.state));
|
||||
return;
|
||||
case "numberShortcut":
|
||||
commitState(
|
||||
@@ -214,7 +222,7 @@ function handleNavigationCommand(
|
||||
);
|
||||
return;
|
||||
case "dismiss":
|
||||
commitState(controller, dismissFlow(controller.state), { finish: true });
|
||||
handleExitFlow(controller, dismissFlow(controller.state));
|
||||
return;
|
||||
case "showSettings":
|
||||
showSettingsModal(controller);
|
||||
@@ -274,6 +282,9 @@ function commitState(
|
||||
nextState: AskState,
|
||||
options: { finish?: boolean; syncSelection?: boolean } = {}
|
||||
) {
|
||||
if (nextState.activeTabIndex !== controller.state.activeTabIndex) {
|
||||
clearDismissNotice(controller);
|
||||
}
|
||||
controller.suppressAutoInputForSelection = false;
|
||||
controller.state = nextState;
|
||||
if (options.syncSelection !== false) {
|
||||
@@ -289,10 +300,11 @@ function commitState(
|
||||
|
||||
function submitEditor(controller: AskFlowController, value: string) {
|
||||
controller.suppressAutoInputForSelection = false;
|
||||
controller.state = maybeAutoSubmitState(
|
||||
submitEditorDraft(controller.state, value),
|
||||
controller.config
|
||||
);
|
||||
const nextState = submitEditorDraft(controller.state, value);
|
||||
if (nextState.activeTabIndex !== controller.state.activeTabIndex) {
|
||||
clearDismissNotice(controller);
|
||||
}
|
||||
controller.state = maybeAutoSubmitState(nextState, controller.config);
|
||||
controller.editor.setText("");
|
||||
refresh(controller);
|
||||
maybeFinish(controller);
|
||||
@@ -313,6 +325,39 @@ function closeEditor(controller: AskFlowController) {
|
||||
refresh(controller);
|
||||
}
|
||||
|
||||
function handleExitFlow(controller: AskFlowController, nextState: AskState) {
|
||||
if (!shouldRequestDismissConfirmation(controller)) {
|
||||
commitState(controller, nextState, { finish: true });
|
||||
return;
|
||||
}
|
||||
if (shouldDiscardAfterConfirmation(!!controller.dismissNotice)) {
|
||||
commitState(controller, nextState, { finish: true });
|
||||
return;
|
||||
}
|
||||
controller.dismissNotice = DIRTY_DISMISS_NOTICE;
|
||||
refresh(controller);
|
||||
}
|
||||
|
||||
function shouldRequestDismissConfirmation(
|
||||
controller: AskFlowController
|
||||
): boolean {
|
||||
return shouldConfirmDirtyDismiss({
|
||||
config: controller.config,
|
||||
state: controller.state,
|
||||
editingText: isEditingView(controller.state)
|
||||
? controller.editor.getText()
|
||||
: "",
|
||||
});
|
||||
}
|
||||
|
||||
function clearDismissNotice(controller: AskFlowController) {
|
||||
controller.dismissNotice = undefined;
|
||||
}
|
||||
|
||||
function getDismissNotice(controller: AskFlowController): string | undefined {
|
||||
return controller.dismissNotice;
|
||||
}
|
||||
|
||||
function showSettingsModal(controller: AskFlowController) {
|
||||
if (controller.settingsOpen) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { AskConfig } from "../config/schema.ts";
|
||||
import type { AskState } from "../types.ts";
|
||||
|
||||
export const DIRTY_DISMISS_NOTICE =
|
||||
"Unsaved ask answers or drafts. Press cancel/dismiss again to discard.";
|
||||
|
||||
export function shouldConfirmDirtyDismiss(args: {
|
||||
config: AskConfig;
|
||||
state: AskState;
|
||||
editingText?: string;
|
||||
}): boolean {
|
||||
if (!args.config.behaviour.confirmDismissWhenDirty) {
|
||||
return false;
|
||||
}
|
||||
return hasDirtyFlowState(args.state, args.editingText ?? "");
|
||||
}
|
||||
|
||||
export function hasDirtyFlowState(state: AskState, editingText = ""): boolean {
|
||||
return Object.keys(state.answers).length > 0 || editingText.length > 0;
|
||||
}
|
||||
|
||||
export function shouldDiscardAfterConfirmation(
|
||||
confirmationPending: boolean
|
||||
): boolean {
|
||||
return confirmationPending;
|
||||
}
|
||||
+10
-2
@@ -30,16 +30,21 @@ export function renderFrameHeader(args: {
|
||||
|
||||
export function renderFrameFooter(args: {
|
||||
config: AskConfig;
|
||||
footerNotice?: string;
|
||||
lines: string[];
|
||||
state: AskState;
|
||||
theme: Theme;
|
||||
width: number;
|
||||
}) {
|
||||
const { config, lines, state, theme, width } = args;
|
||||
const { config, footerNotice, lines, state, theme, width } = args;
|
||||
const add = (text = "") => lines.push(truncateToWidth(text, width));
|
||||
const footer = renderFooter(config, state, width);
|
||||
if (footer.length > 0) {
|
||||
const noticeLines = footerNotice ? wrapText(footerNotice, width) : [];
|
||||
if (noticeLines.length > 0 || footer.length > 0) {
|
||||
add();
|
||||
for (const line of noticeLines) {
|
||||
add(theme.fg("warning", line));
|
||||
}
|
||||
for (const line of footer) {
|
||||
add(theme.fg("dim", line));
|
||||
}
|
||||
@@ -185,6 +190,9 @@ function renderFooter(
|
||||
state: AskState,
|
||||
width: number
|
||||
): string[] {
|
||||
if (!config.behaviour.showFooterHints) {
|
||||
return [];
|
||||
}
|
||||
let footer: string;
|
||||
if (state.view.kind === "input") {
|
||||
footer = renderFooterText(config, "input");
|
||||
|
||||
+3
-2
@@ -12,12 +12,13 @@ import type { QuestionRenderContext, Theme } from "./render-types.ts";
|
||||
|
||||
export function renderAskScreen(args: {
|
||||
config: AskConfig;
|
||||
footerNotice?: string;
|
||||
state: AskState;
|
||||
theme: Theme;
|
||||
width: number;
|
||||
editor: QuestionRenderContext["editor"];
|
||||
}): string[] {
|
||||
const { config, state, theme, width, editor } = args;
|
||||
const { config, footerNotice, state, theme, width, editor } = args;
|
||||
const lines: string[] = [];
|
||||
const question = getCurrentQuestion(state);
|
||||
const options = getRenderableOptions(question);
|
||||
@@ -38,6 +39,6 @@ export function renderAskScreen(args: {
|
||||
});
|
||||
}
|
||||
|
||||
renderFrameFooter({ config, lines, state, theme, width });
|
||||
renderFrameFooter({ config, footerNotice, lines, state, theme, width });
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
requestDiscard,
|
||||
setSaving,
|
||||
shouldDiscard,
|
||||
toggleAutoSubmit,
|
||||
toggleBehaviourSetting,
|
||||
} from "./settings-state.ts";
|
||||
|
||||
interface Theme {
|
||||
@@ -58,11 +58,29 @@ const BEHAVIOUR_ROWS = [
|
||||
{
|
||||
description:
|
||||
"Auto-submit completed ask flows when no question or option notes were added.",
|
||||
key: "autoSubmitWhenAnsweredWithoutNotes",
|
||||
getValue(config: AskConfig) {
|
||||
return config.behaviour.autoSubmitWhenAnsweredWithoutNotes;
|
||||
},
|
||||
label: "Auto-submit when answered without notes",
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Require a second cancel or dismiss action before discarding answered or drafted ask content.",
|
||||
key: "confirmDismissWhenDirty",
|
||||
getValue(config: AskConfig) {
|
||||
return config.behaviour.confirmDismissWhenDirty;
|
||||
},
|
||||
label: "Confirm dismiss when dirty",
|
||||
},
|
||||
{
|
||||
description: "Show footer keymap hints at the bottom of the ask flow.",
|
||||
key: "showFooterHints",
|
||||
getValue(config: AskConfig) {
|
||||
return config.behaviour.showFooterHints;
|
||||
},
|
||||
label: "Show footer hints",
|
||||
},
|
||||
] as const;
|
||||
|
||||
type SettingsTab = "keymaps" | "behaviour";
|
||||
@@ -168,7 +186,11 @@ export class AskSettingsModal {
|
||||
return;
|
||||
}
|
||||
if (matchesAny(data, SETTINGS_MODAL_KEYS.toggle)) {
|
||||
this.state = toggleAutoSubmit(this.state);
|
||||
const row = BEHAVIOUR_ROWS[this.state.behaviourFocusIndex];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
this.state = toggleBehaviourSetting(this.state, row.key);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,15 +131,17 @@ export function shouldDiscard(state: AskSettingsState, now: number): boolean {
|
||||
return !!state.pendingCloseUntil && now <= state.pendingCloseUntil;
|
||||
}
|
||||
|
||||
export function toggleAutoSubmit(state: AskSettingsState): AskSettingsState {
|
||||
export function toggleBehaviourSetting(
|
||||
state: AskSettingsState,
|
||||
key: keyof AskConfig["behaviour"]
|
||||
): AskSettingsState {
|
||||
return {
|
||||
...state,
|
||||
draftConfig: {
|
||||
...state.draftConfig,
|
||||
behaviour: {
|
||||
...state.draftConfig.behaviour,
|
||||
autoSubmitWhenAnsweredWithoutNotes:
|
||||
!state.draftConfig.behaviour.autoSubmitWhenAnsweredWithoutNotes,
|
||||
[key]: !state.draftConfig.behaviour[key],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -149,6 +151,9 @@ function deepEqual(left: AskConfig, right: AskConfig): boolean {
|
||||
return (
|
||||
left.behaviour.autoSubmitWhenAnsweredWithoutNotes ===
|
||||
right.behaviour.autoSubmitWhenAnsweredWithoutNotes &&
|
||||
left.behaviour.confirmDismissWhenDirty ===
|
||||
right.behaviour.confirmDismissWhenDirty &&
|
||||
left.behaviour.showFooterHints === right.behaviour.showFooterHints &&
|
||||
left.keymaps.cancel === right.keymaps.cancel &&
|
||||
left.keymaps.dismiss === right.keymaps.dismiss &&
|
||||
left.keymaps.toggle === right.keymaps.toggle &&
|
||||
|
||||
Reference in New Issue
Block a user