feat(settings) support editing string settings. (#6732)
This commit is contained in:
parent
10286934e6
commit
29699274bb
|
@ -27,11 +27,61 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { SettingsDialog } from './SettingsDialog.js';
|
import { SettingsDialog } from './SettingsDialog.js';
|
||||||
import { LoadedSettings } from '../../config/settings.js';
|
import { LoadedSettings } from '../../config/settings.js';
|
||||||
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
||||||
|
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||||
|
|
||||||
// Mock the VimModeContext
|
// Mock the VimModeContext
|
||||||
const mockToggleVimEnabled = vi.fn();
|
const mockToggleVimEnabled = vi.fn();
|
||||||
const mockSetVimMode = vi.fn();
|
const mockSetVimMode = vi.fn();
|
||||||
|
|
||||||
|
const createMockSettings = (
|
||||||
|
userSettings = {},
|
||||||
|
systemSettings = {},
|
||||||
|
workspaceSettings = {},
|
||||||
|
) =>
|
||||||
|
new LoadedSettings(
|
||||||
|
{
|
||||||
|
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
|
||||||
|
path: '/system/settings.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
customThemes: {},
|
||||||
|
mcpServers: {},
|
||||||
|
...userSettings,
|
||||||
|
},
|
||||||
|
path: '/user/settings.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settings: { customThemes: {}, mcpServers: {}, ...workspaceSettings },
|
||||||
|
path: '/workspace/settings.json',
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock('../contexts/SettingsContext.js', async () => {
|
||||||
|
const actual = await vi.importActual('../contexts/SettingsContext.js');
|
||||||
|
let settings = createMockSettings({ 'a.string.setting': 'initial' });
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useSettings: () => ({
|
||||||
|
settings,
|
||||||
|
setSetting: (key: string, value: string) => {
|
||||||
|
settings = createMockSettings({ [key]: value });
|
||||||
|
},
|
||||||
|
getSettingDefinition: (key: string) => {
|
||||||
|
if (key === 'a.string.setting') {
|
||||||
|
return {
|
||||||
|
type: 'string',
|
||||||
|
description: 'A string setting',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('../contexts/VimModeContext.js', async () => {
|
vi.mock('../contexts/VimModeContext.js', async () => {
|
||||||
const actual = await vi.importActual('../contexts/VimModeContext.js');
|
const actual = await vi.importActual('../contexts/VimModeContext.js');
|
||||||
return {
|
return {
|
||||||
|
@ -53,28 +103,6 @@ vi.mock('../../utils/settingsUtils.js', async () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock the useKeypress hook to avoid context issues
|
|
||||||
interface Key {
|
|
||||||
name: string;
|
|
||||||
ctrl: boolean;
|
|
||||||
meta: boolean;
|
|
||||||
shift: boolean;
|
|
||||||
paste: boolean;
|
|
||||||
sequence: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variables for keypress simulation (not currently used)
|
|
||||||
// let currentKeypressHandler: ((key: Key) => void) | null = null;
|
|
||||||
// let isKeypressActive = false;
|
|
||||||
|
|
||||||
vi.mock('../hooks/useKeypress.js', () => ({
|
|
||||||
useKeypress: vi.fn(
|
|
||||||
(_handler: (key: Key) => void, _options: { isActive: boolean }) => {
|
|
||||||
// Mock implementation - simplified for test stability
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Helper function to simulate key presses (commented out for now)
|
// Helper function to simulate key presses (commented out for now)
|
||||||
// const simulateKeyPress = async (keyData: Partial<Key> & { name: string }) => {
|
// const simulateKeyPress = async (keyData: Partial<Key> & { name: string }) => {
|
||||||
// if (currentKeypressHandler) {
|
// if (currentKeypressHandler) {
|
||||||
|
@ -149,7 +177,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
@ -163,7 +193,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
@ -176,7 +208,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
@ -191,7 +225,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Press down arrow
|
// Press down arrow
|
||||||
|
@ -207,7 +243,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// First go down, then up
|
// First go down, then up
|
||||||
|
@ -224,7 +262,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate with vim keys
|
// Navigate with vim keys
|
||||||
|
@ -241,7 +281,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try to go up from first item
|
// Try to go up from first item
|
||||||
|
@ -259,7 +301,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Press Enter to toggle current setting
|
// Press Enter to toggle current setting
|
||||||
|
@ -274,7 +318,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Press Space to toggle current setting
|
// Press Space to toggle current setting
|
||||||
|
@ -289,7 +335,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate to vim mode setting and toggle it
|
// Navigate to vim mode setting and toggle it
|
||||||
|
@ -308,7 +356,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch to scope focus
|
// Switch to scope focus
|
||||||
|
@ -327,7 +377,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for initial render
|
// Wait for initial render
|
||||||
|
@ -352,11 +404,13 @@ describe('SettingsDialog', () => {
|
||||||
const onRestartRequest = vi.fn();
|
const onRestartRequest = vi.fn();
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = render(
|
||||||
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
onRestartRequest={onRestartRequest}
|
onRestartRequest={onRestartRequest}
|
||||||
/>,
|
/>
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// This test would need to trigger a restart-required setting change
|
// This test would need to trigger a restart-required setting change
|
||||||
|
@ -371,11 +425,13 @@ describe('SettingsDialog', () => {
|
||||||
const onRestartRequest = vi.fn();
|
const onRestartRequest = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
onRestartRequest={onRestartRequest}
|
onRestartRequest={onRestartRequest}
|
||||||
/>,
|
/>
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Press 'r' key (this would only work if restart prompt is showing)
|
// Press 'r' key (this would only work if restart prompt is showing)
|
||||||
|
@ -393,7 +449,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for initial render
|
// Wait for initial render
|
||||||
|
@ -418,7 +476,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch to scope selector
|
// Switch to scope selector
|
||||||
|
@ -442,7 +502,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should show user scope values initially
|
// Should show user scope values initially
|
||||||
|
@ -459,7 +521,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try to toggle a setting (this might trigger vim mode toggle)
|
// Try to toggle a setting (this might trigger vim mode toggle)
|
||||||
|
@ -477,7 +541,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toggle a setting
|
// Toggle a setting
|
||||||
|
@ -499,7 +565,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate down many times to test scrolling
|
// Navigate down many times to test scrolling
|
||||||
|
@ -519,7 +587,9 @@ describe('SettingsDialog', () => {
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<VimModeProvider settings={settings}>
|
<VimModeProvider settings={settings}>
|
||||||
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>
|
||||||
</VimModeProvider>,
|
</VimModeProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -542,7 +612,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
@ -555,7 +627,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toggle a non-restart-required setting (like hideTips)
|
// Toggle a non-restart-required setting (like hideTips)
|
||||||
|
@ -571,7 +645,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// This test would need to navigate to a specific restart-required setting
|
// This test would need to navigate to a specific restart-required setting
|
||||||
|
@ -591,7 +667,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { unmount } = render(
|
const { unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restart prompt should be cleared when switching scopes
|
// Restart prompt should be cleared when switching scopes
|
||||||
|
@ -609,7 +687,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
@ -626,7 +706,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
@ -641,7 +723,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rapid navigation
|
// Rapid navigation
|
||||||
|
@ -660,7 +744,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Press Ctrl+C to reset current setting to default
|
// Press Ctrl+C to reset current setting to default
|
||||||
|
@ -676,7 +762,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Press Ctrl+L to reset current setting to default
|
// Press Ctrl+L to reset current setting to default
|
||||||
|
@ -692,7 +780,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try to navigate when potentially at bounds
|
// Try to navigate when potentially at bounds
|
||||||
|
@ -709,7 +799,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for initial render
|
// Wait for initial render
|
||||||
|
@ -739,7 +831,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should still render without crashing
|
// Should still render without crashing
|
||||||
|
@ -752,7 +846,9 @@ describe('SettingsDialog', () => {
|
||||||
|
|
||||||
// Should not crash even if some settings are missing definitions
|
// Should not crash even if some settings are missing definitions
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(lastFrame()).toContain('Settings');
|
expect(lastFrame()).toContain('Settings');
|
||||||
|
@ -765,7 +861,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { lastFrame, unmount } = render(
|
const { lastFrame, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for initial render
|
// Wait for initial render
|
||||||
|
@ -793,7 +891,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toggle first setting (should require restart)
|
// Toggle first setting (should require restart)
|
||||||
|
@ -822,7 +922,9 @@ describe('SettingsDialog', () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
<SettingsDialog settings={settings} onSelect={onSelect} />,
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Multiple scope changes
|
// Multiple scope changes
|
||||||
|
@ -846,11 +948,13 @@ describe('SettingsDialog', () => {
|
||||||
const onRestartRequest = vi.fn();
|
const onRestartRequest = vi.fn();
|
||||||
|
|
||||||
const { stdin, unmount } = render(
|
const { stdin, unmount } = render(
|
||||||
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => {}}
|
onSelect={() => {}}
|
||||||
onRestartRequest={onRestartRequest}
|
onRestartRequest={onRestartRequest}
|
||||||
/>,
|
/>
|
||||||
|
</KeypressProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// This would test the restart workflow if we could trigger it
|
// This would test the restart workflow if we could trigger it
|
||||||
|
@ -863,4 +967,58 @@ describe('SettingsDialog', () => {
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('String Settings Editing', () => {
|
||||||
|
it('should allow editing and committing a string setting', async () => {
|
||||||
|
let settings = createMockSettings({ 'a.string.setting': 'initial' });
|
||||||
|
const onSelect = vi.fn();
|
||||||
|
|
||||||
|
const { stdin, unmount, rerender } = render(
|
||||||
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for the dialog to render
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
// Navigate to the last setting
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
stdin.write('j'); // Down
|
||||||
|
await wait(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press Enter to start editing
|
||||||
|
stdin.write('\r');
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
// Type a new value
|
||||||
|
stdin.write('new value');
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
// Press Enter to commit
|
||||||
|
stdin.write('\r');
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
settings = createMockSettings(
|
||||||
|
{ 'a.string.setting': 'new value' },
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
rerender(
|
||||||
|
<KeypressProvider kittyProtocolEnabled={false}>
|
||||||
|
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||||
|
</KeypressProvider>,
|
||||||
|
);
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
// Press Escape to exit
|
||||||
|
stdin.write('\u001B');
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
expect(onSelect).toHaveBeenCalledWith(undefined, 'User');
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,7 +35,7 @@ import {
|
||||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { cpSlice, cpLen } from '../utils/textUtils.js';
|
import { cpSlice, cpLen, stripUnsafeCharacters } from '../utils/textUtils.js';
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
settings: LoadedSettings;
|
settings: LoadedSettings;
|
||||||
|
@ -78,8 +78,8 @@ export function SettingsDialog({
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Preserve pending changes across scope switches (boolean and number values only)
|
// Preserve pending changes across scope switches
|
||||||
type PendingValue = boolean | number;
|
type PendingValue = boolean | number | string;
|
||||||
const [globalPendingChanges, setGlobalPendingChanges] = useState<
|
const [globalPendingChanges, setGlobalPendingChanges] = useState<
|
||||||
Map<string, PendingValue>
|
Map<string, PendingValue>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
|
@ -99,7 +99,10 @@ export function SettingsDialog({
|
||||||
const def = getSettingDefinition(key);
|
const def = getSettingDefinition(key);
|
||||||
if (def?.type === 'boolean' && typeof value === 'boolean') {
|
if (def?.type === 'boolean' && typeof value === 'boolean') {
|
||||||
updated = setPendingSettingValue(key, value, updated);
|
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);
|
updated = setPendingSettingValueAny(key, value, updated);
|
||||||
}
|
}
|
||||||
newModified.add(key);
|
newModified.add(key);
|
||||||
|
@ -123,7 +126,7 @@ export function SettingsDialog({
|
||||||
type: definition?.type,
|
type: definition?.type,
|
||||||
toggle: () => {
|
toggle: () => {
|
||||||
if (definition?.type !== 'boolean') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const currentValue = getSettingValue(key, pendingSettings, {});
|
const currentValue = getSettingValue(key, pendingSettings, {});
|
||||||
|
@ -220,7 +223,7 @@ export function SettingsDialog({
|
||||||
|
|
||||||
const items = generateSettingsItems();
|
const items = generateSettingsItems();
|
||||||
|
|
||||||
// Number edit state
|
// Generic edit state
|
||||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||||
const [editBuffer, setEditBuffer] = useState<string>('');
|
const [editBuffer, setEditBuffer] = useState<string>('');
|
||||||
const [editCursorPos, setEditCursorPos] = useState<number>(0); // Cursor position within edit buffer
|
const [editCursorPos, setEditCursorPos] = useState<number>(0); // Cursor position within edit buffer
|
||||||
|
@ -235,29 +238,40 @@ export function SettingsDialog({
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [editingKey]);
|
}, [editingKey]);
|
||||||
|
|
||||||
const startEditingNumber = (key: string, initial?: string) => {
|
const startEditing = (key: string, initial?: string) => {
|
||||||
setEditingKey(key);
|
setEditingKey(key);
|
||||||
const initialValue = initial ?? '';
|
const initialValue = initial ?? '';
|
||||||
setEditBuffer(initialValue);
|
setEditBuffer(initialValue);
|
||||||
setEditCursorPos(cpLen(initialValue)); // Position cursor at end of initial value
|
setEditCursorPos(cpLen(initialValue)); // Position cursor at end of initial value
|
||||||
};
|
};
|
||||||
|
|
||||||
const commitNumberEdit = (key: string) => {
|
const commitEdit = (key: string) => {
|
||||||
if (editBuffer.trim() === '') {
|
const definition = getSettingDefinition(key);
|
||||||
// Nothing entered; cancel edit
|
const type = definition?.type;
|
||||||
|
|
||||||
|
if (editBuffer.trim() === '' && type === 'number') {
|
||||||
|
// Nothing entered for a number; cancel edit
|
||||||
setEditingKey(null);
|
setEditingKey(null);
|
||||||
setEditBuffer('');
|
setEditBuffer('');
|
||||||
setEditCursorPos(0);
|
setEditCursorPos(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parsed = Number(editBuffer.trim());
|
|
||||||
if (Number.isNaN(parsed)) {
|
let parsed: string | number;
|
||||||
|
if (type === 'number') {
|
||||||
|
const numParsed = Number(editBuffer.trim());
|
||||||
|
if (Number.isNaN(numParsed)) {
|
||||||
// Invalid number; cancel edit
|
// Invalid number; cancel edit
|
||||||
setEditingKey(null);
|
setEditingKey(null);
|
||||||
setEditBuffer('');
|
setEditBuffer('');
|
||||||
setEditCursorPos(0);
|
setEditCursorPos(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
parsed = numParsed;
|
||||||
|
} else {
|
||||||
|
// For strings, use the buffer as is.
|
||||||
|
parsed = editBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
// Update pending
|
// Update pending
|
||||||
setPendingSettings((prev) => setPendingSettingValueAny(key, parsed, prev));
|
setPendingSettings((prev) => setPendingSettingValueAny(key, parsed, prev));
|
||||||
|
@ -347,10 +361,16 @@ export function SettingsDialog({
|
||||||
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
|
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
|
||||||
}
|
}
|
||||||
if (focusSection === 'settings') {
|
if (focusSection === 'settings') {
|
||||||
// If editing a number, capture numeric input and control keys
|
// If editing, capture input and control keys
|
||||||
if (editingKey) {
|
if (editingKey) {
|
||||||
|
const definition = getSettingDefinition(editingKey);
|
||||||
|
const type = definition?.type;
|
||||||
|
|
||||||
if (key.paste && key.sequence) {
|
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) {
|
if (pasted) {
|
||||||
setEditBuffer((b) => {
|
setEditBuffer((b) => {
|
||||||
const before = cpSlice(b, 0, editCursorPos);
|
const before = cpSlice(b, 0, editCursorPos);
|
||||||
|
@ -380,16 +400,27 @@ export function SettingsDialog({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === 'escape') {
|
if (name === 'escape') {
|
||||||
commitNumberEdit(editingKey);
|
commitEdit(editingKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === 'return') {
|
if (name === 'return') {
|
||||||
commitNumberEdit(editingKey);
|
commitEdit(editingKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Allow digits, minus, plus, and dot
|
|
||||||
const ch = key.sequence;
|
let ch = key.sequence;
|
||||||
if (/[0-9\-+.]/.test(ch)) {
|
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) => {
|
setEditBuffer((currentBuffer) => {
|
||||||
const beforeCursor = cpSlice(currentBuffer, 0, editCursorPos);
|
const beforeCursor = cpSlice(currentBuffer, 0, editCursorPos);
|
||||||
const afterCursor = cpSlice(currentBuffer, editCursorPos);
|
const afterCursor = cpSlice(currentBuffer, editCursorPos);
|
||||||
|
@ -398,6 +429,7 @@ export function SettingsDialog({
|
||||||
setEditCursorPos((pos) => pos + 1);
|
setEditCursorPos((pos) => pos + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow key navigation
|
// Arrow key navigation
|
||||||
if (name === 'left') {
|
if (name === 'left') {
|
||||||
setEditCursorPos((pos) => Math.max(0, pos - 1));
|
setEditCursorPos((pos) => Math.max(0, pos - 1));
|
||||||
|
@ -422,7 +454,7 @@ export function SettingsDialog({
|
||||||
if (name === 'up' || name === 'k') {
|
if (name === 'up' || name === 'k') {
|
||||||
// If editing, commit first
|
// If editing, commit first
|
||||||
if (editingKey) {
|
if (editingKey) {
|
||||||
commitNumberEdit(editingKey);
|
commitEdit(editingKey);
|
||||||
}
|
}
|
||||||
const newIndex =
|
const newIndex =
|
||||||
activeSettingIndex > 0 ? activeSettingIndex - 1 : items.length - 1;
|
activeSettingIndex > 0 ? activeSettingIndex - 1 : items.length - 1;
|
||||||
|
@ -436,7 +468,7 @@ export function SettingsDialog({
|
||||||
} else if (name === 'down' || name === 'j') {
|
} else if (name === 'down' || name === 'j') {
|
||||||
// If editing, commit first
|
// If editing, commit first
|
||||||
if (editingKey) {
|
if (editingKey) {
|
||||||
commitNumberEdit(editingKey);
|
commitEdit(editingKey);
|
||||||
}
|
}
|
||||||
const newIndex =
|
const newIndex =
|
||||||
activeSettingIndex < items.length - 1 ? activeSettingIndex + 1 : 0;
|
activeSettingIndex < items.length - 1 ? activeSettingIndex + 1 : 0;
|
||||||
|
@ -449,15 +481,18 @@ export function SettingsDialog({
|
||||||
}
|
}
|
||||||
} else if (name === 'return' || name === 'space') {
|
} else if (name === 'return' || name === 'space') {
|
||||||
const currentItem = items[activeSettingIndex];
|
const currentItem = items[activeSettingIndex];
|
||||||
if (currentItem?.type === 'number') {
|
if (
|
||||||
startEditingNumber(currentItem.value);
|
currentItem?.type === 'number' ||
|
||||||
|
currentItem?.type === 'string'
|
||||||
|
) {
|
||||||
|
startEditing(currentItem.value);
|
||||||
} else {
|
} else {
|
||||||
currentItem?.toggle();
|
currentItem?.toggle();
|
||||||
}
|
}
|
||||||
} else if (/^[0-9]$/.test(key.sequence || '') && !editingKey) {
|
} else if (/^[0-9]$/.test(key.sequence || '') && !editingKey) {
|
||||||
const currentItem = items[activeSettingIndex];
|
const currentItem = items[activeSettingIndex];
|
||||||
if (currentItem?.type === 'number') {
|
if (currentItem?.type === 'number') {
|
||||||
startEditingNumber(currentItem.value, key.sequence);
|
startEditing(currentItem.value, key.sequence);
|
||||||
}
|
}
|
||||||
} else if (ctrl && (name === 'c' || name === 'l')) {
|
} else if (ctrl && (name === 'c' || name === 'l')) {
|
||||||
// Ctrl+C or Ctrl+L: Clear current setting and reset to default
|
// Ctrl+C or Ctrl+L: Clear current setting and reset to default
|
||||||
|
@ -475,8 +510,11 @@ export function SettingsDialog({
|
||||||
prev,
|
prev,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (defType === 'number') {
|
} else if (defType === 'number' || defType === 'string') {
|
||||||
if (typeof defaultValue === 'number') {
|
if (
|
||||||
|
typeof defaultValue === 'number' ||
|
||||||
|
typeof defaultValue === 'string'
|
||||||
|
) {
|
||||||
setPendingSettings((prev) =>
|
setPendingSettings((prev) =>
|
||||||
setPendingSettingValueAny(
|
setPendingSettingValueAny(
|
||||||
currentSetting.value,
|
currentSetting.value,
|
||||||
|
@ -509,7 +547,8 @@ export function SettingsDialog({
|
||||||
? typeof defaultValue === 'boolean'
|
? typeof defaultValue === 'boolean'
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: false
|
: false
|
||||||
: typeof defaultValue === 'number'
|
: typeof defaultValue === 'number' ||
|
||||||
|
typeof defaultValue === 'string'
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: undefined;
|
: undefined;
|
||||||
const immediateSettingsObject =
|
const immediateSettingsObject =
|
||||||
|
@ -541,7 +580,9 @@ export function SettingsDialog({
|
||||||
(currentSetting.type === 'boolean' &&
|
(currentSetting.type === 'boolean' &&
|
||||||
typeof defaultValue === 'boolean') ||
|
typeof defaultValue === 'boolean') ||
|
||||||
(currentSetting.type === 'number' &&
|
(currentSetting.type === 'number' &&
|
||||||
typeof defaultValue === 'number')
|
typeof defaultValue === 'number') ||
|
||||||
|
(currentSetting.type === 'string' &&
|
||||||
|
typeof defaultValue === 'string')
|
||||||
) {
|
) {
|
||||||
setGlobalPendingChanges((prev) => {
|
setGlobalPendingChanges((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
|
@ -584,7 +625,7 @@ export function SettingsDialog({
|
||||||
}
|
}
|
||||||
if (name === 'escape') {
|
if (name === 'escape') {
|
||||||
if (editingKey) {
|
if (editingKey) {
|
||||||
commitNumberEdit(editingKey);
|
commitEdit(editingKey);
|
||||||
} else {
|
} else {
|
||||||
onSelect(undefined, selectedScope);
|
onSelect(undefined, selectedScope);
|
||||||
}
|
}
|
||||||
|
@ -637,8 +678,8 @@ export function SettingsDialog({
|
||||||
// Cursor not visible
|
// Cursor not visible
|
||||||
displayValue = editBuffer;
|
displayValue = editBuffer;
|
||||||
}
|
}
|
||||||
} else if (item.type === 'number') {
|
} else if (item.type === 'number' || item.type === 'string') {
|
||||||
// For numbers, get the actual current value from pending settings
|
// For numbers/strings, get the actual current value from pending settings
|
||||||
const path = item.value.split('.');
|
const path = item.value.split('.');
|
||||||
const currentValue = getNestedValue(pendingSettings, path);
|
const currentValue = getNestedValue(pendingSettings, path);
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import stripAnsi from 'strip-ansi';
|
|
||||||
import { stripVTControlCharacters } from 'util';
|
|
||||||
import { spawnSync } from 'child_process';
|
import { spawnSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
@ -13,7 +11,12 @@ import pathMod from 'path';
|
||||||
import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
|
import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||||
import stringWidth from 'string-width';
|
import stringWidth from 'string-width';
|
||||||
import { unescapePath } from '@google/gemini-cli-core';
|
import { unescapePath } from '@google/gemini-cli-core';
|
||||||
import { toCodePoints, cpLen, cpSlice } from '../../utils/textUtils.js';
|
import {
|
||||||
|
toCodePoints,
|
||||||
|
cpLen,
|
||||||
|
cpSlice,
|
||||||
|
stripUnsafeCharacters,
|
||||||
|
} from '../../utils/textUtils.js';
|
||||||
import { handleVimAction, VimAction } from './vim-buffer-actions.js';
|
import { handleVimAction, VimAction } from './vim-buffer-actions.js';
|
||||||
|
|
||||||
export type Direction =
|
export type Direction =
|
||||||
|
@ -494,51 +497,6 @@ export const replaceRangeInternal = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip characters that can break terminal rendering.
|
|
||||||
*
|
|
||||||
* Uses Node.js built-in stripVTControlCharacters to handle VT sequences,
|
|
||||||
* then filters remaining control characters that can disrupt display.
|
|
||||||
*
|
|
||||||
* Characters stripped:
|
|
||||||
* - ANSI escape sequences (via strip-ansi)
|
|
||||||
* - VT control sequences (via Node.js util.stripVTControlCharacters)
|
|
||||||
* - C0 control chars (0x00-0x1F) except CR/LF which are handled elsewhere
|
|
||||||
* - C1 control chars (0x80-0x9F) that can cause display issues
|
|
||||||
*
|
|
||||||
* Characters preserved:
|
|
||||||
* - All printable Unicode including emojis
|
|
||||||
* - DEL (0x7F) - handled functionally by applyOperations, not a display issue
|
|
||||||
* - CR/LF (0x0D/0x0A) - needed for line breaks
|
|
||||||
*/
|
|
||||||
function stripUnsafeCharacters(str: string): string {
|
|
||||||
const strippedAnsi = stripAnsi(str);
|
|
||||||
const strippedVT = stripVTControlCharacters(strippedAnsi);
|
|
||||||
|
|
||||||
return toCodePoints(strippedVT)
|
|
||||||
.filter((char) => {
|
|
||||||
const code = char.codePointAt(0);
|
|
||||||
if (code === undefined) return false;
|
|
||||||
|
|
||||||
// Preserve CR/LF for line handling
|
|
||||||
if (code === 0x0a || code === 0x0d) return true;
|
|
||||||
|
|
||||||
// Remove C0 control chars (except CR/LF) that can break display
|
|
||||||
// Examples: BELL(0x07) makes noise, BS(0x08) moves cursor, VT(0x0B), FF(0x0C)
|
|
||||||
if (code >= 0x00 && code <= 0x1f) return false;
|
|
||||||
|
|
||||||
// Remove C1 control chars (0x80-0x9F) - legacy 8-bit control codes
|
|
||||||
if (code >= 0x80 && code <= 0x9f) return false;
|
|
||||||
|
|
||||||
// Preserve DEL (0x7F) - it's handled functionally by applyOperations as backspace
|
|
||||||
// and doesn't cause rendering issues when displayed
|
|
||||||
|
|
||||||
// Preserve all other characters including Unicode/emojis
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Viewport {
|
export interface Viewport {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import { stripVTControlCharacters } from 'util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the maximum width of a multi-line ASCII art string.
|
* Calculates the maximum width of a multi-line ASCII art string.
|
||||||
* @param asciiArt The ASCII art string.
|
* @param asciiArt The ASCII art string.
|
||||||
|
@ -38,3 +41,48 @@ export function cpSlice(str: string, start: number, end?: number): string {
|
||||||
const arr = toCodePoints(str).slice(start, end);
|
const arr = toCodePoints(str).slice(start, end);
|
||||||
return arr.join('');
|
return arr.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip characters that can break terminal rendering.
|
||||||
|
*
|
||||||
|
* Uses Node.js built-in stripVTControlCharacters to handle VT sequences,
|
||||||
|
* then filters remaining control characters that can disrupt display.
|
||||||
|
*
|
||||||
|
* Characters stripped:
|
||||||
|
* - ANSI escape sequences (via strip-ansi)
|
||||||
|
* - VT control sequences (via Node.js util.stripVTControlCharacters)
|
||||||
|
* - C0 control chars (0x00-0x1F) except CR/LF which are handled elsewhere
|
||||||
|
* - C1 control chars (0x80-0x9F) that can cause display issues
|
||||||
|
*
|
||||||
|
* Characters preserved:
|
||||||
|
* - All printable Unicode including emojis
|
||||||
|
* - DEL (0x7F) - handled functionally by applyOperations, not a display issue
|
||||||
|
* - CR/LF (0x0D/0x0A) - needed for line breaks
|
||||||
|
*/
|
||||||
|
export function stripUnsafeCharacters(str: string): string {
|
||||||
|
const strippedAnsi = stripAnsi(str);
|
||||||
|
const strippedVT = stripVTControlCharacters(strippedAnsi);
|
||||||
|
|
||||||
|
return toCodePoints(strippedVT)
|
||||||
|
.filter((char) => {
|
||||||
|
const code = char.codePointAt(0);
|
||||||
|
if (code === undefined) return false;
|
||||||
|
|
||||||
|
// Preserve CR/LF for line handling
|
||||||
|
if (code === 0x0a || code === 0x0d) return true;
|
||||||
|
|
||||||
|
// Remove C0 control chars (except CR/LF) that can break display
|
||||||
|
// Examples: BELL(0x07) makes noise, BS(0x08) moves cursor, VT(0x0B), FF(0x0C)
|
||||||
|
if (code >= 0x00 && code <= 0x1f) return false;
|
||||||
|
|
||||||
|
// Remove C1 control chars (0x80-0x9f) - legacy 8-bit control codes
|
||||||
|
if (code >= 0x80 && code <= 0x9f) return false;
|
||||||
|
|
||||||
|
// Preserve DEL (0x7f) - it's handled functionally by applyOperations as backspace
|
||||||
|
// and doesn't cause rendering issues when displayed
|
||||||
|
|
||||||
|
// Preserve all other characters including Unicode/emojis
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue