feat: implement file referencing with @filename autocomplete in chat input (#118)
This commit is contained in:
@@ -82,6 +82,7 @@ func (s *Server) setupRoutes() {
|
||||
// Workspace file operations (simple HTTP)
|
||||
api.GET("/workspace/tree", s.handleFileTree)
|
||||
api.GET("/workspace/file/content", s.handleFileContent)
|
||||
api.GET("/workspace/search", s.handleFileSearch)
|
||||
|
||||
// Shell access (HTTP endpoints only - streaming is via /workspace/stream)
|
||||
api.GET("/shell/status", s.handleShellStatus)
|
||||
|
||||
@@ -151,6 +151,21 @@ func (s *Server) handleFileContent(c *gin.Context) {
|
||||
c.JSON(200, types.FileContentResponse{Path: path, Content: content, Size: size})
|
||||
}
|
||||
|
||||
// handleFileSearch handles file search requests via HTTP GET
|
||||
func (s *Server) handleFileSearch(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
limit := 20
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if parsed := mustParseInt(l); parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
files := s.procMgr.GetWorkspaceTracker().SearchFiles(query, limit)
|
||||
|
||||
c.JSON(200, types.FileSearchResponse{Files: files})
|
||||
}
|
||||
|
||||
// mustParseInt parses a string to int, returns 0 on error
|
||||
func mustParseInt(s string) int {
|
||||
var n int
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -1183,3 +1184,65 @@ func (wt *WorkspaceTracker) GetFileContent(reqPath string) (string, int64, error
|
||||
|
||||
return string(content), info.Size(), nil
|
||||
}
|
||||
|
||||
// scoredMatch holds a file path and its match score for sorting
|
||||
type scoredMatch struct {
|
||||
path string
|
||||
score int
|
||||
}
|
||||
|
||||
// SearchFiles searches for files matching the query string.
|
||||
// It uses fuzzy matching with scoring based on how well the query matches.
|
||||
func (wt *WorkspaceTracker) SearchFiles(query string, limit int) []string {
|
||||
if query == "" {
|
||||
return []string{}
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
query = strings.ToLower(query)
|
||||
var matches []scoredMatch
|
||||
|
||||
wt.mu.RLock()
|
||||
files := wt.currentFiles.Files
|
||||
wt.mu.RUnlock()
|
||||
|
||||
for _, file := range files {
|
||||
path := file.Path
|
||||
lowerPath := strings.ToLower(path)
|
||||
name := filepath.Base(lowerPath)
|
||||
|
||||
score := 0
|
||||
if name == query {
|
||||
score = 100 // Exact filename match
|
||||
} else if strings.HasPrefix(name, query) {
|
||||
score = 75 // Filename starts with query
|
||||
} else if strings.Contains(name, query) {
|
||||
score = 50 // Filename contains query
|
||||
} else if strings.Contains(lowerPath, query) {
|
||||
score = 25 // Path contains query
|
||||
}
|
||||
|
||||
if score > 0 {
|
||||
matches = append(matches, scoredMatch{path: path, score: score})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
if matches[i].score != matches[j].score {
|
||||
return matches[i].score > matches[j].score
|
||||
}
|
||||
// Secondary sort by path length (shorter paths first)
|
||||
return len(matches[i].path) < len(matches[j].path)
|
||||
})
|
||||
|
||||
// Return top limit results
|
||||
result := make([]string, 0, limit)
|
||||
for i := 0; i < len(matches) && i < limit; i++ {
|
||||
result = append(result, matches[i].path)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user