mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 18:24:21 +01:00
feat: show current git branch in status bar, and make it responsive (#1339)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
This commit is contained in:
@@ -101,8 +101,9 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tuiModel := tui.NewModel(app_).(*tui.Model)
|
||||||
program := tea.NewProgram(
|
program := tea.NewProgram(
|
||||||
tui.NewModel(app_),
|
tuiModel,
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
tea.WithMouseCellMotion(),
|
tea.WithMouseCellMotion(),
|
||||||
)
|
)
|
||||||
@@ -132,6 +133,7 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
sig := <-sigChan
|
sig := <-sigChan
|
||||||
slog.Info("Received signal, shutting down gracefully", "signal", sig)
|
slog.Info("Received signal, shutting down gracefully", "signal", sig)
|
||||||
|
tuiModel.Cleanup()
|
||||||
program.Quit()
|
program.Quit()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -141,5 +143,6 @@ func main() {
|
|||||||
slog.Error("TUI error", "error", err)
|
slog.Error("TUI error", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tuiModel.Cleanup()
|
||||||
slog.Info("TUI exited", "result", result)
|
slog.Info("TUI exited", "result", result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ go 1.24.0
|
|||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/alecthomas/chroma/v2 v2.18.0
|
github.com/alecthomas/chroma/v2 v2.18.0
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
|
||||||
github.com/charmbracelet/glamour v0.10.0
|
github.com/charmbracelet/glamour v0.10.0
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
|
||||||
github.com/charmbracelet/x/ansi v0.9.3
|
github.com/charmbracelet/x/ansi v0.9.3
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lithammer/fuzzysearch v1.1.8
|
github.com/lithammer/fuzzysearch v1.1.8
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||||
@@ -37,7 +37,6 @@ require (
|
|||||||
github.com/charmbracelet/x/input v0.3.7 // indirect
|
github.com/charmbracelet/x/input v0.3.7 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.1 // indirect
|
github.com/charmbracelet/x/windows v0.2.1 // indirect
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
|
||||||
github.com/getkin/kin-openapi v0.127.0 // indirect
|
github.com/getkin/kin-openapi v0.127.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
|
|||||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=
|
||||||
|
|||||||
@@ -2,42 +2,63 @@ package status
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
"github.com/sst/opencode/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GitBranchUpdatedMsg struct {
|
||||||
|
Branch string
|
||||||
|
}
|
||||||
|
|
||||||
type StatusComponent interface {
|
type StatusComponent interface {
|
||||||
tea.Model
|
tea.Model
|
||||||
tea.ViewModel
|
tea.ViewModel
|
||||||
|
Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
type statusComponent struct {
|
type statusComponent struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
width int
|
width int
|
||||||
cwd string
|
cwd string
|
||||||
|
branch string
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
done chan struct{}
|
||||||
|
lastUpdate time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusComponent) Init() tea.Cmd {
|
func (m *statusComponent) Init() tea.Cmd {
|
||||||
return nil
|
return m.startGitWatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case GitBranchUpdatedMsg:
|
||||||
|
if m.branch != msg.Branch {
|
||||||
|
m.branch = msg.Branch
|
||||||
|
}
|
||||||
|
// Continue watching for changes (persistent watcher)
|
||||||
|
return m, m.watchForGitChanges()
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusComponent) logo() string {
|
func (m *statusComponent) logo() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
|
base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
|
||||||
emphasis := styles.NewStyle().
|
emphasis := styles.NewStyle().
|
||||||
@@ -47,23 +68,56 @@ func (m statusComponent) logo() string {
|
|||||||
Render
|
Render
|
||||||
|
|
||||||
open := base("open")
|
open := base("open")
|
||||||
code := emphasis("code ")
|
code := emphasis("code")
|
||||||
version := base(m.app.Version)
|
version := base(" " + m.app.Version)
|
||||||
|
|
||||||
|
content := open + code
|
||||||
|
if m.width > 40 {
|
||||||
|
content += version
|
||||||
|
}
|
||||||
return styles.NewStyle().
|
return styles.NewStyle().
|
||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Render(open + code + version)
|
Render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusComponent) View() string {
|
func (m *statusComponent) collapsePath(path string, maxWidth int) string {
|
||||||
|
if lipgloss.Width(path) <= maxWidth {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
const ellipsis = ".."
|
||||||
|
ellipsisLen := len(ellipsis)
|
||||||
|
|
||||||
|
if maxWidth <= ellipsisLen {
|
||||||
|
if maxWidth > 0 {
|
||||||
|
return "..."[:maxWidth]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
separator := string(filepath.Separator)
|
||||||
|
parts := strings.Split(path, separator)
|
||||||
|
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return path[:maxWidth-ellipsisLen] + ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
truncatedPath := parts[len(parts)-1]
|
||||||
|
for i := len(parts) - 2; i >= 0; i-- {
|
||||||
|
part := parts[i]
|
||||||
|
if len(truncatedPath)+len(separator)+len(part)+ellipsisLen > maxWidth {
|
||||||
|
return ellipsis + separator + truncatedPath
|
||||||
|
}
|
||||||
|
truncatedPath = part + separator + truncatedPath
|
||||||
|
}
|
||||||
|
return truncatedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *statusComponent) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
logo := m.logo()
|
logo := m.logo()
|
||||||
|
logoWidth := lipgloss.Width(logo)
|
||||||
cwd := styles.NewStyle().
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Background(t.BackgroundPanel()).
|
|
||||||
Padding(0, 1).
|
|
||||||
Render(m.cwd)
|
|
||||||
|
|
||||||
var modeBackground compat.AdaptiveColor
|
var modeBackground compat.AdaptiveColor
|
||||||
var modeForeground compat.AdaptiveColor
|
var modeForeground compat.AdaptiveColor
|
||||||
@@ -113,28 +167,182 @@ func (m statusComponent) View() string {
|
|||||||
BorderBackground(t.BackgroundPanel()).
|
BorderBackground(t.BackgroundPanel()).
|
||||||
Render(mode)
|
Render(mode)
|
||||||
|
|
||||||
mode = styles.NewStyle().
|
faintStyle := styles.NewStyle().
|
||||||
Faint(true).
|
Faint(true).
|
||||||
Background(t.BackgroundPanel()).
|
Background(t.BackgroundPanel()).
|
||||||
|
Foreground(t.TextMuted())
|
||||||
|
mode = faintStyle.Render(key+" ") + mode
|
||||||
|
modeWidth := lipgloss.Width(mode)
|
||||||
|
|
||||||
|
availableWidth := m.width - logoWidth - modeWidth
|
||||||
|
branchSuffix := ""
|
||||||
|
if m.branch != "" {
|
||||||
|
branchSuffix = ":" + m.branch
|
||||||
|
}
|
||||||
|
|
||||||
|
maxCwdWidth := availableWidth - lipgloss.Width(branchSuffix)
|
||||||
|
cwdDisplay := m.collapsePath(m.cwd, maxCwdWidth)
|
||||||
|
|
||||||
|
if m.branch != "" && availableWidth > lipgloss.Width(cwdDisplay)+lipgloss.Width(branchSuffix) {
|
||||||
|
cwdDisplay += faintStyle.Render(branchSuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd := styles.NewStyle().
|
||||||
Foreground(t.TextMuted()).
|
Foreground(t.TextMuted()).
|
||||||
Render(key+" ") +
|
Background(t.BackgroundPanel()).
|
||||||
mode
|
Padding(0, 1).
|
||||||
|
Render(cwdDisplay)
|
||||||
|
|
||||||
space := max(
|
background := t.BackgroundPanel()
|
||||||
0,
|
status := layout.Render(
|
||||||
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(mode),
|
layout.FlexOptions{
|
||||||
|
Background: &background,
|
||||||
|
Direction: layout.Row,
|
||||||
|
Justify: layout.JustifySpaceBetween,
|
||||||
|
Align: layout.AlignStretch,
|
||||||
|
Width: m.width,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: logo + cwd,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: mode,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
|
||||||
|
|
||||||
status := logo + cwd + spacer + mode
|
|
||||||
|
|
||||||
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
|
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
|
||||||
return blank + "\n" + status
|
return blank + "\n" + status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *statusComponent) startGitWatcher() tea.Cmd {
|
||||||
|
cmd := util.CmdHandler(
|
||||||
|
GitBranchUpdatedMsg{Branch: getCurrentGitBranch(m.app.Info.Path.Root)},
|
||||||
|
)
|
||||||
|
if err := m.initWatcher(); err != nil {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
return tea.Batch(cmd, m.watchForGitChanges())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *statusComponent) initWatcher() error {
|
||||||
|
gitDir := filepath.Join(m.app.Info.Path.Root, ".git")
|
||||||
|
headFile := filepath.Join(gitDir, "HEAD")
|
||||||
|
if info, err := os.Stat(gitDir); err != nil || !info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := watcher.Add(headFile); err != nil {
|
||||||
|
watcher.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also watch the ref file if HEAD points to a ref
|
||||||
|
refFile := getGitRefFile(m.app.Info.Path.Cwd)
|
||||||
|
if refFile != headFile && refFile != "" {
|
||||||
|
if _, err := os.Stat(refFile); err == nil {
|
||||||
|
watcher.Add(refFile) // Ignore error, HEAD watching is sufficient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.watcher = watcher
|
||||||
|
m.done = make(chan struct{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *statusComponent) watchForGitChanges() tea.Cmd {
|
||||||
|
if m.watcher == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tea.Cmd(func() tea.Msg {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-m.watcher.Events:
|
||||||
|
branch := getCurrentGitBranch(m.app.Info.Path.Root)
|
||||||
|
if !ok {
|
||||||
|
return GitBranchUpdatedMsg{Branch: branch}
|
||||||
|
}
|
||||||
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
||||||
|
// Debounce updates to prevent excessive refreshes
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(m.lastUpdate) < 100*time.Millisecond {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.lastUpdate = now
|
||||||
|
if strings.HasSuffix(event.Name, "HEAD") {
|
||||||
|
m.updateWatchedFiles()
|
||||||
|
}
|
||||||
|
return GitBranchUpdatedMsg{Branch: branch}
|
||||||
|
}
|
||||||
|
case <-m.watcher.Errors:
|
||||||
|
// Continue watching even on errors
|
||||||
|
case <-m.done:
|
||||||
|
return GitBranchUpdatedMsg{Branch: ""}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *statusComponent) updateWatchedFiles() {
|
||||||
|
if m.watcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refFile := getGitRefFile(m.app.Info.Path.Root)
|
||||||
|
headFile := filepath.Join(m.app.Info.Path.Root, ".git", "HEAD")
|
||||||
|
if refFile != headFile && refFile != "" {
|
||||||
|
if _, err := os.Stat(refFile); err == nil {
|
||||||
|
// Try to add the new ref file (ignore error if already watching)
|
||||||
|
m.watcher.Add(refFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentGitBranch(cwd string) string {
|
||||||
|
cmd := exec.Command("git", "branch", "--show-current")
|
||||||
|
cmd.Dir = cwd
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGitRefFile(cwd string) string {
|
||||||
|
headFile := filepath.Join(cwd, ".git", "HEAD")
|
||||||
|
content, err := os.ReadFile(headFile)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
headContent := strings.TrimSpace(string(content))
|
||||||
|
if after, ok := strings.CutPrefix(headContent, "ref: "); ok {
|
||||||
|
// HEAD points to a ref file
|
||||||
|
refPath := after
|
||||||
|
return filepath.Join(cwd, ".git", refPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEAD contains a direct commit hash
|
||||||
|
return headFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *statusComponent) Cleanup() {
|
||||||
|
if m.done != nil {
|
||||||
|
close(m.done)
|
||||||
|
}
|
||||||
|
if m.watcher != nil {
|
||||||
|
m.watcher.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewStatusCmp(app *app.App) StatusComponent {
|
func NewStatusCmp(app *app.App) StatusComponent {
|
||||||
statusComponent := &statusComponent{
|
statusComponent := &statusComponent{
|
||||||
app: app,
|
app: app,
|
||||||
|
lastUpdate: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
homePath, err := os.UserHomeDir()
|
homePath, err := os.UserHomeDir()
|
||||||
|
|||||||
100
packages/tui/internal/components/status/status_test.go
Normal file
100
packages/tui/internal/components/status/status_test.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCurrentGitBranch(t *testing.T) {
|
||||||
|
// Test in current directory (should be a git repo)
|
||||||
|
branch := getCurrentGitBranch(".")
|
||||||
|
if branch == "" {
|
||||||
|
t.Skip("Not in a git repository, skipping test")
|
||||||
|
}
|
||||||
|
t.Logf("Current branch: %s", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGitRefFile(t *testing.T) {
|
||||||
|
// Create a temporary git directory structure for testing
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
gitDir := filepath.Join(tmpDir, ".git")
|
||||||
|
err := os.MkdirAll(gitDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 1: HEAD points to a ref
|
||||||
|
headFile := filepath.Join(gitDir, "HEAD")
|
||||||
|
err = os.WriteFile(headFile, []byte("ref: refs/heads/main\n"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refFile := getGitRefFile(tmpDir)
|
||||||
|
expected := filepath.Join(gitDir, "refs", "heads", "main")
|
||||||
|
if refFile != expected {
|
||||||
|
t.Errorf("Expected %s, got %s", expected, refFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 2: HEAD contains a direct commit hash
|
||||||
|
err = os.WriteFile(headFile, []byte("abc123def456\n"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refFile = getGitRefFile(tmpDir)
|
||||||
|
if refFile != headFile {
|
||||||
|
t.Errorf("Expected %s, got %s", headFile, refFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileWatcherIntegration(t *testing.T) {
|
||||||
|
// This test requires being in a git repository
|
||||||
|
if getCurrentGitBranch(".") == "" {
|
||||||
|
t.Skip("Not in a git repository, skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the file watcher setup doesn't crash
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
gitDir := filepath.Join(tmpDir, ".git")
|
||||||
|
err := os.MkdirAll(gitDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headFile := filepath.Join(gitDir, "HEAD")
|
||||||
|
err = os.WriteFile(headFile, []byte("ref: refs/heads/main\n"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the refs directory and file
|
||||||
|
refsDir := filepath.Join(gitDir, "refs", "heads")
|
||||||
|
err = os.MkdirAll(refsDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRef := filepath.Join(refsDir, "main")
|
||||||
|
err = os.WriteFile(mainRef, []byte("abc123def456\n"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can create a watcher without crashing
|
||||||
|
// This is a basic smoke test
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Test passed - no crash
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Error("Test timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,12 +71,11 @@ type Model struct {
|
|||||||
symbolsProvider completions.CompletionProvider
|
symbolsProvider completions.CompletionProvider
|
||||||
showCompletionDialog bool
|
showCompletionDialog bool
|
||||||
leaderBinding *key.Binding
|
leaderBinding *key.Binding
|
||||||
// isLeaderSequence bool
|
toastManager *toast.ToastManager
|
||||||
toastManager *toast.ToastManager
|
interruptKeyState InterruptKeyState
|
||||||
interruptKeyState InterruptKeyState
|
exitKeyState ExitKeyState
|
||||||
exitKeyState ExitKeyState
|
messagesRight bool
|
||||||
messagesRight bool
|
fileViewer fileviewer.Model
|
||||||
fileViewer fileviewer.Model
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Model) Init() tea.Cmd {
|
func (a Model) Init() tea.Cmd {
|
||||||
@@ -650,6 +649,10 @@ func (a Model) View() string {
|
|||||||
return mainLayout + "\n" + a.status.View()
|
return mainLayout + "\n" + a.status.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a Model) Cleanup() {
|
||||||
|
a.status.Cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
|
func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
response, err := a.app.Client.File.Read(
|
response, err := a.app.Client.File.Read(
|
||||||
|
|||||||
Reference in New Issue
Block a user