feat(settings) support editing string settings. (#6732)

This commit is contained in:
Jacob Richman
2025-08-21 16:43:56 -07:00
committed by GitHub
parent 10286934e6
commit 29699274bb
4 changed files with 361 additions and 156 deletions

View File

@@ -35,7 +35,7 @@ import {
import { useVimMode } from '../contexts/VimModeContext.js';
import { useKeypress } from '../hooks/useKeypress.js';
import chalk from 'chalk';
import { cpSlice, cpLen } from '../utils/textUtils.js';
import { cpSlice, cpLen, stripUnsafeCharacters } from '../utils/textUtils.js';
interface SettingsDialogProps {
settings: LoadedSettings;
@@ -78,8 +78,8 @@ export function SettingsDialog({
new Set(),
);
// Preserve pending changes across scope switches (boolean and number values only)
type PendingValue = boolean | number;
// Preserve pending changes across scope switches
type PendingValue = boolean | number | string;
const [globalPendingChanges, setGlobalPendingChanges] = useState<
Map<string, PendingValue>
>(new Map());
@@ -99,7 +99,10 @@ export function SettingsDialog({
const def = getSettingDefinition(key);
if (def?.type === 'boolean' && typeof value === 'boolean') {
updated = setPendingSettingValue(key, value, updated);
} else if (def?.type === 'number' && typeof value === 'number') {
} else if (
(def?.type === 'number' && typeof value === 'number') ||
(def?.type === 'string' && typeof value === 'string')
) {
updated = setPendingSettingValueAny(key, value, updated);
}
newModified.add(key);
@@ -123,7 +126,7 @@ export function SettingsDialog({
type: definition?.type,
toggle: () => {
if (definition?.type !== 'boolean') {
// For non-boolean (e.g., number) items, toggle will be handled via edit mode.
// For non-boolean items, toggle will be handled via edit mode.
return;
}
const currentValue = getSettingValue(key, pendingSettings, {});
@@ -220,7 +223,7 @@ export function SettingsDialog({
const items = generateSettingsItems();
// Number edit state
// Generic edit state
const [editingKey, setEditingKey] = useState<string | null>(null);
const [editBuffer, setEditBuffer] = useState<string>('');
const [editCursorPos, setEditCursorPos] = useState<number>(0); // Cursor position within edit buffer
@@ -235,28 +238,39 @@ export function SettingsDialog({
return () => clearInterval(id);
}, [editingKey]);
const startEditingNumber = (key: string, initial?: string) => {
const startEditing = (key: string, initial?: string) => {
setEditingKey(key);
const initialValue = initial ?? '';
setEditBuffer(initialValue);
setEditCursorPos(cpLen(initialValue)); // Position cursor at end of initial value
};
const commitNumberEdit = (key: string) => {
if (editBuffer.trim() === '') {
// Nothing entered; cancel edit
const commitEdit = (key: string) => {
const definition = getSettingDefinition(key);
const type = definition?.type;
if (editBuffer.trim() === '' && type === 'number') {
// Nothing entered for a number; cancel edit
setEditingKey(null);
setEditBuffer('');
setEditCursorPos(0);
return;
}
const parsed = Number(editBuffer.trim());
if (Number.isNaN(parsed)) {
// Invalid number; cancel edit
setEditingKey(null);
setEditBuffer('');
setEditCursorPos(0);
return;
let parsed: string | number;
if (type === 'number') {
const numParsed = Number(editBuffer.trim());
if (Number.isNaN(numParsed)) {
// Invalid number; cancel edit
setEditingKey(null);
setEditBuffer('');
setEditCursorPos(0);
return;
}
parsed = numParsed;
} else {
// For strings, use the buffer as is.
parsed = editBuffer;
}
// Update pending
@@ -347,10 +361,16 @@ export function SettingsDialog({
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
}
if (focusSection === 'settings') {
// If editing a number, capture numeric input and control keys
// If editing, capture input and control keys
if (editingKey) {
const definition = getSettingDefinition(editingKey);
const type = definition?.type;
if (key.paste && key.sequence) {
const pasted = key.sequence.replace(/[^0-9\-+.]/g, '');
let pasted = key.sequence;
if (type === 'number') {
pasted = key.sequence.replace(/[^0-9\-+.]/g, '');
}
if (pasted) {
setEditBuffer((b) => {
const before = cpSlice(b, 0, editCursorPos);
@@ -380,16 +400,27 @@ export function SettingsDialog({
return;
}
if (name === 'escape') {
commitNumberEdit(editingKey);
commitEdit(editingKey);
return;
}
if (name === 'return') {
commitNumberEdit(editingKey);
commitEdit(editingKey);
return;
}
// Allow digits, minus, plus, and dot
const ch = key.sequence;
if (/[0-9\-+.]/.test(ch)) {
let ch = key.sequence;
let isValidChar = false;
if (type === 'number') {
// Allow digits, minus, plus, and dot.
isValidChar = /[0-9\-+.]/.test(ch);
} else {
ch = stripUnsafeCharacters(ch);
// For strings, allow any single character that isn't a control
// sequence.
isValidChar = ch.length === 1;
}
if (isValidChar) {
setEditBuffer((currentBuffer) => {
const beforeCursor = cpSlice(currentBuffer, 0, editCursorPos);
const afterCursor = cpSlice(currentBuffer, editCursorPos);
@@ -398,6 +429,7 @@ export function SettingsDialog({
setEditCursorPos((pos) => pos + 1);
return;
}
// Arrow key navigation
if (name === 'left') {
setEditCursorPos((pos) => Math.max(0, pos - 1));
@@ -422,7 +454,7 @@ export function SettingsDialog({
if (name === 'up' || name === 'k') {
// If editing, commit first
if (editingKey) {
commitNumberEdit(editingKey);
commitEdit(editingKey);
}
const newIndex =
activeSettingIndex > 0 ? activeSettingIndex - 1 : items.length - 1;
@@ -436,7 +468,7 @@ export function SettingsDialog({
} else if (name === 'down' || name === 'j') {
// If editing, commit first
if (editingKey) {
commitNumberEdit(editingKey);
commitEdit(editingKey);
}
const newIndex =
activeSettingIndex < items.length - 1 ? activeSettingIndex + 1 : 0;
@@ -449,15 +481,18 @@ export function SettingsDialog({
}
} else if (name === 'return' || name === 'space') {
const currentItem = items[activeSettingIndex];
if (currentItem?.type === 'number') {
startEditingNumber(currentItem.value);
if (
currentItem?.type === 'number' ||
currentItem?.type === 'string'
) {
startEditing(currentItem.value);
} else {
currentItem?.toggle();
}
} else if (/^[0-9]$/.test(key.sequence || '') && !editingKey) {
const currentItem = items[activeSettingIndex];
if (currentItem?.type === 'number') {
startEditingNumber(currentItem.value, key.sequence);
startEditing(currentItem.value, key.sequence);
}
} else if (ctrl && (name === 'c' || name === 'l')) {
// Ctrl+C or Ctrl+L: Clear current setting and reset to default
@@ -475,8 +510,11 @@ export function SettingsDialog({
prev,
),
);
} else if (defType === 'number') {
if (typeof defaultValue === 'number') {
} else if (defType === 'number' || defType === 'string') {
if (
typeof defaultValue === 'number' ||
typeof defaultValue === 'string'
) {
setPendingSettings((prev) =>
setPendingSettingValueAny(
currentSetting.value,
@@ -509,7 +547,8 @@ export function SettingsDialog({
? typeof defaultValue === 'boolean'
? defaultValue
: false
: typeof defaultValue === 'number'
: typeof defaultValue === 'number' ||
typeof defaultValue === 'string'
? defaultValue
: undefined;
const immediateSettingsObject =
@@ -541,7 +580,9 @@ export function SettingsDialog({
(currentSetting.type === 'boolean' &&
typeof defaultValue === 'boolean') ||
(currentSetting.type === 'number' &&
typeof defaultValue === 'number')
typeof defaultValue === 'number') ||
(currentSetting.type === 'string' &&
typeof defaultValue === 'string')
) {
setGlobalPendingChanges((prev) => {
const next = new Map(prev);
@@ -584,7 +625,7 @@ export function SettingsDialog({
}
if (name === 'escape') {
if (editingKey) {
commitNumberEdit(editingKey);
commitEdit(editingKey);
} else {
onSelect(undefined, selectedScope);
}
@@ -637,8 +678,8 @@ export function SettingsDialog({
// Cursor not visible
displayValue = editBuffer;
}
} else if (item.type === 'number') {
// For numbers, get the actual current value from pending settings
} else if (item.type === 'number' || item.type === 'string') {
// For numbers/strings, get the actual current value from pending settings
const path = item.value.split('.');
const currentValue = getNestedValue(pendingSettings, path);