From 4d3eb56c21fa78d0b581c1cdd4a19829047abe89 Mon Sep 17 00:00:00 2001 From: cutelisp Date: Mon, 14 Jul 2025 15:21:20 +0100 Subject: [PATCH 1/7] Add MoveTab Function This commits add a new function `(tl *TabList) MoveTab(t *Tab, i int)` `MoveTab` moves the specified tab to the given index in TabList.List --- internal/action/tab.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/action/tab.go b/internal/action/tab.go index 076df5f801..c2275277cc 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -75,6 +75,19 @@ func (t *TabList) RemoveTab(id uint64) { } } +// MoveTab moves the specified tab to the given index +func (tl *TabList) MoveTab(t *Tab, i int) { + if i == tl.Active() || i < 0 || i >= len(tl.List) { + return + } + tl.RemoveTab(t.Panes[0].ID()) + tl.List = append(tl.List, nil) + copy(tl.List[i+1:], tl.List[i:]) + tl.List[i] = t + tl.Resize() + tl.UpdateNames() +} + // Resize resizes all elements within the tab list // One thing to note is that when there is only 1 tab // the tab bar should not be drawn so resizing must take From 5718281dec061cee529bf3c3537549162308d9a7 Mon Sep 17 00:00:00 2001 From: cutelisp Date: Mon, 14 Jul 2025 16:43:37 +0100 Subject: [PATCH 2/7] Refactor TabMoveCmd() - Simplified the overall logic. - Improved error messages. - Moved tab movement responsibility to a `(tl *TabList) MoveTab` call - Enhanced the function description. --- internal/action/command.go | 50 +++++++++----------------------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/internal/action/command.go b/internal/action/command.go index 9bf979b995..a249610477 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -160,55 +160,27 @@ func (h *BufPane) TextFilterCmd(args []string) { } } -// TabMoveCmd moves the current tab to a given index (starts at 1). The -// displaced tabs are moved up. +// TabMoveCmd moves the current tab to a given index (starting at 1), +// or by a relative offset using +N/-N. Displaced tabs shift accordingly. func (h *BufPane) TabMoveCmd(args []string) { - if len(args) <= 0 { - InfoBar.Error("Not enough arguments: provide an index, starting at 1") - return - } - - if len(args[0]) <= 0 { - InfoBar.Error("Invalid argument: empty string") + if len(args) < 1 { + InfoBar.Error("Not enough arguments: provide a tab position") return } - num, err := strconv.Atoi(args[0]) + i, err := strconv.Atoi(args[0]) if err != nil { - InfoBar.Error("Invalid argument: ", err) + InfoBar.Error("Invalid tab position") return } - // Preserve sign for relative move, if one exists - var shiftDirection byte - if strings.Contains("-+", string([]byte{args[0][0]})) { - shiftDirection = args[0][0] - } - - // Relative positions -> absolute positions - idxFrom := Tabs.Active() - idxTo := 0 - offset := util.Abs(num) - if shiftDirection == '-' { - idxTo = idxFrom - offset - } else if shiftDirection == '+' { - idxTo = idxFrom + offset + if strings.ContainsRune("-+", rune(args[0][0])) { + i = util.Clamp(i+Tabs.Active(), 0, len(Tabs.List)-1) } else { - idxTo = offset - 1 + i = util.Clamp(i-1, 0, len(Tabs.List)-1) } - - // Restrain position to within the valid range - idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1) - - activeTab := Tabs.List[idxFrom] - Tabs.RemoveTab(activeTab.Panes[0].ID()) - Tabs.List = append(Tabs.List, nil) - copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:]) - Tabs.List[idxTo] = activeTab - Tabs.Resize() - Tabs.UpdateNames() - Tabs.SetActive(idxTo) - // InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1)) + Tabs.MoveTab(MainTab(), i) + Tabs.SetActive(i) } // TabSwitchCmd switches to a given tab either by name or by number From 7ab907b76ccc403ba36ab641cd2d0d3ea75a34bc Mon Sep 17 00:00:00 2001 From: cutelisp Date: Mon, 14 Jul 2025 17:04:33 +0100 Subject: [PATCH 3/7] Refactor TabList.HandleEvent() - Now `Tablist`tracks whether a mouse is released and whether last mouse click occurred on the tab bar. - Prevents any action from propagating to the active tab if the mouse is held from an initial tab bar click. - Explicitly disables all dragging on the tab bar. ref: #3417 --- internal/action/tab.go | 58 ++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/internal/action/tab.go b/internal/action/tab.go index c2275277cc..2acc039777 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -17,6 +17,11 @@ import ( type TabList struct { *display.TabWindow List []*Tab + + // captures whether the mouse is released + release bool + // captures whether the last mouse click occurred on the tab bar + tbClick bool } // NewTabList creates a TabList from a list of buffers by creating a Tab @@ -35,6 +40,7 @@ func NewTabList(bufs []*buffer.Buffer) *TabList { } tl.TabWindow = display.NewTabWindow(w, 0) tl.Names = make([]string, len(bufs)) + tl.release = true return tl } @@ -118,40 +124,48 @@ func (t *TabList) HandleEvent(event tcell.Event) { t.Resize() case *tcell.EventMouse: mx, my := e.Position() - switch e.Buttons() { - case tcell.Button1: - if my == t.Y && len(t.List) > 1 { - if mx == 0 { + if e.Buttons() == tcell.ButtonNone { + t.release = true + t.tbClick = false + if t.List[t.Active()].release { + // Mouse release received, while already released + t.ResetMouse() + return + } + } else if my == t.Y && len(t.List) > 1 { + switch e.Buttons() { + case tcell.Button1: + if !t.release { + // Tab bar dragging + return + } + t.release = false + t.tbClick = true + switch mx { + case 0: t.Scroll(-4) - } else if mx == t.Width-1 { + case t.Width - 1: t.Scroll(4) - } else { + default: ind := t.LocFromVisual(buffer.Loc{mx, my}) if ind != -1 { t.SetActive(ind) } } - return - } - case tcell.ButtonNone: - if t.List[t.Active()].release { - // Mouse release received, while already released - t.ResetMouse() - return - } - case tcell.WheelUp: - if my == t.Y && len(t.List) > 1 { + case tcell.WheelUp: t.Scroll(4) - return - } - case tcell.WheelDown: - if my == t.Y && len(t.List) > 1 { + case tcell.WheelDown: t.Scroll(-4) - return } + return + } else if t.release { + // Click outside tab bar + t.release = false } } - t.List[t.Active()].HandleEvent(event) + if t.release || !t.tbClick { + t.List[t.Active()].HandleEvent(event) + } } // Display updates the names and then displays the tab bar From bbaf5230dc508e4c30fd48587c62451a85cab4e8 Mon Sep 17 00:00:00 2001 From: cutelisp Date: Mon, 14 Jul 2025 20:08:18 +0100 Subject: [PATCH 4/7] New Tab Bar Feature: Add Tab on Mouse Middle Click Adds a new tab if the tab bar is middle mouse clicked in an empty spot (no tab), and sets it as active. --- internal/action/tab.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/action/tab.go b/internal/action/tab.go index 2acc039777..20a2d1b6b9 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -152,6 +152,17 @@ func (t *TabList) HandleEvent(event tcell.Event) { t.SetActive(ind) } } + case tcell.Button3: + if !t.release { + // Tab bar dragging + return + } + t.release = false + t.tbClick = true + + if i := t.LocFromVisual(buffer.Loc{mx, my}); i == -1 { + t.List[t.Active()].CurPane().AddTab() + } case tcell.WheelUp: t.Scroll(4) case tcell.WheelDown: From 22ef9e9a48492729557ce5f1e50b126066ff63c5 Mon Sep 17 00:00:00 2001 From: cutelisp Date: Tue, 15 Jul 2025 06:09:56 +0100 Subject: [PATCH 5/7] New Tab Bar Feature: Drag Tabs --- internal/action/tab.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/internal/action/tab.go b/internal/action/tab.go index 20a2d1b6b9..278a8d4a20 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -22,6 +22,8 @@ type TabList struct { release bool // captures whether the last mouse click occurred on the tab bar tbClick bool + // captures whether a tab is being dragged within the tab bar + tabDrag bool } // NewTabList creates a TabList from a list of buffers by creating a Tab @@ -127,6 +129,7 @@ func (t *TabList) HandleEvent(event tcell.Event) { if e.Buttons() == tcell.ButtonNone { t.release = true t.tbClick = false + t.tabDrag = false if t.List[t.Active()].release { // Mouse release received, while already released t.ResetMouse() @@ -135,10 +138,11 @@ func (t *TabList) HandleEvent(event tcell.Event) { } else if my == t.Y && len(t.List) > 1 { switch e.Buttons() { case tcell.Button1: - if !t.release { - // Tab bar dragging + if !t.release && !t.tabDrag { + // Invalid tab bar dragging return } + isDrag := !t.release t.release = false t.tbClick = true switch mx { @@ -147,9 +151,20 @@ func (t *TabList) HandleEvent(event tcell.Event) { case t.Width - 1: t.Scroll(4) default: - ind := t.LocFromVisual(buffer.Loc{mx, my}) - if ind != -1 { - t.SetActive(ind) + i := t.LocFromVisual(buffer.Loc{mx, my}) + if i != -1 { + t.tabDrag = true + if i != t.Active() { + if isDrag { + t.MoveTab(t.List[t.Active()], i) + } + t.SetActive(i) + } + } else if isDrag { + if i = len(t.List) - 1; i != t.Active() { + t.MoveTab(t.List[t.Active()], i) + t.SetActive(i) + } } } case tcell.Button3: From b70df27a46687a049357494ff54472fdf7b98d4e Mon Sep 17 00:00:00 2001 From: cutelisp Date: Tue, 15 Jul 2025 07:06:05 +0100 Subject: [PATCH 6/7] New Tab Bar Feature: Add New Tab on Double Click Whenever a non-tab double-click occurs within the tab bar, a new tab is added and set as active. - `TabList.tbLastClick` holds the timestamp of the last non-tab click within the tab bar - After adding a new tab, `TabList.tbLastClick` is reset to its zero value to avoid chaining unintended double-clicks --- internal/action/tab.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/action/tab.go b/internal/action/tab.go index 278a8d4a20..29311af083 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -2,6 +2,7 @@ package action import ( luar "layeh.com/gopher-luar" + "time" "github.com/micro-editor/tcell/v2" "github.com/zyedidia/micro/v2/internal/buffer" @@ -24,6 +25,8 @@ type TabList struct { tbClick bool // captures whether a tab is being dragged within the tab bar tabDrag bool + // timestamp of the last non-tab click within the tab bar + tbLastClick time.Time } // NewTabList creates a TabList from a list of buffers by creating a Tab @@ -160,10 +163,19 @@ func (t *TabList) HandleEvent(event tcell.Event) { } t.SetActive(i) } - } else if isDrag { - if i = len(t.List) - 1; i != t.Active() { - t.MoveTab(t.List[t.Active()], i) - t.SetActive(i) + } else { + if isDrag { + if i = len(t.List) - 1; i != t.Active() { + t.MoveTab(t.List[t.Active()], i) + t.SetActive(i) + } + } else { + if time.Since(t.tbLastClick)/time.Millisecond < config.DoubleClickThreshold { + t.List[t.Active()].CurPane().AddTab() + t.tbLastClick = time.Time{} + } else { + t.tbLastClick = time.Now() + } } } } From 5d5326c546360bfdc56e7a7ef52297849662a984 Mon Sep 17 00:00:00 2001 From: cutelisp Date: Wed, 16 Jul 2025 23:31:57 +0100 Subject: [PATCH 7/7] New Tab Bar Feature: Remove Tab on Mouse Middle Click Removes a tab when middle-clicked. If the tab contains at least one dirty buffer, a prompt is shown. If the user cancels the prompt, they remain on the tab; otherwise, the previously active tab is restored. --- internal/action/tab.go | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/internal/action/tab.go b/internal/action/tab.go index 29311af083..87d41ccd03 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -189,6 +189,55 @@ func (t *TabList) HandleEvent(event tcell.Event) { if i := t.LocFromVisual(buffer.Loc{mx, my}); i == -1 { t.List[t.Active()].CurPane().AddTab() + } else { + anyModified := false + for _, p := range t.List[i].Panes { + if bp, ok := p.(*BufPane); ok { + if bp.Buf.Modified() { + anyModified = true + break + } + } + } + + removeTab := func() { + panes := append([]Pane(nil), t.List[i].Panes...) + for _, p := range panes { + switch t := p.(type) { + case *BufPane: + t.ForceQuit() + case *RawPane: + t.Quit() + case *TermPane: + t.Quit() + } + } + } + + a := t.Active() + if anyModified { + t.SetActive(i) + InfoBar.YNPrompt("Discard unsaved changes? (y,n,esc)", func(yes, canceled bool) { + if !canceled { + if yes { + removeTab() + if i <= a { + a-- + } + } + t.SetActive(a) + } + }) + t.release = true + t.tbClick = false + t.tabDrag = false + } else { + removeTab() + if i <= a { + t.SetActive(a - 1) + } + } + return } case tcell.WheelUp: t.Scroll(4)