Skip to content

Commit 96fcd6e

Browse files
claudesinelaw
authored andcommitted
fix: improve git find file popup display (#707)
The file entries in the git find file popup were being truncated to very short strings (30 chars) because the suggestion renderer used fixed column widths designed for the command palette. Changes: 1. Dynamically calculate name column width based on available space - When no keybinding/source columns are needed, use up to 60% of available width for the name column - Hide keybinding/source columns entirely when not used 2. Improve truncation for file paths - For paths containing '/' or '\', truncate from the beginning instead of the end to preserve the filename - Example: "src/very/long/path/file.rs" -> "…long/path/file.rs" instead of "src/very/long/path/fi…"
1 parent c3247c4 commit 96fcd6e

1 file changed

Lines changed: 183 additions & 122 deletions

File tree

src/view/ui/suggestions.rs

Lines changed: 183 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,34 @@ impl SuggestionsRenderer {
8686
let column_spacing = 2;
8787
let available_width = inner_area.width as usize;
8888

89+
// Check if any visible suggestions have keybinding or source
90+
// If not, we can use more space for the name column
91+
let has_keybinding = visible_suggestions.iter().any(|s| s.keybinding.is_some());
92+
let has_source = visible_suggestions.iter().any(|s| s.source.is_some());
93+
8994
// Fixed column widths for consistent layout
90-
let name_column_width = 30; // Fixed width for command names
91-
let keybinding_column_width = 12; // Fixed width for keybindings (e.g., "Ctrl+Shift+P")
92-
let source_column_width = 15; // Fixed width for source (e.g., "builtin", "live_grep")
95+
let keybinding_column_width = if has_keybinding { 12 } else { 0 };
96+
let source_column_width = if has_source { 15 } else { 0 };
97+
98+
// Calculate name column width dynamically based on available space
99+
// Reserve space for: left_margin + name + spacing + keybinding + spacing + desc + spacing + source
100+
let reserved_for_other_columns = left_margin
101+
+ column_spacing // after name
102+
+ keybinding_column_width
103+
+ (if has_keybinding { column_spacing } else { 0 }) // after keybinding
104+
+ column_spacing // after desc
105+
+ source_column_width;
106+
107+
// Give name column a reasonable portion of remaining space
108+
// Minimum 30, but can expand if there's room and no keybinding/source
109+
let base_name_width = 30;
110+
let name_column_width = if !has_keybinding && !has_source {
111+
// For file finders etc., use up to 60% of available width for name
112+
let max_name_width = (available_width * 60 / 100).max(base_name_width);
113+
max_name_width.min(available_width.saturating_sub(reserved_for_other_columns))
114+
} else {
115+
base_name_width
116+
};
93117

94118
for (idx, suggestion) in visible_suggestions.iter().enumerate() {
95119
let actual_idx = start_idx + idx;
@@ -141,82 +165,110 @@ impl SuggestionsRenderer {
141165
let name_text = if name_visual_width > name_column_width {
142166
// Truncate name by visual width
143167
let truncate_at = name_column_width.saturating_sub(1); // -1 for "…"
144-
let mut width = 0;
145-
let truncated: String = name
146-
.chars()
147-
.take_while(|ch| {
148-
let w = char_width(*ch);
149-
if width + w <= truncate_at {
150-
width += w;
151-
true
168+
169+
// For file paths (containing '/'), truncate from the beginning
170+
// to preserve the filename which is usually at the end
171+
if name.contains('/') || name.contains('\\') {
172+
// Calculate how many chars to skip from the beginning
173+
let mut total_width = 0;
174+
let char_widths: Vec<(char, usize)> =
175+
name.chars().map(|ch| (ch, char_width(ch))).collect();
176+
177+
// Find where to start to fit within truncate_at
178+
let mut start_idx = 0;
179+
for (i, &(_, w)) in char_widths.iter().enumerate().rev() {
180+
if total_width + w <= truncate_at {
181+
total_width += w;
182+
start_idx = i;
152183
} else {
153-
false
184+
break;
154185
}
155-
})
156-
.collect();
157-
format!("{}…", truncated)
158-
} else {
159-
name.clone()
160-
};
161-
spans.push(Span::styled(name_text.clone(), base_style));
162-
let name_display_width = str_width(&name_text);
163-
let name_padding = name_column_width.saturating_sub(name_display_width);
164-
if name_padding > 0 {
165-
spans.push(Span::styled(" ".repeat(name_padding), base_style));
166-
}
167-
168-
// Spacing before keybinding column
169-
spans.push(Span::styled(" ".repeat(column_spacing), base_style));
170-
171-
// Column 2: Keyboard shortcut (fixed width)
172-
let keybinding_style = if suggestion.disabled {
173-
base_style
174-
} else if is_selected {
175-
Style::default()
176-
.fg(theme.help_key_fg)
177-
.bg(theme.suggestion_selected_bg)
178-
} else if is_hovered {
179-
Style::default()
180-
.fg(theme.help_key_fg)
181-
.bg(theme.menu_hover_bg)
182-
} else {
183-
Style::default()
184-
.fg(theme.line_number_fg)
185-
.bg(theme.suggestion_bg)
186-
};
186+
}
187187

188-
if let Some(keybinding) = &suggestion.keybinding {
189-
let kb_visual_width = str_width(keybinding);
190-
let kb_text = if kb_visual_width > keybinding_column_width {
191-
// Truncate keybinding by visual width
188+
let truncated: String =
189+
char_widths[start_idx..].iter().map(|(ch, _)| *ch).collect();
190+
format!("…{}", truncated)
191+
} else {
192+
// For non-paths, truncate from the end as before
192193
let mut width = 0;
193-
keybinding
194+
let truncated: String = name
194195
.chars()
195196
.take_while(|ch| {
196197
let w = char_width(*ch);
197-
if width + w <= keybinding_column_width {
198+
if width + w <= truncate_at {
198199
width += w;
199200
true
200201
} else {
201202
false
202203
}
203204
})
204-
.collect()
205+
.collect();
206+
format!("{}…", truncated)
207+
}
208+
} else {
209+
name.clone()
210+
};
211+
spans.push(Span::styled(name_text.clone(), base_style));
212+
let name_display_width = str_width(&name_text);
213+
let name_padding = name_column_width.saturating_sub(name_display_width);
214+
if name_padding > 0 {
215+
spans.push(Span::styled(" ".repeat(name_padding), base_style));
216+
}
217+
218+
// Column 2: Keyboard shortcut (only if any suggestions have keybindings)
219+
if has_keybinding {
220+
// Spacing before keybinding column
221+
spans.push(Span::styled(" ".repeat(column_spacing), base_style));
222+
223+
let keybinding_style = if suggestion.disabled {
224+
base_style
225+
} else if is_selected {
226+
Style::default()
227+
.fg(theme.help_key_fg)
228+
.bg(theme.suggestion_selected_bg)
229+
} else if is_hovered {
230+
Style::default()
231+
.fg(theme.help_key_fg)
232+
.bg(theme.menu_hover_bg)
205233
} else {
206-
keybinding.clone()
234+
Style::default()
235+
.fg(theme.line_number_fg)
236+
.bg(theme.suggestion_bg)
207237
};
208-
spans.push(Span::styled(kb_text.clone(), keybinding_style));
209-
let kb_display_width = str_width(&kb_text);
210-
let kb_padding = keybinding_column_width.saturating_sub(kb_display_width);
211-
if kb_padding > 0 {
212-
spans.push(Span::styled(" ".repeat(kb_padding), base_style));
238+
239+
if let Some(keybinding) = &suggestion.keybinding {
240+
let kb_visual_width = str_width(keybinding);
241+
let kb_text = if kb_visual_width > keybinding_column_width {
242+
// Truncate keybinding by visual width
243+
let mut width = 0;
244+
keybinding
245+
.chars()
246+
.take_while(|ch| {
247+
let w = char_width(*ch);
248+
if width + w <= keybinding_column_width {
249+
width += w;
250+
true
251+
} else {
252+
false
253+
}
254+
})
255+
.collect()
256+
} else {
257+
keybinding.clone()
258+
};
259+
spans.push(Span::styled(kb_text.clone(), keybinding_style));
260+
let kb_display_width = str_width(&kb_text);
261+
let kb_padding = keybinding_column_width.saturating_sub(kb_display_width);
262+
if kb_padding > 0 {
263+
spans.push(Span::styled(" ".repeat(kb_padding), base_style));
264+
}
265+
} else {
266+
// No keybinding for this command, pad the column
267+
spans.push(Span::styled(
268+
" ".repeat(keybinding_column_width),
269+
base_style,
270+
));
213271
}
214-
} else {
215-
// No keybinding for this command, pad the column
216-
spans.push(Span::styled(
217-
" ".repeat(keybinding_column_width),
218-
base_style,
219-
));
220272
}
221273

222274
// Spacing before description column
@@ -226,11 +278,18 @@ impl SuggestionsRenderer {
226278
let fixed_columns_width = left_margin
227279
+ name_column_width
228280
+ column_spacing
229-
+ keybinding_column_width
230-
+ column_spacing;
281+
+ (if has_keybinding {
282+
keybinding_column_width + column_spacing
283+
} else {
284+
0
285+
});
231286

232-
// Reserve space for source column at the end
233-
let source_reserved = column_spacing + source_column_width;
287+
// Reserve space for source column at the end (only if showing sources)
288+
let source_reserved = if has_source {
289+
column_spacing + source_column_width
290+
} else {
291+
0
292+
};
234293

235294
// Column 3: Description (flexible width, leaves room for source)
236295
if let Some(desc) = &suggestion.description {
@@ -279,65 +338,67 @@ impl SuggestionsRenderer {
279338
}
280339
}
281340

282-
// Spacing before source column
283-
spans.push(Span::styled(" ".repeat(column_spacing), base_style));
341+
// Column 4: Source (only if any suggestions have source info)
342+
if has_source {
343+
// Spacing before source column
344+
spans.push(Span::styled(" ".repeat(column_spacing), base_style));
284345

285-
// Column 4: Source (right-aligned, fixed width)
286-
let source_style = if suggestion.disabled {
287-
base_style
288-
} else if is_selected {
289-
Style::default()
290-
.fg(theme.line_number_fg)
291-
.bg(theme.suggestion_selected_bg)
292-
.add_modifier(Modifier::DIM)
293-
} else if is_hovered {
294-
Style::default()
295-
.fg(theme.line_number_fg)
296-
.bg(theme.menu_hover_bg)
297-
.add_modifier(Modifier::DIM)
298-
} else {
299-
Style::default()
300-
.fg(theme.line_number_fg)
301-
.bg(theme.suggestion_bg)
302-
.add_modifier(Modifier::DIM)
303-
};
304-
305-
if let Some(source) = &suggestion.source {
306-
let source_text = match source {
307-
CommandSource::Builtin => "builtin".to_string(),
308-
CommandSource::Plugin(name) => name.clone(),
309-
};
310-
let source_visual_width = str_width(&source_text);
311-
let source_display = if source_visual_width > source_column_width {
312-
// Truncate source by visual width
313-
let truncate_at = source_column_width.saturating_sub(1); // -1 for "…"
314-
let mut width = 0;
315-
let truncated: String = source_text
316-
.chars()
317-
.take_while(|ch| {
318-
let w = char_width(*ch);
319-
if width + w <= truncate_at {
320-
width += w;
321-
true
322-
} else {
323-
false
324-
}
325-
})
326-
.collect();
327-
format!("{}…", truncated)
346+
let source_style = if suggestion.disabled {
347+
base_style
348+
} else if is_selected {
349+
Style::default()
350+
.fg(theme.line_number_fg)
351+
.bg(theme.suggestion_selected_bg)
352+
.add_modifier(Modifier::DIM)
353+
} else if is_hovered {
354+
Style::default()
355+
.fg(theme.line_number_fg)
356+
.bg(theme.menu_hover_bg)
357+
.add_modifier(Modifier::DIM)
328358
} else {
329-
source_text
359+
Style::default()
360+
.fg(theme.line_number_fg)
361+
.bg(theme.suggestion_bg)
362+
.add_modifier(Modifier::DIM)
330363
};
331-
let source_display_width = str_width(&source_display);
332-
// Right-align the source text within its column
333-
let source_padding = source_column_width.saturating_sub(source_display_width);
334-
if source_padding > 0 {
335-
spans.push(Span::styled(" ".repeat(source_padding), base_style));
364+
365+
if let Some(source) = &suggestion.source {
366+
let source_text = match source {
367+
CommandSource::Builtin => "builtin".to_string(),
368+
CommandSource::Plugin(name) => name.clone(),
369+
};
370+
let source_visual_width = str_width(&source_text);
371+
let source_display = if source_visual_width > source_column_width {
372+
// Truncate source by visual width
373+
let truncate_at = source_column_width.saturating_sub(1); // -1 for "…"
374+
let mut width = 0;
375+
let truncated: String = source_text
376+
.chars()
377+
.take_while(|ch| {
378+
let w = char_width(*ch);
379+
if width + w <= truncate_at {
380+
width += w;
381+
true
382+
} else {
383+
false
384+
}
385+
})
386+
.collect();
387+
format!("{}…", truncated)
388+
} else {
389+
source_text
390+
};
391+
let source_display_width = str_width(&source_display);
392+
// Right-align the source text within its column
393+
let source_padding = source_column_width.saturating_sub(source_display_width);
394+
if source_padding > 0 {
395+
spans.push(Span::styled(" ".repeat(source_padding), base_style));
396+
}
397+
spans.push(Span::styled(source_display, source_style));
398+
} else {
399+
// No source info, just pad
400+
spans.push(Span::styled(" ".repeat(source_column_width), base_style));
336401
}
337-
spans.push(Span::styled(source_display, source_style));
338-
} else {
339-
// No source info, just pad
340-
spans.push(Span::styled(" ".repeat(source_column_width), base_style));
341402
}
342403

343404
// Fill any remaining space with background (shouldn't be needed but safe)

0 commit comments

Comments
 (0)