22"""Output formatters for CLI search results."""
33
44from abc import ABC , abstractmethod
5- from typing import List , Optional
5+ from typing import List
66
77import typer
88
99from .models .document_result import DocumentResult
1010
11+ # Display constants
12+ BOX_CONTENT_WIDTH = 75
13+ BOX_TOTAL_WIDTH = 77
14+ SNIPPET_MAX_LENGTH = 400
15+ SENTENCE_PREVIEW_LENGTH = 50
16+ MAX_SENTENCES_DISPLAY = 5
17+
1118
1219class SearchResultFormatter (ABC ):
1320 """Base class for search result formatters."""
@@ -40,7 +47,10 @@ def _get_file_icon(self, uri: str) -> str:
4047 return "📄"
4148
4249 def _clean_and_wrap_snippet (
43- self , snippet : str , width : int = 75 , max_length : int = 400
50+ self ,
51+ snippet : str ,
52+ width : int = BOX_CONTENT_WIDTH ,
53+ max_length : int = SNIPPET_MAX_LENGTH ,
4454 ) -> List [str ]:
4555 """Clean snippet and wrap to specified width with max length limit."""
4656 # Clean the snippet
@@ -69,7 +79,9 @@ def _clean_and_wrap_snippet(
6979
7080 return lines
7181
72- def _format_uri_display (self , uri : str , icon : str , max_width : int = 75 ) -> str :
82+ def _format_uri_display (
83+ self , uri : str , icon : str , max_width : int = BOX_CONTENT_WIDTH
84+ ) -> str :
7385 """Format URI for display with icon and truncation."""
7486 if not uri :
7587 return ""
@@ -82,7 +94,15 @@ def _format_uri_display(self, uri: str, icon: str, max_width: int = 75) -> str:
8294
8395
8496class BoxedFormatter (SearchResultFormatter ):
85- """Base class for boxed result formatters."""
97+ """Boxed formatter for search results with optional debug information."""
98+
99+ def __init__ (self , show_debug : bool = False ):
100+ """Initialize formatter.
101+
102+ Args:
103+ show_debug: Whether to show debug information and sentence details
104+ """
105+ self .show_debug = show_debug
86106
87107 def format_results (self , results : List [DocumentResult ], query : str ) -> None :
88108 if not results :
@@ -98,56 +118,39 @@ def format_results(self, results: List[DocumentResult], query: str) -> None:
98118 def _format_single_result (self , doc : DocumentResult , idx : int ) -> None :
99119 """Format a single result with box layout."""
100120 icon = self ._get_file_icon (doc .document .uri or "" )
121+ snippet_text = doc .get_preview (max_chars = SNIPPET_MAX_LENGTH )
122+ snippet_lines = self ._clean_and_wrap_snippet (snippet_text )
101123
102- # Get snippet from DocumentResult (handles sentence-based preview automatically)
103- snippet_text = doc .get_preview (max_chars = 400 )
104-
105- snippet_lines = self ._clean_and_wrap_snippet (
106- snippet_text , width = 75 , max_length = 400
107- )
108-
109- # Draw the result box header
110- header = f"┌─ Result #{ idx } " + "─" * (67 - len (str (idx )))
124+ # Draw box header
125+ header = f"┌─ Result #{ idx } " + "─" * (BOX_TOTAL_WIDTH - 10 - len (str (idx )))
111126 typer .echo (header )
112127
113- # Display URI if available
128+ # Display URI and debug info
114129 if doc .document .uri :
115- uri_display = self ._format_uri_display (doc .document .uri , icon , 75 )
116- typer .echo (f"│ { uri_display :<75 } │" )
130+ uri_display = self ._format_uri_display (doc .document .uri , icon )
131+ typer .echo (f"│ { uri_display :<{ BOX_CONTENT_WIDTH } } │" )
117132
118- # Add debug info if needed
119- debug_line = self ._get_debug_line (doc )
120- if debug_line :
121- typer .echo (debug_line )
133+ if self .show_debug :
134+ self ._print_debug_line (doc )
122135
123- typer .echo ("├" + "─" * 77 + "┤" )
124- elif self ._should_show_debug ():
125- debug_line = self ._get_debug_line (doc )
126- if debug_line :
127- typer .echo (debug_line )
128- typer .echo ("├" + "─" * 77 + "┤" )
136+ typer .echo ("├" + "─" * BOX_TOTAL_WIDTH + "┤" )
137+ elif self .show_debug :
138+ self ._print_debug_line (doc )
139+ typer .echo ("├" + "─" * BOX_TOTAL_WIDTH + "┤" )
129140
130141 # Display snippet
131142 for line in snippet_lines :
132- typer .echo (f"│ { line :<75 } │" )
143+ typer .echo (f"│ { line :<{ BOX_CONTENT_WIDTH } } │" )
133144
134- typer .echo ("└" + "─" * 77 + "┘" )
135- typer .echo ()
145+ # Display sentence details in debug mode
146+ if self .show_debug and doc .sentences :
147+ self ._print_sentence_details (doc )
136148
137- def _get_debug_line (self , doc : DocumentResult ) -> Optional [str ]:
138- """Get debug information line. Override in subclasses."""
139- return None
140-
141- def _should_show_debug (self ) -> bool :
142- """Whether to show debug information. Override in subclasses."""
143- return False
144-
145-
146- class BoxedDebugFormatter (BoxedFormatter ):
147- """Modern detailed formatter with debug information in boxes."""
149+ typer .echo ("└" + "─" * BOX_TOTAL_WIDTH + "┘" )
150+ typer .echo ()
148151
149- def _get_debug_line (self , doc : DocumentResult ) -> str :
150- """Format debug metrics line."""
152+ def _print_debug_line (self , doc : DocumentResult ) -> None :
153+ """Print debug metrics line."""
151154 combined = (
152155 f"{ doc .combined_rank :.5f} " if doc .combined_rank is not None else "N/A"
153156 )
@@ -161,88 +164,36 @@ def _get_debug_line(self, doc: DocumentResult) -> str:
161164 if doc .fts_rank is not None
162165 else "N/A"
163166 )
164- return f"│ Combined: { combined } │ Vector: { vec_info } │ FTS: { fts_info } "
165-
166- def _should_show_debug (self ) -> bool :
167- return True
168-
169- def _format_single_result (self , doc : DocumentResult , idx : int ) -> None :
170- """Format a single result with box layout including sentence summary."""
171- icon = self ._get_file_icon (doc .document .uri or "" )
172-
173- # Get snippet from DocumentResult (handles sentence-based preview automatically)
174- snippet_text = doc .get_preview (max_chars = 400 )
175-
176- snippet_lines = self ._clean_and_wrap_snippet (
177- snippet_text , width = 75 , max_length = 400
178- )
167+ debug_line = f"│ Combined: { combined } │ Vector: { vec_info } │ FTS: { fts_info } "
168+ typer .echo (debug_line )
179169
180- # Draw the result box header
181- header = f"┌─ Result #{ idx } " + "─" * (67 - len (str (idx )))
182- typer .echo (header )
170+ def _print_sentence_details (self , doc : DocumentResult ) -> None :
171+ """Print sentence-level details."""
172+ typer .echo ("├" + "─" * BOX_TOTAL_WIDTH + "┤" )
173+ typer .echo (f"│ Sentences:{ ' ' * (BOX_CONTENT_WIDTH - 10 )} │" )
183174
184- # Display URI if available
185- if doc .document .uri :
186- uri_display = self ._format_uri_display (doc .document .uri , icon , 75 )
187- typer .echo (f"│ { uri_display :<75} │" )
188-
189- # Add debug info
190- debug_line = self ._get_debug_line (doc )
191- if debug_line :
192- typer .echo (debug_line )
193-
194- typer .echo ("├" + "─" * 77 + "┤" )
195- elif self ._should_show_debug ():
196- debug_line = self ._get_debug_line (doc )
197- if debug_line :
198- typer .echo (debug_line )
199- typer .echo ("├" + "─" * 77 + "┤" )
200-
201- # Display snippet preview
202- for line in snippet_lines :
203- typer .echo (f"│ { line :<75} │" )
204-
205- # Display sentence details if available
206- if doc .sentences :
207- typer .echo ("├" + "─" * 77 + "┤" )
208- typer .echo (
209- "│ Sentences: │"
175+ for sentence in doc .sentences [:MAX_SENTENCES_DISPLAY ]:
176+ distance_str = (
177+ f"{ sentence .distance :.6f} " if sentence .distance is not None else "N/A"
210178 )
211-
212- for sentence in doc .sentences [:5 ]: # Show max 5 sentences
213- distance_str = (
214- f"{ sentence .distance :.6f} "
215- if sentence .distance is not None
216- else "N/A"
217- )
218- rank_str = f"#{ sentence .rank } " if sentence .rank is not None else "N/A"
219-
220- # Extract sentence preview (first 50 chars)
221- if (
222- sentence .start_offset is not None
223- and sentence .end_offset is not None
224- ):
225- sentence_text = doc .chunk_content [
226- sentence .start_offset : sentence .end_offset
227- ].strip ()
228- # Truncate and clean for display
229- sentence_preview = sentence_text .replace ("\n " , " " ).replace (
230- "\r " , ""
179+ rank_str = f"#{ sentence .rank } " if sentence .rank is not None else "N/A"
180+
181+ # Extract sentence preview
182+ if sentence .start_offset is not None and sentence .end_offset is not None :
183+ sentence_text = doc .chunk_content [
184+ sentence .start_offset : sentence .end_offset
185+ ].strip ()
186+ sentence_preview = sentence_text .replace ("\n " , " " ).replace ("\r " , "" )
187+ if len (sentence_preview ) > SENTENCE_PREVIEW_LENGTH :
188+ sentence_preview = (
189+ sentence_preview [: SENTENCE_PREVIEW_LENGTH - 3 ] + "..."
231190 )
232- if len (sentence_preview ) > 50 :
233- sentence_preview = sentence_preview [:47 ] + "..."
234- else :
235- sentence_preview = "[No offset info]"
236-
237- # Format sentence line
238- sentence_line = (
239- f"│ { rank_str :>3} ({ distance_str } ) | { sentence_preview } "
240- )
241- # Pad to 78 chars and add closing border
242- typer .echo (sentence_line .ljust (78 ) + " │" )
243-
244- typer .echo ("└" + "─" * 77 + "┘" )
245- typer .echo ()
191+ else :
192+ sentence_preview = "[No offset info]"
193+
194+ # Format and print sentence line
195+ sentence_line = f"│ { rank_str :>3} ({ distance_str } ) | { sentence_preview } "
196+ typer .echo (sentence_line .ljust (BOX_TOTAL_WIDTH + 1 ) + " │" )
246197
247198
248199class TableDebugFormatter (SearchResultFormatter ):
@@ -312,10 +263,15 @@ def _print_table_row(self, idx: int, doc: DocumentResult) -> None:
312263def get_formatter (
313264 debug : bool = False , table_view : bool = False
314265) -> SearchResultFormatter :
315- """Factory function to get the appropriate formatter."""
266+ """Factory function to get the appropriate formatter.
267+
268+ Args:
269+ debug: Show debug information and sentence details
270+ table_view: Use table format instead of boxed format
271+
272+ Returns:
273+ SearchResultFormatter instance
274+ """
316275 if table_view :
317276 return TableDebugFormatter ()
318- elif debug :
319- return BoxedDebugFormatter ()
320- else :
321- return BoxedFormatter ()
277+ return BoxedFormatter (show_debug = debug )
0 commit comments