| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- package model
- import (
- "context"
- "fmt"
- "log/slog"
- "path/filepath"
- "slices"
- "strings"
- tea "charm.land/bubbletea/v2"
- "charm.land/lipgloss/v2"
- "github.com/charmbracelet/crush/internal/diff"
- "github.com/charmbracelet/crush/internal/fsext"
- "github.com/charmbracelet/crush/internal/history"
- "github.com/charmbracelet/crush/internal/session"
- "github.com/charmbracelet/crush/internal/ui/common"
- "github.com/charmbracelet/crush/internal/ui/styles"
- "github.com/charmbracelet/crush/internal/ui/util"
- "github.com/charmbracelet/x/ansi"
- )
- // loadSessionMsg is a message indicating that a session and its files have
- // been loaded.
- type loadSessionMsg struct {
- session *session.Session
- files []SessionFile
- readFiles []string
- }
- // lspFilePaths returns deduplicated file paths from both modified and read
- // files for starting LSP servers.
- func (msg loadSessionMsg) lspFilePaths() []string {
- seen := make(map[string]struct{}, len(msg.files)+len(msg.readFiles))
- paths := make([]string, 0, len(msg.files)+len(msg.readFiles))
- for _, f := range msg.files {
- p := f.LatestVersion.Path
- if _, ok := seen[p]; ok {
- continue
- }
- seen[p] = struct{}{}
- paths = append(paths, p)
- }
- for _, p := range msg.readFiles {
- if _, ok := seen[p]; ok {
- continue
- }
- seen[p] = struct{}{}
- paths = append(paths, p)
- }
- return paths
- }
- // SessionFile tracks the first and latest versions of a file in a session,
- // along with the total additions and deletions.
- type SessionFile struct {
- FirstVersion history.File
- LatestVersion history.File
- Additions int
- Deletions int
- }
- // loadSession loads the session along with its associated files and computes
- // the diff statistics (additions and deletions) for each file in the session.
- // It returns a tea.Cmd that, when executed, fetches the session data and
- // returns a sessionFilesLoadedMsg containing the processed session files.
- func (m *UI) loadSession(sessionID string) tea.Cmd {
- return func() tea.Msg {
- session, err := m.com.App.Sessions.Get(context.Background(), sessionID)
- if err != nil {
- return util.ReportError(err)
- }
- sessionFiles, err := m.loadSessionFiles(sessionID)
- if err != nil {
- return util.ReportError(err)
- }
- readFiles, err := m.com.App.FileTracker.ListReadFiles(context.Background(), sessionID)
- if err != nil {
- slog.Error("Failed to load read files for session", "error", err)
- }
- return loadSessionMsg{
- session: &session,
- files: sessionFiles,
- readFiles: readFiles,
- }
- }
- }
- func (m *UI) loadSessionFiles(sessionID string) ([]SessionFile, error) {
- files, err := m.com.App.History.ListBySession(context.Background(), sessionID)
- if err != nil {
- return nil, err
- }
- filesByPath := make(map[string][]history.File)
- for _, f := range files {
- filesByPath[f.Path] = append(filesByPath[f.Path], f)
- }
- sessionFiles := make([]SessionFile, 0, len(filesByPath))
- for _, versions := range filesByPath {
- if len(versions) == 0 {
- continue
- }
- first := versions[0]
- last := versions[0]
- for _, v := range versions {
- if v.Version < first.Version {
- first = v
- }
- if v.Version > last.Version {
- last = v
- }
- }
- _, additions, deletions := diff.GenerateDiff(first.Content, last.Content, first.Path)
- sessionFiles = append(sessionFiles, SessionFile{
- FirstVersion: first,
- LatestVersion: last,
- Additions: additions,
- Deletions: deletions,
- })
- }
- slices.SortFunc(sessionFiles, func(a, b SessionFile) int {
- if a.LatestVersion.UpdatedAt > b.LatestVersion.UpdatedAt {
- return -1
- }
- if a.LatestVersion.UpdatedAt < b.LatestVersion.UpdatedAt {
- return 1
- }
- return 0
- })
- return sessionFiles, nil
- }
- // handleFileEvent processes file change events and updates the session file
- // list with new or updated file information.
- func (m *UI) handleFileEvent(file history.File) tea.Cmd {
- if m.session == nil || file.SessionID != m.session.ID {
- return nil
- }
- return func() tea.Msg {
- sessionFiles, err := m.loadSessionFiles(m.session.ID)
- // could not load session files
- if err != nil {
- return util.NewErrorMsg(err)
- }
- return sessionFilesUpdatesMsg{
- sessionFiles: sessionFiles,
- }
- }
- }
- // filesInfo renders the modified files section for the sidebar, showing files
- // with their addition/deletion counts.
- func (m *UI) filesInfo(cwd string, width, maxItems int, isSection bool) string {
- t := m.com.Styles
- title := t.Subtle.Render("Modified Files")
- if isSection {
- title = common.Section(t, "Modified Files", width)
- }
- list := t.Subtle.Render("None")
- var filesWithChanges []SessionFile
- for _, f := range m.sessionFiles {
- if f.Additions == 0 && f.Deletions == 0 {
- continue
- }
- filesWithChanges = append(filesWithChanges, f)
- }
- if len(filesWithChanges) > 0 {
- list = fileList(t, cwd, filesWithChanges, width, maxItems)
- }
- return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
- }
- // fileList renders a list of files with their diff statistics, truncating to
- // maxItems and showing a "...and N more" message if needed.
- func fileList(t *styles.Styles, cwd string, filesWithChanges []SessionFile, width, maxItems int) string {
- if maxItems <= 0 {
- return ""
- }
- var renderedFiles []string
- filesShown := 0
- for _, f := range filesWithChanges {
- // Skip files with no changes
- if filesShown >= maxItems {
- break
- }
- // Build stats string with colors
- var statusParts []string
- if f.Additions > 0 {
- statusParts = append(statusParts, t.Files.Additions.Render(fmt.Sprintf("+%d", f.Additions)))
- }
- if f.Deletions > 0 {
- statusParts = append(statusParts, t.Files.Deletions.Render(fmt.Sprintf("-%d", f.Deletions)))
- }
- extraContent := strings.Join(statusParts, " ")
- // Format file path
- filePath := f.FirstVersion.Path
- if rel, err := filepath.Rel(cwd, filePath); err == nil {
- filePath = rel
- }
- filePath = fsext.DirTrim(filePath, 2)
- filePath = ansi.Truncate(filePath, width-(lipgloss.Width(extraContent)-2), "…")
- line := t.Files.Path.Render(filePath)
- if extraContent != "" {
- line = fmt.Sprintf("%s %s", line, extraContent)
- }
- renderedFiles = append(renderedFiles, line)
- filesShown++
- }
- if len(filesWithChanges) > maxItems {
- remaining := len(filesWithChanges) - maxItems
- renderedFiles = append(renderedFiles, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
- }
- return lipgloss.JoinVertical(lipgloss.Left, renderedFiles...)
- }
- // startLSPs starts LSP servers for the given file paths.
- func (m *UI) startLSPs(paths []string) tea.Cmd {
- if len(paths) == 0 {
- return nil
- }
- return func() tea.Msg {
- ctx := context.Background()
- for _, path := range paths {
- m.com.App.LSPManager.Start(ctx, path)
- }
- return nil
- }
- }
|