Skip to content

Commit 5309e18

Browse files
gutsavqwencoder
andcommitted
feat(sidebar): Enhance terminal switching with active-only navigation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent a042d40 commit 5309e18

File tree

4 files changed

+215
-32
lines changed

4 files changed

+215
-32
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@ Install with your favorite plugin manager:
4444
- `:GeminiSend` - Send selected text to AI (use in visual mode)
4545
- `:GeminiSendLineDiagnostic` - Send line diagnostics to AI
4646
- `:GeminiSendFileDiagnostic` - Send file diagnostics to AI
47+
- `:GeminiSwitchSidebarStyle` - Switch the appearance/style of the sidebar
48+
49+
### Sidebar Terminal Switching
50+
When multiple commands are configured (e.g., both `gemini` and `qwen`), you can switch between active sidebar terminals using default key mappings:
51+
52+
- `<M-]>` - Switch to the next active terminal (Alt + ])
53+
- `<M-[>` - Switch to the previous active terminal (Alt + [)
54+
55+
To customize these key bindings, you can override the default mappings in your setup function:
56+
57+
```lua
58+
require("gemini").setup({
59+
cmds = { "gemini", "qwen" },
60+
-- Override default key mappings
61+
on_buf = function(buf)
62+
-- Add your own custom mappings
63+
vim.api.nvim_buf_set_keymap(buf, 't', '<Tab>',
64+
'<Cmd>lua require("gemini.ideSidebar").switchSidebar()<CR>',
65+
{ noremap = true, silent = true }
66+
)
67+
vim.api.nvim_buf_set_keymap(buf, 't', '<S-Tab>',
68+
'<Cmd>lua require("gemini.ideSidebar").switchSidebar("prev")<CR>',
69+
{ noremap = true, silent = true }
70+
)
71+
end
72+
})
73+
```
74+
75+
**Note:** Be aware that `<Tab>` and `<S-Tab>` may already be mapped to other functionality by the Gemini/Qwen CLIs themselves, so using these keys for terminal switching might interfere with their built-in features. Consider using alternative key bindings such as `<C-Tab>` and `<C-S-Tab>` or the default Alt-based mappings instead.
4776

4877
### Diff Management
4978
When AI suggests changes, you can:
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# v0.7.1 Release Announcement
2+
3+
## 🎉 Improvements
4+
5+
### Enhanced Sidebar Terminal Switching
6+
The sidebar terminal switching functionality has been improved with more reliable behavior:
7+
- Only switches between *active* terminals (no more spawning new processes when switching)
8+
- Supports forward and backward navigation between active terminals
9+
- Better safety checks to prevent errors when few terminals are active
10+
11+
## ⚠️ Breaking Changes
12+
13+
### Behavior Change
14+
- **Old**: `<Tab>` would hide current terminal and spawn/show the next one in the list
15+
- **New**: `<M-]>` (Alt+]) and `<M-[>` (Alt+[) only switch between already *active* terminals without spawning new processes
16+
17+
### Function Renamed
18+
- **Old**: `require('gemini.ideSidebar').switchTerms()`
19+
- **New**: `require('gemini.ideSidebar').switchSidebar()`
20+
21+
## 🔄 What You Need to Do
22+
23+
### If You're Using the Function Manually
24+
If you were calling `switchTerms()` manually in your configuration, update your code:
25+
26+
```lua
27+
-- Change from this:
28+
require('gemini.ideSidebar').switchTerms()
29+
30+
-- To this:
31+
require('gemini.ideSidebar').switchSidebar([direction])
32+
-- where direction can be 'next' (default) or 'prev'
33+
```
34+
35+
### For Key Binding Customization
36+
If you want to customize the terminal switching key bindings, see the updated README documentation which shows how to override the default Alt-based mappings.

lua/gemini/ideSidebar.lua

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -450,19 +450,34 @@ function ideSidebar.switchToCli(arg)
450450
end
451451
end
452452

453-
--- Switch to next sidebar terminal in command list
454-
-- Hide current terminal and open next one in list
455-
-- Used when multiple commands configured and Tab pressed in terminal mode
453+
--- Switch between active sidebar terminals
454+
-- Switch to the next or previous active terminal in the list
455+
-- Used when multiple commands are configured and key mappings are triggered
456+
-- @param direction string Optional, 'next' or 'prev', defaults to 'next'
456457
-- @return nil
457-
function ideSidebar.switchTerms()
458-
local opts = ideSidebarState.terminalOpts[ideSidebarState.lastActiveIdx]
459-
terminal.create(opts.cmd, opts):hide()
460-
ideSidebarState.lastActiveIdx = (
461-
ideSidebarState.lastActiveIdx % #ideSidebarState.terminalOpts
462-
) + 1
458+
function ideSidebar.switchSidebar(direction)
459+
local activeSidebarTermIds = {}
460+
local currentActiveIndex = -1
461+
for idx, opts in ipairs(ideSidebarState.terminalOpts) do
462+
local term = terminal.getActiveTerminals()[opts.id]
463+
if term then
464+
local activeInfo = { id = opts.id, idx = idx }
465+
table.insert(activeSidebarTermIds, activeInfo)
466+
-- Track the position of the currently active terminal in the active list
467+
if idx == ideSidebarState.lastActiveIdx then
468+
currentActiveIndex = #activeSidebarTermIds
469+
end
470+
end
471+
end
463472

464-
local nextOpts = ideSidebarState.terminalOpts[ideSidebarState.lastActiveIdx]
465-
terminal.create(nextOpts.cmd, nextOpts):show()
473+
-- If there are less than 2 active sidebar terminals, do nothing
474+
if #activeSidebarTermIds < 2 then return end
475+
direction = direction or 'next'
476+
local directionInt = (direction:lower() == 'prev' and -1 or 1)
477+
local switchIndex = (
478+
(currentActiveIndex - 1 + directionInt) % #activeSidebarTermIds
479+
) + 1
480+
ideSidebar.switchToCli('sidebar ' .. activeSidebarTermIds[switchIndex].idx)
466481
end
467482

468483
--- Toggle the sidebar terminal
@@ -615,7 +630,6 @@ function ideSidebar.setup(opts)
615630
for idx, cmd in ipairs(opts.cmds) do
616631
local termOpts =
617632
vim.tbl_deep_extend('force', vim.deepcopy(opts), { cmd = cmd })
618-
local onBuffer = termOpts.on_buf
619633
local name = string.format('%s-ngc-%d(%s)', cwdBase, idx, cmd)
620634

621635
termOpts.name = name
@@ -640,14 +654,25 @@ function ideSidebar.setup(opts)
640654
ideSidebar.createDeterministicId(termOpts.cmd, termOpts.env, idx)
641655

642656
if #opts.cmds > 1 then
657+
-- Add default keymaps for switching between active sidebar terminals
658+
-- Users can define their own on_buf function to override these keymaps
659+
local onBuffer = termOpts.on_buf
643660
termOpts.on_buf = function(buf)
644661
vim.api.nvim_buf_set_keymap(
645662
buf,
646663
't',
647-
'<Tab>',
648-
'<Cmd>lua require("gemini.ideSidebar").switchTerms()<CR>', -- Corrected escaping for nested quotes
664+
'<M-]>',
665+
'<Cmd>lua require("gemini.ideSidebar").switchSidebar()<CR>',
666+
{ noremap = true, silent = true }
667+
)
668+
vim.api.nvim_buf_set_keymap(
669+
buf,
670+
't',
671+
'<M-[>',
672+
'<Cmd>lua require("gemini.ideSidebar").switchSidebar("prev")<CR>',
649673
{ noremap = true, silent = true }
650674
)
675+
-- Call user's custom on_buf function if provided
651676
if type(onBuffer) == 'function' then onBuffer(buf) end
652677
end
653678
end

tests/ideSidebar_spec.lua

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ describe('ideSidebar', function()
211211
end)
212212
end)
213213

