Skip to content

Commit 275d091

Browse files
committed
Full list: Stop double-showing the extension
- Stripped the ext from the name column so `photo.jpg` shows as `photo` + `jpg`, not `photo.jpg` + `jpg`. - During inline rename, the editor now spans the name + ext grid cells for more typing room. - Extracted `getDisplayExtension()` from `FullList.svelte` and added `getDisplayName()` in `full-list-utils.ts`, with unit tests for dotfiles, multi-dot names, trailing dots, and directories.
1 parent 383d9b9 commit 275d091

3 files changed

Lines changed: 95 additions & 26 deletions

File tree

apps/desktop/src/lib/file-explorer/views/FullList.svelte

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
buildFileSizeTooltip,
2828
getDisplaySize,
2929
hasSizeMismatch,
30+
getDisplayExtension,
31+
getDisplayName,
3032
} from './full-list-utils'
3133
import {
3234
getRowHeight,
@@ -118,16 +120,6 @@
118120
// Measures multiple sample dates to find the maximum width needed.
119121
const dateColumnWidth = $derived(measureDateColumnWidth(formatDateTime))
120122
121-
/** Extracts display extension from a filename (no dot). Matches Rust sorting logic:
122-
* dotfiles without secondary dot → empty, no extension → empty, otherwise last segment. */
123-
function getDisplayExtension(name: string, isDirectory: boolean): string {
124-
if (isDirectory) return ''
125-
if (name.startsWith('.') && !name.slice(1).includes('.')) return ''
126-
const dotPos = name.lastIndexOf('.')
127-
if (dotPos <= 0 || dotPos === name.length - 1) return ''
128-
return name.slice(dotPos + 1)
129-
}
130-
131123
// Size display mode (smart/logical/physical)
132124
const sizeDisplayMode = $derived(getSizeDisplayMode())
133125
@@ -467,23 +459,25 @@
467459
>
468460
<FileIcon {file} {syncIcon} />
469461
{#if renameState?.active && renameState.target?.index === globalIndex}
470-
<InlineRenameEditor
471-
value={renameState.currentName}
472-
severity={renameState.validation.severity}
473-
shaking={renameState.shaking}
474-
ariaLabel={`Rename ${renameState.target.originalName}`}
475-
ariaInvalid={renameState.validation.severity === 'error'}
476-
validationMessage={renameState.validation.message}
477-
focusTrigger={renameState.focusTrigger}
478-
onInput={(v: string) => onRenameInput?.(v)}
479-
onSubmit={() => onRenameSubmit?.()}
480-
onCancel={() => onRenameCancel?.()}
481-
onShakeEnd={() => onRenameShakeEnd?.()}
482-
/>
462+
<div class="col-rename">
463+
<InlineRenameEditor
464+
value={renameState.currentName}
465+
severity={renameState.validation.severity}
466+
shaking={renameState.shaking}
467+
ariaLabel={`Rename ${renameState.target.originalName}`}
468+
ariaInvalid={renameState.validation.severity === 'error'}
469+
validationMessage={renameState.validation.message}
470+
focusTrigger={renameState.focusTrigger}
471+
onInput={(v: string) => onRenameInput?.(v)}
472+
onSubmit={() => onRenameSubmit?.()}
473+
onCancel={() => onRenameCancel?.()}
474+
onShakeEnd={() => onRenameShakeEnd?.()}
475+
/>
476+
</div>
483477
{:else}
484-
<span class="col-name">{file.name}</span>
478+
<span class="col-name">{getDisplayName(file.name, file.isDirectory)}</span>
479+
<span class="col-ext">{getDisplayExtension(file.name, file.isDirectory)}</span>
485480
{/if}
486-
<span class="col-ext">{getDisplayExtension(file.name, file.isDirectory)}</span>
487481
<span
488482
class="col-size"
489483
use:tooltip={file.isDirectory
@@ -631,6 +625,13 @@
631625
white-space: nowrap;
632626
}
633627
628+
/* During rename, span the name + ext columns for more editing room */
629+
.col-rename {
630+
grid-column: 2 / span 2;
631+
min-width: 0;
632+
height: 100%;
633+
}
634+
634635
.col-ext {
635636
overflow: hidden;
636637
text-overflow: ellipsis;

apps/desktop/src/lib/file-explorer/views/full-list-utils.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
* Tests for full-list-utils.ts
33
*/
44
import { describe, it, expect, vi } from 'vitest'
5-
import { getVisibleItemsCount, FULL_LIST_ROW_HEIGHT, getVirtualizationBufferRows } from './full-list-utils'
5+
import {
6+
getVisibleItemsCount,
7+
FULL_LIST_ROW_HEIGHT,
8+
getVirtualizationBufferRows,
9+
getDisplayExtension,
10+
getDisplayName,
11+
} from './full-list-utils'
612

713
// Mock the settings store
814
vi.mock('$lib/settings/settings-store', () => ({
@@ -48,3 +54,40 @@ describe('getVisibleItemsCount', () => {
4854
expect(getVisibleItemsCount(410, 40)).toBe(11) // ceil(410 / 40) = 11
4955
})
5056
})
57+
58+
describe('getDisplayExtension / getDisplayName', () => {
59+
it('splits a plain filename', () => {
60+
expect(getDisplayExtension('photo.jpg', false)).toBe('jpg')
61+
expect(getDisplayName('photo.jpg', false)).toBe('photo')
62+
})
63+
64+
it('keeps dotfiles intact (no secondary dot)', () => {
65+
expect(getDisplayExtension('.bashrc', false)).toBe('')
66+
expect(getDisplayName('.bashrc', false)).toBe('.bashrc')
67+
})
68+
69+
it('treats only the last segment of a multi-dot name as the extension', () => {
70+
expect(getDisplayExtension('file.tar.gz', false)).toBe('gz')
71+
expect(getDisplayName('file.tar.gz', false)).toBe('file.tar')
72+
})
73+
74+
it('returns empty ext for directories and keeps the full name', () => {
75+
expect(getDisplayExtension('My Folder.d', true)).toBe('')
76+
expect(getDisplayName('My Folder.d', true)).toBe('My Folder.d')
77+
})
78+
79+
it('keeps trailing-dot names intact', () => {
80+
expect(getDisplayExtension('foo.', false)).toBe('')
81+
expect(getDisplayName('foo.', false)).toBe('foo.')
82+
})
83+
84+
it('handles names with no dot at all', () => {
85+
expect(getDisplayExtension('README', false)).toBe('')
86+
expect(getDisplayName('README', false)).toBe('README')
87+
})
88+
89+
it('splits a dotfile with a secondary dot', () => {
90+
expect(getDisplayExtension('.env.local', false)).toBe('local')
91+
expect(getDisplayName('.env.local', false)).toBe('.env')
92+
})
93+
})

apps/desktop/src/lib/file-explorer/views/full-list-utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ export function getVisibleItemsCount(containerHeight: number, rowHeight: number
1919
return Math.ceil(containerHeight / rowHeight)
2020
}
2121

22+
// ============================================================================
23+
// Name/Extension Split
24+
// ============================================================================
25+
26+
/**
27+
* Extracts the display extension from a filename (no dot). Matches Rust sorting logic:
28+
* dotfiles without a secondary dot → empty, no extension → empty, otherwise last segment.
29+
*/
30+
export function getDisplayExtension(name: string, isDirectory: boolean): string {
31+
if (isDirectory) return ''
32+
if (name.startsWith('.') && !name.slice(1).includes('.')) return ''
33+
const dotPos = name.lastIndexOf('.')
34+
if (dotPos <= 0 || dotPos === name.length - 1) return ''
35+
return name.slice(dotPos + 1)
36+
}
37+
38+
/**
39+
* Returns the filename with the display extension (and its separating dot) stripped,
40+
* so the name and extension columns don't duplicate the extension.
41+
*/
42+
export function getDisplayName(name: string, isDirectory: boolean): string {
43+
const ext = getDisplayExtension(name, isDirectory)
44+
return ext ? name.slice(0, -(ext.length + 1)) : name
45+
}
46+
2247
// ============================================================================
2348
// Date Column Width Measurement
2449
// ============================================================================

0 commit comments

Comments
 (0)