fix: allow empty editor navigation shortcuts
This commit is contained in:
@@ -163,7 +163,8 @@ Behavior details:
|
|||||||
- on multi-select questions, submitted `values` and `labels` keep selected options first and append the free-form answer last when both exist
|
- on multi-select questions, submitted `values` and `labels` keep selected options first and append the free-form answer last when both exist
|
||||||
- unanswered questions are omitted from `details.answers`
|
- unanswered questions are omitted from `details.answers`
|
||||||
- when the free-form option is selected, it becomes an inline input row with the selected-tab background style spanning the full width
|
- when the free-form option is selected, it becomes an inline input row with the selected-tab background style spanning the full width
|
||||||
- while editing a note or free-form answer, arrow keys and `Tab` stay inside the editor so the typing cursor can move naturally
|
- while editing a note or free-form answer, arrow keys and `Tab` stay inside the editor while text is present so the typing cursor can move naturally
|
||||||
|
- when a note or free-form editor is empty, `Up`/`Down` navigate options and `Tab`/`Shift+Tab`/`Left`/`Right` navigate tabs without closing the editor first
|
||||||
- free-form answer editors support pi-style `@` file path autocomplete for quickly mentioning project files
|
- free-form answer editors support pi-style `@` file path autocomplete for quickly mentioning project files
|
||||||
- `Esc` closes the editor and returns to navigation mode
|
- `Esc` closes the editor and returns to navigation mode
|
||||||
- `Ctrl+C` dismisses the entire ask flow immediately, even from note/free-form editing, without saving the current draft
|
- `Ctrl+C` dismisses the entire ask flow immediately, even from note/free-form editing, without saving the current draft
|
||||||
|
|||||||
+3
-2
@@ -115,8 +115,9 @@ Editing flow:
|
|||||||
- `Enter`: save and submit the current input
|
- `Enter`: save and submit the current input
|
||||||
- `Esc`: save draft and close the editor
|
- `Esc`: save draft and close the editor
|
||||||
- `Ctrl+C`: dismiss the entire flow immediately without saving the current editor draft
|
- `Ctrl+C`: dismiss the entire flow immediately without saving the current editor draft
|
||||||
- arrow keys and `Tab` stay in the editor so the cursor can move while typing
|
- when editor has text, arrow keys and `Tab` stay in the editor so the cursor can move while typing
|
||||||
- navigation resumes only after closing the editor with `Esc`
|
- when editor is empty, `Up`/`Down` move options and `Tab`/`Shift+Tab`/`Left`/`Right` move between tabs without requiring `Esc` first
|
||||||
|
- navigation resumes only after closing the editor with `Esc`, unless the editor is empty and the navigation keys above are used
|
||||||
|
|
||||||
## Non-interactive mode
|
## Non-interactive mode
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,11 @@ function renderController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleControllerInput(controller: AskFlowController, data: string) {
|
function handleControllerInput(controller: AskFlowController, data: string) {
|
||||||
const command = getInputCommand(controller.state, data);
|
const command = getInputCommand(
|
||||||
|
controller.state,
|
||||||
|
data,
|
||||||
|
isEditingView(controller.state) ? controller.editor.getText() : ""
|
||||||
|
);
|
||||||
if (isEditingView(controller.state)) {
|
if (isEditingView(controller.state)) {
|
||||||
handleEditingCommand(controller, command, data);
|
handleEditingCommand(controller, command, data);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+21
-3
@@ -21,14 +21,15 @@ export type AskInputCommand =
|
|||||||
|
|
||||||
export function getInputCommand(
|
export function getInputCommand(
|
||||||
state: AskState,
|
state: AskState,
|
||||||
data: string
|
data: string,
|
||||||
|
editingText = ""
|
||||||
): AskInputCommand {
|
): AskInputCommand {
|
||||||
if (matchesKey(data, Key.ctrl("c"))) {
|
if (matchesKey(data, Key.ctrl("c"))) {
|
||||||
return { kind: "dismiss" };
|
return { kind: "dismiss" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.view.kind === "input" || state.view.kind === "note") {
|
if (state.view.kind === "input" || state.view.kind === "note") {
|
||||||
return getEditingInputCommand(data);
|
return getEditingInputCommand(data, editingText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNavigationInputCommand(data);
|
return getNavigationInputCommand(data);
|
||||||
@@ -46,10 +47,27 @@ export function formatKeybindingLabel(key: string): string {
|
|||||||
.join("+");
|
.join("+");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditingInputCommand(data: string): AskInputCommand {
|
function getEditingInputCommand(
|
||||||
|
data: string,
|
||||||
|
editingText: string
|
||||||
|
): AskInputCommand {
|
||||||
if (matchesKey(data, Key.escape)) {
|
if (matchesKey(data, Key.escape)) {
|
||||||
return { kind: "editClose" };
|
return { kind: "editClose" };
|
||||||
}
|
}
|
||||||
|
if (editingText.length === 0) {
|
||||||
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
||||||
|
return { kind: "editMoveTab", delta: 1 };
|
||||||
|
}
|
||||||
|
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
|
||||||
|
return { kind: "editMoveTab", delta: -1 };
|
||||||
|
}
|
||||||
|
if (matchesKey(data, Key.up)) {
|
||||||
|
return { kind: "editMoveOption", delta: -1 };
|
||||||
|
}
|
||||||
|
if (matchesKey(data, Key.down)) {
|
||||||
|
return { kind: "editMoveOption", delta: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
return { kind: "delegateToEditor" };
|
return { kind: "delegateToEditor" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-22
@@ -21,35 +21,59 @@ function inputState() {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
test("typing mode delegates arrow keys to the editor", () => {
|
test("empty typing mode uses arrows and tab for navigation", () => {
|
||||||
const input = inputState();
|
const input = inputState();
|
||||||
|
|
||||||
assert.deepEqual(getInputCommand(input, "\x1b[A"), {
|
assert.deepEqual(getInputCommand(input, "\x1b[A", ""), {
|
||||||
|
kind: "editMoveOption",
|
||||||
|
delta: -1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\x1b[B", ""), {
|
||||||
|
kind: "editMoveOption",
|
||||||
|
delta: 1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\x1b[C", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: 1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\x1b[D", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: -1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\t", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: 1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\x1b[Z", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: -1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-empty typing mode keeps arrows and tab in editor", () => {
|
||||||
|
const input = inputState();
|
||||||
|
|
||||||
|
assert.deepEqual(getInputCommand(input, "\x1b[A", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(input, "\x1b[B"), {
|
assert.deepEqual(getInputCommand(input, "\x1b[B", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(input, "\x1b[C"), {
|
assert.deepEqual(getInputCommand(input, "\x1b[C", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(input, "\x1b[D"), {
|
assert.deepEqual(getInputCommand(input, "\x1b[D", "x"), {
|
||||||
|
kind: "delegateToEditor",
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\t", "x"), {
|
||||||
|
kind: "delegateToEditor",
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(input, "\x1b[Z", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("typing mode delegates tab to the editor", () => {
|
test("empty note editing mode uses arrows and tab for navigation", () => {
|
||||||
const input = inputState();
|
|
||||||
|
|
||||||
assert.deepEqual(getInputCommand(input, "\t"), {
|
|
||||||
kind: "delegateToEditor",
|
|
||||||
});
|
|
||||||
assert.deepEqual(getInputCommand(input, "\x1b[Z"), {
|
|
||||||
kind: "delegateToEditor",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("note editing mode delegates arrows and tab to the editor", () => {
|
|
||||||
const state = enterQuestionNoteMode(
|
const state = enterQuestionNoteMode(
|
||||||
createInitialState({
|
createInitialState({
|
||||||
questions: [
|
questions: [
|
||||||
@@ -63,19 +87,55 @@ test("note editing mode delegates arrows and tab to the editor", () => {
|
|||||||
"q1"
|
"q1"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepEqual(getInputCommand(state, "\x1b[A"), {
|
assert.deepEqual(getInputCommand(state, "\x1b[A", ""), {
|
||||||
|
kind: "editMoveOption",
|
||||||
|
delta: -1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(state, "\x1b[B", ""), {
|
||||||
|
kind: "editMoveOption",
|
||||||
|
delta: 1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(state, "\x1b[C", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: 1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(state, "\x1b[D", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: -1,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getInputCommand(state, "\t", ""), {
|
||||||
|
kind: "editMoveTab",
|
||||||
|
delta: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-empty note editing mode keeps arrows and tab in editor", () => {
|
||||||
|
const state = enterQuestionNoteMode(
|
||||||
|
createInitialState({
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "q1",
|
||||||
|
prompt: "Question?",
|
||||||
|
options: [{ value: "a", label: "A" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
"q1"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(getInputCommand(state, "\x1b[A", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(state, "\x1b[B"), {
|
assert.deepEqual(getInputCommand(state, "\x1b[B", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(state, "\x1b[C"), {
|
assert.deepEqual(getInputCommand(state, "\x1b[C", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(state, "\x1b[D"), {
|
assert.deepEqual(getInputCommand(state, "\x1b[D", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
assert.deepEqual(getInputCommand(state, "\t"), {
|
assert.deepEqual(getInputCommand(state, "\t", "x"), {
|
||||||
kind: "delegateToEditor",
|
kind: "delegateToEditor",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user