214-
describe('switchTerms method', function()
215-
it('should hide current terminal and show next one', function()
214+
describe('switchSidebar method', function()
215+
it('should switch to the next active terminal', function()
216216
-- Setup multiple commands to have multiple terminals to switch between
217217
ideSidebar.setup({ cmds = { 'gemini', 'qwen' }, port = 12345 })
218218

@@ -228,8 +228,8 @@ describe('ideSidebar', function()
228228
TERM_PROGRAM = 'vscode',
229229
}
230230

231-
local geminiId = ideSidebar.createDeterministicId('gemini', geminiEnv)
232-
local qwenId = ideSidebar.createDeterministicId('qwen', qwenEnv)
231+
local geminiId = ideSidebar.createDeterministicId('gemini', geminiEnv, 1)
232+
local qwenId = ideSidebar.createDeterministicId('qwen', qwenEnv, 2)
233233

234234
local geminiTerm = {
235235
hide = spy.new(function() end),
@@ -247,22 +247,115 @@ describe('ideSidebar', function()
247247

248248
terminalMock.getActiveTerminals = function() return activeTerminals end
249249

250-
-- Mock terminal.create to return the correct terminals
251-
local originalCreate = terminalMock.create
252-
terminalMock.create = spy.new(function(cmd, config)
253-
if cmd == 'gemini' then
254-
return geminiTerm
255-
else
256-
return qwenTerm
257-
end
258-
end)
250+
-- Spy on switchToCli to verify it's called with the correct parameters
251+
local switchToCliSpy = spy.new(function() end)
252+
local originalSwitchToCli = ideSidebar.switchToCli
253+
ideSidebar.switchToCli = switchToCliSpy
254+
255+
-- Call switchSidebar (with default 'next' direction)
256+
ideSidebar.switchSidebar()
257+
258+
-- Check that switchToCli was called with the correct parameters
259+
assert.spy(switchToCliSpy).was.called(1)
260+
local args = switchToCliSpy.calls[1]
261+
assert.truthy(string.find(args.vals[1] or '', 'sidebar'))
262+
263+
-- Restore original function
264+
ideSidebar.switchToCli = originalSwitchToCli
265+
end)
266+
267+
it(
268+
'should switch to the previous active terminal when direction is prev',
269+
function()
270+
-- Setup multiple commands to have multiple terminals to switch between
271+
ideSidebar.setup({ cmds = { 'gemini', 'qwen' }, port = 12345 })
272+
273+
-- Create mock terminals
274+
local geminiEnv = {
275+
GEMINI_CLI_IDE_WORKSPACE_PATH = '/test/dir',
276+
GEMINI_CLI_IDE_SERVER_PORT = '12345',
277+
TERM_PROGRAM = 'vscode',
278+
}
279+
local qwenEnv = {
280+
QWEN_CODE_IDE_WORKSPACE_PATH = '/test/dir',
281+
QWEN_CODE_IDE_SERVER_PORT = '12345',
282+
TERM_PROGRAM = 'vscode',
283+
}
284+
285+
local geminiId =
286+
ideSidebar.createDeterministicId('gemini', geminiEnv, 1)
287+
local qwenId = ideSidebar.createDeterministicId('qwen', qwenEnv, 2)
288+
289+
local geminiTerm = {
290+
hide = spy.new(function() end),
291+
show = spy.new(function() end),
292+
}
293+
294+
local qwenTerm = {
295+
hide = spy.new(function() end),
296+
show = spy.new(function() end),
297+
}
298+
299+
local activeTerminals = {}
300+
activeTerminals[geminiId] = geminiTerm
301+
activeTerminals[qwenId] = qwenTerm
259302

260-
-- Call switchTerms
261-
ideSidebar.switchTerms()
303+
terminalMock.getActiveTerminals = function() return activeTerminals end
262304

263-
-- Check that hide was called on the first terminal and show on the second
264-
assert.spy(geminiTerm.hide).was.called(1)
265-
assert.spy(qwenTerm.show).was.called(1)
305+
-- Spy on switchToCli to verify it's called with the correct parameters
306+
local switchToCliSpy = spy.new(function() end)
307+
local originalSwitchToCli = ideSidebar.switchToCli
308+
ideSidebar.switchToCli = switchToCliSpy
309+
310+
-- Call switchSidebar with 'prev' direction
311+
ideSidebar.switchSidebar('prev')
312+
313+
-- Check that switchToCli was called with the correct parameters
314+
assert.spy(switchToCliSpy).was.called(1)
315+
local args = switchToCliSpy.calls[1]
316+
assert.truthy(string.find(args.vals[1] or '', 'sidebar'))
317+
318+
-- Restore original function
319+
ideSidebar.switchToCli = originalSwitchToCli
320+
end
321+
)
322+
323+
it('should not switch if there are less than 2 active terminals', function()
324+
-- Setup multiple commands to have multiple terminals but only one active
325+
ideSidebar.setup({ cmds = { 'gemini', 'qwen' }, port = 12345 })
326+
327+
-- Create mock terminal for only one command
328+
local geminiEnv = {
329+
GEMINI_CLI_IDE_WORKSPACE_PATH = '/test/dir',
330+
GEMINI_CLI_IDE_SERVER_PORT = '12345',
331+
TERM_PROGRAM = 'vscode',
332+
}
333+
334+
local geminiId = ideSidebar.createDeterministicId('gemini', geminiEnv)
335+
336+
local geminiTerm = {
337+
hide = spy.new(function() end),
338+
show = spy.new(function() end),
339+
}
340+
341+
local activeTerminals = {}
342+
activeTerminals[geminiId] = geminiTerm
343+
344+
terminalMock.getActiveTerminals = function() return activeTerminals end
345+
346+
-- Spy on switchToCli to verify it's not called when less than 2 terminals are active
347+
local switchToCliSpy = spy.new(function() end)
348+
local originalSwitchToCli = ideSidebar.switchToCli
349+
ideSidebar.switchToCli = switchToCliSpy
350+
351+
-- Call switchSidebar
352+
ideSidebar.switchSidebar()
353+
354+
-- Check that switchToCli was not called because only one terminal is active
355+
assert.spy(switchToCliSpy).was_not_called()
356+
357+
-- Restore original function
358+
ideSidebar.switchToCli = originalSwitchToCli
266359
end)
267360
end)
268361

0 commit comments

Comments
 (0)