mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
Create Todo List Tab (#11430)
This commit is contained in:
committed by
GitHub
parent
2ef38065c7
commit
cd76b0b22d
@@ -46,6 +46,7 @@ export enum Command {
|
||||
|
||||
// App level bindings
|
||||
SHOW_ERROR_DETAILS = 'showErrorDetails',
|
||||
SHOW_FULL_TODOS = 'showFullTodos',
|
||||
TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail',
|
||||
TOGGLE_MARKDOWN = 'toggleMarkdown',
|
||||
QUIT = 'quit',
|
||||
@@ -156,6 +157,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
|
||||
// App level bindings
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }],
|
||||
[Command.SHOW_FULL_TODOS]: [{ key: 't', ctrl: true }],
|
||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
|
||||
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
|
||||
[Command.QUIT]: [{ key: 'c', ctrl: true }],
|
||||
|
||||
@@ -788,6 +788,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
);
|
||||
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||
const [showFullTodos, setShowFullTodos] = useState<boolean>(false);
|
||||
const [renderMarkdown, setRenderMarkdown] = useState<boolean>(true);
|
||||
|
||||
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
|
||||
@@ -961,6 +962,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
|
||||
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
||||
setShowErrorDetails((prev) => !prev);
|
||||
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
||||
setShowFullTodos((prev) => !prev);
|
||||
} else if (keyMatchers[Command.TOGGLE_MARKDOWN](key)) {
|
||||
setRenderMarkdown((prev) => {
|
||||
const newValue = !prev;
|
||||
@@ -1129,6 +1132,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
isTrustedFolder,
|
||||
constrainHeight,
|
||||
showErrorDetails,
|
||||
showFullTodos,
|
||||
filteredConsoleMessages,
|
||||
ideContextState,
|
||||
renderMarkdown,
|
||||
@@ -1209,6 +1213,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
isTrustedFolder,
|
||||
constrainHeight,
|
||||
showErrorDetails,
|
||||
showFullTodos,
|
||||
filteredConsoleMessages,
|
||||
ideContextState,
|
||||
renderMarkdown,
|
||||
|
||||
@@ -113,6 +113,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
geminiMdFileCount: 0,
|
||||
renderMarkdown: true,
|
||||
filteredConsoleMessages: [],
|
||||
history: [],
|
||||
sessionStats: {
|
||||
lastPromptTokenCount: 0,
|
||||
sessionTokenCount: 0,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { AnchoredTodoListDisplay } from './messages/Todo.js';
|
||||
|
||||
export const Composer = () => {
|
||||
const config = useConfig();
|
||||
@@ -129,6 +130,8 @@ export const Composer = () => {
|
||||
</OverflowProvider>
|
||||
)}
|
||||
|
||||
<AnchoredTodoListDisplay />
|
||||
|
||||
{uiState.isInputActive && (
|
||||
<InputPrompt
|
||||
buffer={uiState.buffer}
|
||||
|
||||
142
packages/cli/src/ui/components/messages/Todo.test.tsx
Normal file
142
packages/cli/src/ui/components/messages/Todo.test.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Box } from 'ink';
|
||||
import { AnchoredTodoListDisplay, TodoListDisplay } from './Todo.js';
|
||||
import type { TodoList, TodoStatus } from '@google/gemini-cli-core';
|
||||
import type { UIState } from '../../contexts/UIStateContext.js';
|
||||
import { UIStateContext } from '../../contexts/UIStateContext.js';
|
||||
import type { HistoryItem } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
|
||||
describe('<TodoListDisplay />', () => {
|
||||
it('renders an empty todo list correctly', () => {
|
||||
const todos: TodoList = { todos: [] };
|
||||
const { lastFrame } = render(<TodoListDisplay todos={todos} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a todo list with various statuses correctly', () => {
|
||||
const todos: TodoList = {
|
||||
todos: [
|
||||
{ description: 'Task 1', status: 'pending' as TodoStatus },
|
||||
{ description: 'Task 2', status: 'in_progress' as TodoStatus },
|
||||
{ description: 'Task 3', status: 'completed' as TodoStatus },
|
||||
{ description: 'Task 4', status: 'cancelled' as TodoStatus },
|
||||
],
|
||||
};
|
||||
const { lastFrame } = render(<TodoListDisplay todos={todos} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a todo list with long descriptions that wrap', () => {
|
||||
const todos: TodoList = {
|
||||
todos: [
|
||||
{
|
||||
description:
|
||||
'This is a very long description for a pending task that should wrap around multiple lines when the terminal width is constrained.',
|
||||
status: 'pending' as TodoStatus,
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Another completed task with an equally verbose description to test wrapping behavior.',
|
||||
status: 'completed' as TodoStatus,
|
||||
},
|
||||
],
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<Box width="30">
|
||||
<TodoListDisplay todos={todos} />
|
||||
</Box>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a single todo item', () => {
|
||||
const todos: TodoList = {
|
||||
todos: [{ description: 'Single task', status: 'pending' as TodoStatus }],
|
||||
};
|
||||
const { lastFrame } = render(<TodoListDisplay todos={todos} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AnchoredTodoListDisplay />', () => {
|
||||
const mockHistoryItem = {
|
||||
type: 'tool_group',
|
||||
id: '1',
|
||||
tools: [
|
||||
{
|
||||
name: 'write_todos_list',
|
||||
callId: 'tool-1',
|
||||
status: ToolCallStatus.Success,
|
||||
resultDisplay: {
|
||||
todos: [
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
{ description: 'In Progress Task', status: 'in_progress' },
|
||||
{ description: 'Completed Task', status: 'completed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as HistoryItem;
|
||||
|
||||
const renderWithUiState = (uiState: Partial<UIState>) =>
|
||||
render(
|
||||
<UIStateContext.Provider value={uiState as UIState}>
|
||||
<AnchoredTodoListDisplay />
|
||||
</UIStateContext.Provider>,
|
||||
);
|
||||
|
||||
it('renders null when no todos are in the history', () => {
|
||||
const { lastFrame } = renderWithUiState({ history: [] });
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders null when todos exist but none are in progress and full view is off', () => {
|
||||
const historyWithNoInProgress = {
|
||||
type: 'tool_group',
|
||||
id: '1',
|
||||
tools: [
|
||||
{
|
||||
name: 'write_todos_list',
|
||||
callId: 'tool-1',
|
||||
status: ToolCallStatus.Success,
|
||||
resultDisplay: {
|
||||
todos: [
|
||||
{ description: 'Pending Task', status: 'pending' },
|
||||
{ description: 'In Progress Task', status: 'cancelled' },
|
||||
{ description: 'Completed Task', status: 'completed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as HistoryItem;
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [historyWithNoInProgress],
|
||||
showFullTodos: false,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders only the in-progress task when full view is off', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [mockHistoryItem],
|
||||
showFullTodos: false,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the full todo list when full view is on', () => {
|
||||
const { lastFrame } = renderWithUiState({
|
||||
history: [mockHistoryItem],
|
||||
showFullTodos: true,
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
133
packages/cli/src/ui/components/messages/Todo.tsx
Normal file
133
packages/cli/src/ui/components/messages/Todo.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
type Todo,
|
||||
type TodoList,
|
||||
type TodoStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useMemo } from 'react';
|
||||
import type { HistoryItemToolGroup } from '../../types.js';
|
||||
|
||||
const TodoItemDisplay: React.FC<{ todo: Todo }> = ({ todo }) => (
|
||||
<Box flexDirection="row">
|
||||
<Box marginRight={1}>
|
||||
<TodoStatusDisplay status={todo.status} />
|
||||
</Box>
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.primary}>{todo.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Text color={theme.status.success}>✓</Text>;
|
||||
case 'in_progress':
|
||||
return <Text color={theme.text.accent}>»</Text>;
|
||||
case 'pending':
|
||||
return <Text color={theme.text.primary}>☐</Text>;
|
||||
case 'cancelled':
|
||||
default:
|
||||
return <Text color={theme.status.error}>✗</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
export const AnchoredTodoListDisplay: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
|
||||
const todos: TodoList | null = useMemo(() => {
|
||||
// Find the most recent todo list written by the WriteTodosTool
|
||||
for (let i = uiState.history.length - 1; i >= 0; i--) {
|
||||
const entry = uiState.history[i];
|
||||
if (entry.type !== 'tool_group') {
|
||||
continue;
|
||||
}
|
||||
const toolGroup = entry as HistoryItemToolGroup;
|
||||
for (const tool of toolGroup.tools) {
|
||||
if (
|
||||
typeof tool.resultDisplay !== 'object' ||
|
||||
!('todos' in tool.resultDisplay)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return tool.resultDisplay as TodoList;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [uiState.history]);
|
||||
|
||||
const inProgress: Todo | null = useMemo(() => {
|
||||
if (todos === null) {
|
||||
return null;
|
||||
}
|
||||
return todos.todos.find((todo) => todo.status === 'in_progress') || null;
|
||||
}, [todos]);
|
||||
|
||||
if (todos === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uiState.showFullTodos) {
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
borderBottom={false}
|
||||
flexDirection="column"
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Text color={theme.text.accent}>
|
||||
📝 Todo:
|
||||
<Text color={theme.text.secondary}>(ctrl+t to collapse)</Text>
|
||||
</Text>
|
||||
|
||||
<Box paddingLeft={4} paddingRight={2} paddingTop={1}>
|
||||
<TodoListDisplay todos={todos!} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (inProgress === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
borderBottom={false}
|
||||
flexDirection="row"
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Text color={theme.text.accent}>
|
||||
📝 Todo:
|
||||
<Text color={theme.text.secondary}>(ctrl+t to expand)</Text>
|
||||
</Text>
|
||||
<TodoItemDisplay todo={inProgress} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TodoListDisplayProps {
|
||||
todos: TodoList;
|
||||
}
|
||||
|
||||
export const TodoListDisplay: React.FC<TodoListDisplayProps> = ({ todos }) => (
|
||||
<Box flexDirection="column">
|
||||
{todos.todos.map((todo: Todo, index: number) => (
|
||||
<TodoItemDisplay todo={todo} key={index} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TodoListDisplay } from './TodoListDisplay.js';
|
||||
import type { TodoList, TodoStatus } from '@google/gemini-cli-core';
|
||||
|
||||
describe('<TodoListDisplay />', () => {
|
||||
const terminalWidth = 80;
|
||||
|
||||
it('renders an empty todo list correctly', () => {
|
||||
const todos: TodoList = { todos: [] };
|
||||
const { lastFrame } = render(
|
||||
<TodoListDisplay todos={todos} terminalWidth={terminalWidth} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a todo list with various statuses correctly', () => {
|
||||
const todos: TodoList = {
|
||||
todos: [
|
||||
{ description: 'Task 1', status: 'pending' as TodoStatus },
|
||||
{ description: 'Task 2', status: 'in_progress' as TodoStatus },
|
||||
{ description: 'Task 3', status: 'completed' as TodoStatus },
|
||||
{ description: 'Task 4', status: 'cancelled' as TodoStatus },
|
||||
],
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<TodoListDisplay todos={todos} terminalWidth={terminalWidth} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a todo list with long descriptions that wrap', () => {
|
||||
const todos: TodoList = {
|
||||
todos: [
|
||||
{
|
||||
description:
|
||||
'This is a very long description for a pending task that should wrap around multiple lines when the terminal width is constrained.',
|
||||
status: 'pending' as TodoStatus,
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Another completed task with an equally verbose description to test wrapping behavior.',
|
||||
status: 'completed' as TodoStatus,
|
||||
},
|
||||
],
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<TodoListDisplay todos={todos} terminalWidth={40} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a single todo item', () => {
|
||||
const todos: TodoList = {
|
||||
todos: [{ description: 'Single task', status: 'pending' as TodoStatus }],
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<TodoListDisplay todos={todos} terminalWidth={terminalWidth} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { Todo, TodoList, TodoStatus } from '@google/gemini-cli-core';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
export interface TodoListDisplayProps {
|
||||
todos: TodoList;
|
||||
terminalWidth: number;
|
||||
}
|
||||
const TodoStatusDisplay: React.FC<{ status: TodoStatus }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Text color={theme.status.success}>✓</Text>;
|
||||
case 'in_progress':
|
||||
return <Text color={theme.text.accent}>»</Text>;
|
||||
case 'pending':
|
||||
return <Text color={theme.text.primary}>☐</Text>;
|
||||
case 'cancelled':
|
||||
return <Text color={theme.status.error}>✗</Text>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const TodoListDisplay: React.FC<TodoListDisplayProps> = ({
|
||||
todos,
|
||||
terminalWidth,
|
||||
}) => (
|
||||
<Box flexDirection="column" width={terminalWidth}>
|
||||
{todos.todos.map((todo: Todo, index: number) => (
|
||||
<Box key={index} flexDirection="row">
|
||||
<Box marginRight={1}>
|
||||
<TodoStatusDisplay status={todo.status} />
|
||||
</Box>
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.primary}>{todo.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -13,7 +13,7 @@ import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { AnsiOutputText } from '../AnsiOutput.js';
|
||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { TodoListDisplay } from './TodoListDisplay.js';
|
||||
import { TodoListDisplay } from './Todo.js';
|
||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||
import {
|
||||
SHELL_COMMAND_NAME,
|
||||
@@ -173,10 +173,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
/>
|
||||
) : typeof resultDisplay === 'object' &&
|
||||
'todos' in resultDisplay ? (
|
||||
<TodoListDisplay
|
||||
todos={resultDisplay as TodoList}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
<TodoListDisplay todos={resultDisplay as TodoList} />
|
||||
) : (
|
||||
<AnsiOutputText
|
||||
data={resultDisplay as AnsiOutput}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<AnchoredTodoListDisplay /> > renders null when no todos are in the history 1`] = `""`;
|
||||
|
||||
exports[`<AnchoredTodoListDisplay /> > renders null when todos exist but none are in progress and full view is off 1`] = `""`;
|
||||
|
||||
exports[`<AnchoredTodoListDisplay /> > renders only the in-progress task when full view is off 1`] = `
|
||||
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📝 Todo:(ctrl+t to expand)» In Progress Task │"
|
||||
`;
|
||||
|
||||
exports[`<AnchoredTodoListDisplay /> > renders the full todo list when full view is on 1`] = `
|
||||
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📝 Todo:(ctrl+t to collapse) │
|
||||
│ │
|
||||
│ ☐ Pending Task │
|
||||
│ » In Progress Task │
|
||||
│ ✓ Completed Task │"
|
||||
`;
|
||||
|
||||
exports[`<TodoListDisplay /> > renders a single todo item 1`] = `"☐ Single task"`;
|
||||
|
||||
exports[`<TodoListDisplay /> > renders a todo list with long descriptions that wrap 1`] = `
|
||||
"☐ This is a very long
|
||||
description for a pending
|
||||
task that should wrap around
|
||||
multiple lines when the
|
||||
terminal width is
|
||||
constrained.
|
||||
✓ Another completed task with
|
||||
an equally verbose
|
||||
description to test wrapping
|
||||
behavior."
|
||||
`;
|
||||
|
||||
exports[`<TodoListDisplay /> > renders a todo list with various statuses correctly 1`] = `
|
||||
"☐ Task 1
|
||||
» Task 2
|
||||
✓ Task 3
|
||||
✗ Task 4"
|
||||
`;
|
||||
|
||||
exports[`<TodoListDisplay /> > renders an empty todo list correctly 1`] = `""`;
|
||||
@@ -120,6 +120,7 @@ export interface UIState {
|
||||
activePtyId: number | undefined;
|
||||
embeddedShellFocused: boolean;
|
||||
showDebugProfiler: boolean;
|
||||
showFullTodos: boolean;
|
||||
}
|
||||
|
||||
export const UIStateContext = createContext<UIState | null>(null);
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('keyMatchers', () => {
|
||||
key.ctrl && (key.name === 'x' || key.sequence === '\x18'),
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]: (key: Key) => key.ctrl && key.name === 'v',
|
||||
[Command.SHOW_ERROR_DETAILS]: (key: Key) => key.ctrl && key.name === 'o',
|
||||
[Command.SHOW_FULL_TODOS]: (key: Key) => key.ctrl && key.name === 't',
|
||||
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: (key: Key) =>
|
||||
key.ctrl && key.name === 'g',
|
||||
[Command.TOGGLE_MARKDOWN]: (key: Key) => key.meta && key.name === 'm',
|
||||
@@ -214,6 +215,11 @@ describe('keyMatchers', () => {
|
||||
positive: [createKey('o', { ctrl: true })],
|
||||
negative: [createKey('o'), createKey('e', { ctrl: true })],
|
||||
},
|
||||
{
|
||||
command: Command.SHOW_FULL_TODOS,
|
||||
positive: [createKey('t', { ctrl: true })],
|
||||
negative: [createKey('t'), createKey('e', { ctrl: true })],
|
||||
},
|
||||
{
|
||||
command: Command.TOGGLE_IDE_CONTEXT_DETAIL,
|
||||
positive: [createKey('g', { ctrl: true })],
|
||||
|
||||
Reference in New Issue
Block a user