Skip to content

Commit b2f79b9

Browse files
committed
search implementation updated prioritise exact match
1 parent 5bf360d commit b2f79b9

4 files changed

Lines changed: 198 additions & 45 deletions

File tree

internal/filemanager/filemanager.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func (fm *FileManager) ListFiles() ([]string, error) {
265265
}
266266

267267
// SearchFilesContent searches for text within the workspace, trying multiple search terms in order of specificity.
268-
func (fm *FileManager) SearchFilesContent(searchTerms []string) ([]string, error) {
268+
func (fm *FileManager) SearchFilesContent(searchTerms []string) ([]string, []string, error) {
269269
var exactResults []string
270270
var tokenResults []string
271271

@@ -337,21 +337,29 @@ func (fm *FileManager) SearchFilesContent(searchTerms []string) ([]string, error
337337
return nil
338338
}
339339

340-
// 1. Exact Match on any term
341-
matchedExact := false
342-
for _, term := range lowerTerms {
343-
if strings.Contains(lowerContent, term) {
344-
exactResults = append(exactResults, relPath)
345-
matchedExact = true
346-
break
340+
// 1. Exact Match on PRIMARY term only
341+
if len(lowerTerms) > 0 && strings.Contains(lowerContent, lowerTerms[0]) {
342+
exactResults = append(exactResults, relPath)
343+
return nil
344+
}
345+
346+
// 2. Match on alternative variations
347+
matchedAlt := false
348+
if len(lowerTerms) > 1 {
349+
for _, term := range lowerTerms[1:] {
350+
if strings.Contains(lowerContent, term) {
351+
tokenResults = append(tokenResults, relPath)
352+
matchedAlt = true
353+
break
354+
}
347355
}
348356
}
349357

350-
if matchedExact {
358+
if matchedAlt {
351359
return nil
352360
}
353361

354-
// 2. Tokenized/Fuzzy match on any token set (fallback)
362+
// 3. Tokenized/Fuzzy match on any token set (fallback)
355363
for _, tokens := range tokenSets {
356364
allMatched := true
357365
for _, token := range tokens {
@@ -370,13 +378,10 @@ func (fm *FileManager) SearchFilesContent(searchTerms []string) ([]string, error
370378
})
371379

372380
if err != nil {
373-
return nil, fmt.Errorf("failed to search files: %w", err)
381+
return nil, nil, fmt.Errorf("failed to search files: %w", err)
374382
}
375383

376-
if len(exactResults) > 0 {
377-
return exactResults, nil
378-
}
379-
return tokenResults, nil
384+
return exactResults, tokenResults, nil
380385
}
381386

382387
// ListDirectories returns a list of subdirectories in the given directory

internal/ui/app.go

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ type App struct {
9090
statusMessage string // Status bar message
9191
pendingCodeInsert string // Code waiting to be inserted after file creation
9292
buildNumber string // Build number from git commits
93+
searchResults []string // Files found in last search
94+
searchResultIndex int // Current index in searchResults
95+
searchTerms []string // Last search terms used
9396
}
9497

9598
// New creates a new application instance with the provided configuration.
@@ -171,6 +174,9 @@ func New(config *types.AppConfig, buildNumber string) *App {
171174
showExitConfirmation: false,
172175
forceQuit: false,
173176
buildNumber: buildNumber,
177+
searchResults: []string{},
178+
searchResultIndex: 0,
179+
searchTerms: []string{},
174180
}
175181
}
176182

@@ -547,36 +553,48 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
547553

548554
case SearchCompleteMsg:
549555
a.aiPane.streaming = false
550-
if len(msg.Results) > 0 {
551-
// Open the first matching file
552-
fullPath := filepath.Join(a.config.WorkspaceDir, msg.Results[0])
553-
err := a.editorPane.LoadFile(fullPath)
554-
if err != nil {
555-
a.statusMessage = "Error opening found file: " + err.Error()
556-
a.aiPane.DisplayNotification("Found matches for '" + msg.SearchTerm + "', but failed to open " + msg.Results[0])
557-
} else {
558-
a.statusMessage = "Opened: " + msg.Results[0]
559-
a.aiPane.SetWorkingDir(filepath.Dir(a.editorPane.currentFile.Filepath))
560-
561-
// Switch to editor pane
562-
a.activePane = types.EditorPaneType
563-
a.editorPane.focused = true
564-
a.aiPane.focused = false
556+
totalResults := len(msg.ExactResults) + len(msg.AltResults)
557+
if totalResults > 0 {
558+
a.searchResults = append(msg.ExactResults, msg.AltResults...)
559+
a.searchTerms = strings.Split(msg.SearchTerm, ", ")
560+
a.searchResultIndex = 0
561+
562+
a.openSearchResult()
563+
564+
var chatMsg strings.Builder
565+
chatMsg.WriteString(fmt.Sprintf("🔍 Found '%s' in %d file(s):\n", msg.SearchTerm, totalResults))
566+
567+
if len(msg.ExactResults) > 0 {
568+
chatMsg.WriteString("Exact search pattern:\n")
569+
for i, res := range msg.ExactResults {
570+
if i >= 3 {
571+
chatMsg.WriteString(fmt.Sprintf("- ... and %d more exact matches\n", len(msg.ExactResults)-3))
572+
break
573+
}
574+
chatMsg.WriteString("- " + res + "\n")
575+
}
576+
}
565577

566-
var chatMsg strings.Builder
567-
chatMsg.WriteString(fmt.Sprintf("🔍 Found '%s' in %d file(s):\n", msg.SearchTerm, len(msg.Results)))
568-
for i, res := range msg.Results {
569-
if i >= 5 {
570-
chatMsg.WriteString(fmt.Sprintf("- ... and %d more\n", len(msg.Results)-5))
578+
if len(msg.AltResults) > 0 {
579+
chatMsg.WriteString("\nAlternative variations:\n")
580+
for i, res := range msg.AltResults {
581+
if i >= 3 {
582+
chatMsg.WriteString(fmt.Sprintf("- ... and %d more\n", len(msg.AltResults)-3))
571583
break
572584
}
573585
chatMsg.WriteString("- " + res + "\n")
574586
}
575-
chatMsg.WriteString("\nOpening the first match: " + msg.Results[0])
576-
a.aiPane.DisplayNotification(chatMsg.String())
577587
}
588+
589+
matchType := "exact match"
590+
if len(msg.ExactResults) == 0 {
591+
matchType = "alternative variation"
592+
}
593+
chatMsg.WriteString(fmt.Sprintf("\nOpening the first match: %s (%s)", a.searchResults[0], matchType))
594+
chatMsg.WriteString("\n\n*Tip: Use `Alt+N` (Next) and `Alt+P` (Previous) to jump between these files.*")
595+
a.aiPane.DisplayNotification(chatMsg.String())
578596
}
579-
return a, nil
597+
return a, tea.Batch(cmds...)
580598

581599
case TerminalOutputMsg, TerminalDoneMsg, AIResponseMsg, AINotificationMsg, AIAvailabilityMsg, ClearStatusMsg:
582600
// Handle ClearStatusMsg
@@ -1064,6 +1082,31 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
10641082
// Clear AI chat history (New Chat)
10651083
a.aiPane.ClearHistory()
10661084
a.statusMessage = "AI chat history cleared"
1085+
a.searchResults = []string{}
1086+
return a, nil
1087+
1088+
case "alt+n":
1089+
if len(a.searchResults) > 0 {
1090+
a.searchResultIndex++
1091+
if a.searchResultIndex >= len(a.searchResults) {
1092+
a.searchResultIndex = 0 // loop back to start
1093+
}
1094+
a.openSearchResult()
1095+
} else {
1096+
a.statusMessage = "No search results to jump to"
1097+
}
1098+
return a, nil
1099+
1100+
case "alt+p":
1101+
if len(a.searchResults) > 0 {
1102+
a.searchResultIndex--
1103+
if a.searchResultIndex < 0 {
1104+
a.searchResultIndex = len(a.searchResults) - 1 // loop back to end
1105+
}
1106+
a.openSearchResult()
1107+
} else {
1108+
a.statusMessage = "No search results to jump to"
1109+
}
10671110
return a, nil
10681111

10691112
case "ctrl+h":
@@ -2190,7 +2233,7 @@ func (a *App) handleAIMessage(message string) tea.Cmd {
21902233
}
21912234
}
21922235

2193-
results, err := a.fileManager.SearchFilesContent(terms)
2236+
exactResults, altResults, err := a.fileManager.SearchFilesContent(terms)
21942237
if err != nil {
21952238
return AgenticFixResultMsg{
21962239
Result: &agentic.FixResult{
@@ -2201,7 +2244,7 @@ func (a *App) handleAIMessage(message string) tea.Cmd {
22012244
}
22022245
}
22032246

2204-
if len(results) == 0 {
2247+
if len(exactResults) == 0 && len(altResults) == 0 {
22052248
return AgenticFixResultMsg{
22062249
Result: &agentic.FixResult{
22072250
Success: false,
@@ -2212,8 +2255,9 @@ func (a *App) handleAIMessage(message string) tea.Cmd {
22122255
}
22132256

22142257
return SearchCompleteMsg{
2215-
SearchTerm: strings.Join(terms, ", "),
2216-
Results: results,
2258+
SearchTerm: strings.Join(terms, ", "),
2259+
ExactResults: exactResults,
2260+
AltResults: altResults,
22172261
}
22182262
}
22192263
}
@@ -2256,6 +2300,35 @@ func (a *App) handleAIMessage(message string) tea.Cmd {
22562300
}
22572301
}
22582302

2303+
// openSearchResult opens the file at the current search result index and jumps to the matched term
2304+
func (a *App) openSearchResult() {
2305+
if len(a.searchResults) == 0 || a.searchResultIndex < 0 || a.searchResultIndex >= len(a.searchResults) {
2306+
return
2307+
}
2308+
2309+
res := a.searchResults[a.searchResultIndex]
2310+
fullPath := filepath.Join(a.config.WorkspaceDir, res)
2311+
2312+
err := a.editorPane.LoadFile(fullPath)
2313+
if err != nil {
2314+
a.statusMessage = fmt.Sprintf("Error opening file %d/%d: %s", a.searchResultIndex+1, len(a.searchResults), err.Error())
2315+
return
2316+
}
2317+
2318+
a.statusMessage = fmt.Sprintf("Opened search match %d of %d: %s", a.searchResultIndex+1, len(a.searchResults), res)
2319+
a.aiPane.SetWorkingDir(filepath.Dir(a.editorPane.currentFile.Filepath))
2320+
2321+
// Search and jump to the matched term inside the editor
2322+
if len(a.searchTerms) > 0 {
2323+
a.editorPane.SearchAndJump(a.searchTerms)
2324+
}
2325+
2326+
// Switch to editor pane
2327+
a.activePane = types.EditorPaneType
2328+
a.editorPane.focused = true
2329+
a.aiPane.focused = false
2330+
}
2331+
22592332
// ensureGoModule checks if a go.mod exists in the given directory and runs
22602333
// go mod init and go mod tidy if it doesn't, to automate Go project creation.
22612334
func (a *App) ensureGoModule(dir string) {

internal/ui/editor.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,87 @@ func (e *EditorPane) HasUnsavedChanges() bool {
247247
// Returns:
248248
// - string: The current line text
249249
func (e *EditorPane) GetCurrentLine() string {
250-
lines := strings.Split(e.content, "\n")
250+
content := e.GetContent()
251+
lines := strings.Split(content, "\n")
252+
253+
if len(lines) == 0 {
254+
return ""
255+
}
256+
251257
if e.cursorLine >= 0 && e.cursorLine < len(lines) {
252258
return lines[e.cursorLine]
253259
}
260+
254261
return ""
255262
}
256263

264+
// SearchAndJump finds the first occurrence of any of the search terms and jumps the cursor to it
265+
func (e *EditorPane) SearchAndJump(terms []string) {
266+
if len(terms) == 0 || e.currentFile == nil {
267+
return
268+
}
269+
270+
// Prepare terms (lowercase for case-insensitive search, similar to filemanager)
271+
var lowerTerms []string
272+
var tokenSets [][]string
273+
for _, t := range terms {
274+
lower := strings.ToLower(strings.TrimSpace(t))
275+
if lower != "" {
276+
lowerTerms = append(lowerTerms, lower)
277+
cleanTerm := strings.ReplaceAll(lower, "_", " ")
278+
cleanTerm = strings.ReplaceAll(cleanTerm, "-", " ")
279+
cleanTerm = strings.ReplaceAll(cleanTerm, ".", " ")
280+
tokens := strings.Fields(cleanTerm)
281+
if len(tokens) > 0 {
282+
tokenSets = append(tokenSets, tokens)
283+
}
284+
}
285+
}
286+
287+
contentLines := strings.Split(e.GetContent(), "\n")
288+
289+
for i, line := range contentLines {
290+
lowerLine := strings.ToLower(line)
291+
292+
// Exact Match check
293+
matched := false
294+
for _, term := range lowerTerms {
295+
if strings.Contains(lowerLine, term) {
296+
matched = true
297+
break
298+
}
299+
}
300+
301+
if !matched {
302+
// Fuzzy token check
303+
for _, tokens := range tokenSets {
304+
allMatched := true
305+
for _, token := range tokens {
306+
if !strings.Contains(lowerLine, token) {
307+
allMatched = false
308+
break
309+
}
310+
}
311+
if allMatched {
312+
matched = true
313+
break
314+
}
315+
}
316+
}
317+
318+
if matched {
319+
e.cursorLine = i
320+
// Adjust scroll to center the match
321+
e.scrollOffset = e.cursorLine - (e.height / 2)
322+
if e.scrollOffset < 0 {
323+
e.scrollOffset = 0
324+
}
325+
e.cursorCol = 0
326+
return
327+
}
328+
}
329+
}
330+
257331
// Update handles messages for the editor pane.
258332
// Only processes messages when the pane is focused.
259333
// Delegates keyboard input to handleKeyPress.

internal/ui/messages.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type AgenticFixResultMsg struct {
1515

1616
// SearchCompleteMsg is sent when a workspace search completes.
1717
type SearchCompleteMsg struct {
18-
SearchTerm string
19-
Results []string
18+
SearchTerm string
19+
ExactResults []string
20+
AltResults []string
2021
}

0 commit comments

Comments
 (0)