diff options
| author | Adictya <[email protected]> | 2025-05-15 08:42:45 +0530 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-05-15 08:29:54 -0500 |
| commit | 15bf40bc102ed5426fa2148b9e8f39acef1174a0 (patch) | |
| tree | 4b005eba92703864a4bc2884b3cdc1fb5ab21a3c /internal/completions | |
| parent | a33e3e25b6646b6128975836d7f182e89f5eb891 (diff) | |
| download | opencode-15bf40bc102ed5426fa2148b9e8f39acef1174a0.tar.gz opencode-15bf40bc102ed5426fa2148b9e8f39acef1174a0.zip | |
feat(complete-module): add completions logic, dialog and providers
Diffstat (limited to 'internal/completions')
| -rw-r--r-- | internal/completions/files-folders.go | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/internal/completions/files-folders.go b/internal/completions/files-folders.go new file mode 100644 index 000000000..af1b5a874 --- /dev/null +++ b/internal/completions/files-folders.go @@ -0,0 +1,191 @@ +package completions + +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" + + "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/opencode-ai/opencode/internal/fileutil" + "github.com/opencode-ai/opencode/internal/logging" + "github.com/opencode-ai/opencode/internal/tui/components/dialog" +) + +type filesAndFoldersContextGroup struct { + prefix string +} + +func (cg *filesAndFoldersContextGroup) GetId() string { + return cg.prefix +} + +func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI { + return dialog.NewCompletionItem(dialog.CompletionItem{ + Title: "Files & Folders", + Value: "files", + }) +} + +func processNullTerminatedOutput(outputBytes []byte) []string { + if len(outputBytes) > 0 && outputBytes[len(outputBytes)-1] == 0 { + outputBytes = outputBytes[:len(outputBytes)-1] + } + + if len(outputBytes) == 0 { + return []string{} + } + + split := bytes.Split(outputBytes, []byte{0}) + matches := make([]string, 0, len(split)) + + for _, p := range split { + if len(p) == 0 { + continue + } + + path := string(p) + path = filepath.Join(".", path) + + if !fileutil.SkipHidden(path) { + matches = append(matches, path) + } + } + + return matches +} + +func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) { + cmdRg := fileutil.GetRgCmd("") // No glob pattern for this use case + cmdFzf := fileutil.GetFzfCmd(query) + + var matches []string + // Case 1: Both rg and fzf available + if cmdRg != nil && cmdFzf != nil { + rgPipe, err := cmdRg.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("failed to get rg stdout pipe: %w", err) + } + defer rgPipe.Close() + + cmdFzf.Stdin = rgPipe + var fzfOut bytes.Buffer + var fzfErr bytes.Buffer + cmdFzf.Stdout = &fzfOut + cmdFzf.Stderr = &fzfErr + + if err := cmdFzf.Start(); err != nil { + return nil, fmt.Errorf("failed to start fzf: %w", err) + } + + errRg := cmdRg.Run() + errFzf := cmdFzf.Wait() + + if errRg != nil { + logging.Warn(fmt.Sprintf("rg command failed during pipe: %v", errRg)) + } + + if errFzf != nil { + if exitErr, ok := errFzf.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return []string{}, nil // No matches from fzf + } + return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", errFzf, fzfErr.String()) + } + + matches = processNullTerminatedOutput(fzfOut.Bytes()) + + // Case 2: Only rg available + } else if cmdRg != nil { + logging.Debug("Using Ripgrep with fuzzy match fallback for file completions") + var rgOut bytes.Buffer + var rgErr bytes.Buffer + cmdRg.Stdout = &rgOut + cmdRg.Stderr = &rgErr + + if err := cmdRg.Run(); err != nil { + return nil, fmt.Errorf("rg command failed: %w\nStderr: %s", err, rgErr.String()) + } + + allFiles := processNullTerminatedOutput(rgOut.Bytes()) + matches = fuzzy.Find(query, allFiles) + + // Case 3: Only fzf available + } else if cmdFzf != nil { + logging.Debug("Using FZF with doublestar fallback for file completions") + files, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0) + if err != nil { + return nil, fmt.Errorf("failed to list files for fzf: %w", err) + } + + allFiles := make([]string, 0, len(files)) + for _, file := range files { + if !fileutil.SkipHidden(file) { + allFiles = append(allFiles, file) + } + } + + var fzfIn bytes.Buffer + for _, file := range allFiles { + fzfIn.WriteString(file) + fzfIn.WriteByte(0) + } + + cmdFzf.Stdin = &fzfIn + var fzfOut bytes.Buffer + var fzfErr bytes.Buffer + cmdFzf.Stdout = &fzfOut + cmdFzf.Stderr = &fzfErr + + if err := cmdFzf.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return []string{}, nil + } + return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", err, fzfErr.String()) + } + + matches = processNullTerminatedOutput(fzfOut.Bytes()) + + // Case 4: Fallback to doublestar with fuzzy match + } else { + logging.Debug("Using doublestar with fuzzy match for file completions") + allFiles, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0) + if err != nil { + return nil, fmt.Errorf("failed to glob files: %w", err) + } + + filteredFiles := make([]string, 0, len(allFiles)) + for _, file := range allFiles { + if !fileutil.SkipHidden(file) { + filteredFiles = append(filteredFiles, file) + } + } + + matches = fuzzy.Find(query, filteredFiles) + } + + return matches, nil +} + +func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) { + matches, err := cg.getFiles(query) + if err != nil { + return nil, err + } + + items := make([]dialog.CompletionItemI, 0, len(matches)) + for _, file := range matches { + item := dialog.NewCompletionItem(dialog.CompletionItem{ + Title: file, + Value: file, + }) + items = append(items, item) + } + + return items, nil +} + +func NewFileAndFolderContextGroup() dialog.CompletionProvider { + return &filesAndFoldersContextGroup{ + prefix: "file", + } +} |
