mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Vim mode (#3936)
This commit is contained in:
@@ -73,6 +73,8 @@ import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
import { useFocus } from './hooks/useFocus.js';
|
||||
import { useBracketedPaste } from './hooks/useBracketedPaste.js';
|
||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||
import { useVimMode, VimModeProvider } from './contexts/VimModeContext.js';
|
||||
import { useVim } from './hooks/vim.js';
|
||||
import * as fs from 'fs';
|
||||
import { UpdateNotification } from './components/UpdateNotification.js';
|
||||
import {
|
||||
@@ -97,7 +99,9 @@ interface AppProps {
|
||||
|
||||
export const AppWrapper = (props: AppProps) => (
|
||||
<SessionStatsProvider>
|
||||
<App {...props} />
|
||||
<VimModeProvider settings={props.settings}>
|
||||
<App {...props} />
|
||||
</VimModeProvider>
|
||||
</SessionStatsProvider>
|
||||
);
|
||||
|
||||
@@ -374,6 +378,49 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
config.setFlashFallbackHandler(flashFallbackHandler);
|
||||
}, [config, addItem, userTier]);
|
||||
|
||||
// Terminal and UI setup
|
||||
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
const widthFraction = 0.9;
|
||||
const inputWidth = Math.max(
|
||||
20,
|
||||
Math.floor(terminalWidth * widthFraction) - 3,
|
||||
);
|
||||
const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8));
|
||||
|
||||
// Utility callbacks
|
||||
const isValidPath = useCallback((filePath: string): boolean => {
|
||||
try {
|
||||
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPreferredEditor = useCallback(() => {
|
||||
const editorType = settings.merged.preferredEditor;
|
||||
const isValidEditor = isEditorAvailable(editorType);
|
||||
if (!isValidEditor) {
|
||||
openEditorDialog();
|
||||
return;
|
||||
}
|
||||
return editorType as EditorType;
|
||||
}, [settings, openEditorDialog]);
|
||||
|
||||
const onAuthError = useCallback(() => {
|
||||
setAuthError('reauth required');
|
||||
openAuthDialog();
|
||||
}, [openAuthDialog, setAuthError]);
|
||||
|
||||
// Core hooks and processors
|
||||
const {
|
||||
vimEnabled: vimModeEnabled,
|
||||
vimMode,
|
||||
toggleVimEnabled,
|
||||
} = useVimMode();
|
||||
|
||||
const {
|
||||
handleSlashCommand,
|
||||
slashCommands,
|
||||
@@ -394,26 +441,41 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
toggleCorgiMode,
|
||||
setQuittingMessages,
|
||||
openPrivacyNotice,
|
||||
toggleVimEnabled,
|
||||
);
|
||||
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
|
||||
|
||||
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
|
||||
const isInitialMount = useRef(true);
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const isValidPath = useCallback((filePath: string): boolean => {
|
||||
try {
|
||||
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const widthFraction = 0.9;
|
||||
const inputWidth = Math.max(
|
||||
20,
|
||||
Math.floor(terminalWidth * widthFraction) - 3,
|
||||
const {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
initError,
|
||||
pendingHistoryItems: pendingGeminiHistoryItems,
|
||||
thought,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
history,
|
||||
addItem,
|
||||
setShowHelp,
|
||||
config,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
);
|
||||
|
||||
// Input handling
|
||||
const handleFinalSubmit = useCallback(
|
||||
(submittedValue: string) => {
|
||||
const trimmedValue = submittedValue.trim();
|
||||
if (trimmedValue.length > 0) {
|
||||
submitQuery(trimmedValue);
|
||||
}
|
||||
},
|
||||
[submitQuery],
|
||||
);
|
||||
const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8));
|
||||
|
||||
const buffer = useTextBuffer({
|
||||
initialText: '',
|
||||
@@ -424,6 +486,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
shellModeActive,
|
||||
});
|
||||
|
||||
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
|
||||
pendingHistoryItems.push(...pendingGeminiHistoryItems);
|
||||
|
||||
const { elapsedTime, currentLoadingPhrase } =
|
||||
useLoadingIndicator(streamingState);
|
||||
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
|
||||
|
||||
const handleExit = useCallback(
|
||||
(
|
||||
pressedOnce: boolean,
|
||||
@@ -489,57 +559,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const getPreferredEditor = useCallback(() => {
|
||||
const editorType = settings.merged.preferredEditor;
|
||||
const isValidEditor = isEditorAvailable(editorType);
|
||||
if (!isValidEditor) {
|
||||
openEditorDialog();
|
||||
return;
|
||||
}
|
||||
return editorType as EditorType;
|
||||
}, [settings, openEditorDialog]);
|
||||
|
||||
const onAuthError = useCallback(() => {
|
||||
setAuthError('reauth required');
|
||||
openAuthDialog();
|
||||
}, [openAuthDialog, setAuthError]);
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
initError,
|
||||
pendingHistoryItems: pendingGeminiHistoryItems,
|
||||
thought,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
history,
|
||||
addItem,
|
||||
setShowHelp,
|
||||
config,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
);
|
||||
pendingHistoryItems.push(...pendingGeminiHistoryItems);
|
||||
const { elapsedTime, currentLoadingPhrase } =
|
||||
useLoadingIndicator(streamingState);
|
||||
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
|
||||
|
||||
const handleFinalSubmit = useCallback(
|
||||
(submittedValue: string) => {
|
||||
const trimmedValue = submittedValue.trim();
|
||||
if (trimmedValue.length > 0) {
|
||||
submitQuery(trimmedValue);
|
||||
}
|
||||
},
|
||||
[submitQuery],
|
||||
);
|
||||
|
||||
const logger = useLogger();
|
||||
const [userMessages, setUserMessages] = useState<string[]>([]);
|
||||
|
||||
@@ -697,6 +716,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
// Arbitrary threshold to ensure that items in the static area are large
|
||||
// enough but not too large to make the terminal hard to use.
|
||||
const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100);
|
||||
const placeholder = vimModeEnabled
|
||||
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
|
||||
: ' Type your message or @path/to/file';
|
||||
|
||||
return (
|
||||
<StreamingContext.Provider value={streamingState}>
|
||||
<Box flexDirection="column" marginBottom={1} width="90%">
|
||||
@@ -938,6 +961,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
shellModeActive={shellModeActive}
|
||||
setShellModeActive={setShellModeActive}
|
||||
focus={isFocused}
|
||||
vimHandleInput={vimHandleInput}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -989,6 +1014,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
}
|
||||
promptTokenCount={sessionStats.lastPromptTokenCount}
|
||||
nightly={nightly}
|
||||
vimMode={vimModeEnabled ? vimMode : undefined}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user