Skip to content

Commit 0bb51dd

Browse files
authored
Better URL shortening in browser tab labels (#306765)
* Better URL shortening in browser tab labels * file:// tests
1 parent 184f6ec commit 0bb51dd

3 files changed

Lines changed: 78 additions & 24 deletions

File tree

src/vs/base/common/labels.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,15 @@ export function untildify(path: string, userHome: string): string {
209209
*/
210210
const ellipsis = '\u2026';
211211
const unc = '\\\\';
212+
const urlSchemaRegexp = /^[^:/\\?#]+?:\/\//;
212213
const home = '~';
213-
export function shorten(paths: string[], pathSeparator: string = sep): string[] {
214+
export function shorten(paths: string[], defaultPathSeparator: string = sep): string[] {
214215
const shortenedPaths: string[] = new Array(paths.length);
215216

216217
// for every path
217218
let match = false;
218219
for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) {
220+
let pathSeparator = defaultPathSeparator;
219221
const originalPath = paths[pathIndex];
220222

221223
if (originalPath === '') {
@@ -233,7 +235,11 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[]
233235
// trim for now and concatenate unc path (e.g. \\network) or root path (/etc, ~/etc) later
234236
let prefix = '';
235237
let trimmedPath = originalPath;
236-
if (trimmedPath.indexOf(unc) === 0) {
238+
if (urlSchemaRegexp.test(trimmedPath)) {
239+
prefix = trimmedPath.substr(0, trimmedPath.indexOf('//') + 2);
240+
trimmedPath = trimmedPath.substr(trimmedPath.indexOf('//') + 2);
241+
pathSeparator = '/';
242+
} else if (trimmedPath.indexOf(unc) === 0) {
237243
prefix = trimmedPath.substr(0, trimmedPath.indexOf(unc) + unc.length);
238244
trimmedPath = trimmedPath.substr(trimmedPath.indexOf(unc) + unc.length);
239245
} else if (trimmedPath.indexOf(pathSeparator) === 0) {
@@ -296,7 +302,12 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[]
296302

297303
// add ellipsis at the end if needed
298304
if (start + subpathLength < segments.length) {
299-
result = result + pathSeparator + ellipsis;
305+
// If the last segment is empty, preserve the trailing slash.
306+
if (start + subpathLength === segments.length - 1 && segments[segments.length - 1] === '') {
307+
result = result + pathSeparator;
308+
} else {
309+
result = result + pathSeparator + ellipsis;
310+
}
300311
}
301312

302313
shortenedPaths[pathIndex] = result;

src/vs/base/test/common/labels.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ suite('Labels', () => {
6161
assert.deepStrictEqual(labels.shorten(['a', 'a\\b', 'b']), ['a', 'a\\b', 'b']);
6262
assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b\\c', 'a\\c']), ['.\\', 'a', 'b', 'b\\c', 'a\\c']);
6363
assert.deepStrictEqual(labels.shorten(['src\\vs\\workbench\\parts\\execution\\electron-browser', 'src\\vs\\workbench\\parts\\execution\\electron-browser\\something', 'src\\vs\\workbench\\parts\\terminal\\electron-browser']), ['…\\execution\\electron-browser', '…\\something', '…\\terminal\\…']);
64+
65+
// url paths
66+
assert.deepStrictEqual(labels.shorten(['https://a.com/b', 'C:\\foo\\d']), ['https://a.com/b', 'C:\\…\\d']);
67+
assert.deepStrictEqual(labels.shorten(['https://a.com/x', 'C:\\foo\\x']), ['https://a.com/…', 'C:\\foo\\…']);
68+
assert.deepStrictEqual(labels.shorten(['https://a.com/y/z', 'C:\\foo\\bar\\z']), ['https://a.com/y/…', 'C:\\…\\bar\\…']);
69+
assert.deepStrictEqual(labels.shorten(['file://C:/foo/bar/z', 'C:\\foo\\bar\\z']), ['file://C:/…/bar/z', 'C:\\…\\bar\\z']);
6470
});
6571

6672
(isWindows ? test.skip : test)('shorten - not windows', () => {
@@ -106,6 +112,19 @@ suite('Labels', () => {
106112
assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']), ['a', 'a/b', 'a/b/c', 'd/b/c', 'd/b']);
107113
assert.deepStrictEqual(labels.shorten(['a', 'a/b', 'b']), ['a', 'a/b', 'b']);
108114
assert.deepStrictEqual(labels.shorten(['', 'a', 'b', 'b/c', 'a/c']), ['./', 'a', 'b', 'b/c', 'a/c']);
115+
116+
// url paths
117+
assert.deepStrictEqual(labels.shorten(['https://a.com/b']), ['https://a.com/b']);
118+
assert.deepStrictEqual(labels.shorten(['https://a.com/', 'https://b.com/']), ['https://a.com/', 'https://b.com/']);
119+
assert.deepStrictEqual(labels.shorten(['https://a.com/b', 'https://a.com/c']), ['https://a.com/b', 'https://a.com/c']);
120+
assert.deepStrictEqual(labels.shorten(['https://a.com/x/y', 'https://b.com/x/y']), ['https://a.com/…', 'https://b.com/…']);
121+
assert.deepStrictEqual(labels.shorten(['https://a.com/b', 'https://a.com/b/c']), ['https://a.com/b', 'https://a.com/…/c']);
122+
assert.deepStrictEqual(labels.shorten(['https://a.com/b', 'http://a.com/b']), ['https://a.com/b', 'http://a.com/b']);
123+
assert.deepStrictEqual(labels.shorten(['https://a.com/x/y/z', 'https://a.com/x/w/z']), ['https://a.com/…/y/…', 'https://a.com/…/w/…']);
124+
assert.deepStrictEqual(labels.shorten(['https://a.com/b', '/c/d']), ['https://a.com/b', '/c/d']);
125+
assert.deepStrictEqual(labels.shorten(['https://a.com/x', '/c/x']), ['https://a.com/…', '/c/x']);
126+
assert.deepStrictEqual(labels.shorten(['https://a.com/x/y', '/c/x/y']), ['https://a.com/…', '/c/x/…']);
127+
assert.deepStrictEqual(labels.shorten(['file:///foo/bar/z', '/foo/bar/z']), ['file:///foo/bar/z', '/foo/bar/z']);
109128
});
110129

111130
test('template', () => {

src/vs/workbench/contrib/browserView/common/browserEditorInput.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js';
1010
import { generateUuid } from '../../../../base/common/uuid.js';
1111
import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';
1212
import { IBrowserEditorViewState } from './browserView.js';
13-
import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js';
13+
import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput, Verbosity } from '../../../common/editor.js';
1414
import { EditorInput } from '../../../common/editor/editorInput.js';
1515
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
1616
import { TAB_ACTIVE_FOREGROUND } from '../../../common/theme.js';
@@ -21,6 +21,7 @@ import { hasKey } from '../../../../base/common/types.js';
2121
import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';
2222
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
2323
import { logBrowserOpen } from '../../../../platform/browserView/common/browserViewTelemetry.js';
24+
import { LRUCachedFunction } from '../../../../base/common/cache.js';
2425

2526
const LOADING_SPINNER_SVG = (color: string | undefined) => `
2627
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
@@ -184,32 +185,55 @@ export class BrowserEditorInput extends EditorInput {
184185
}
185186

186187
override getName(): string {
187-
return truncate(this.getTitle(), MAX_TITLE_LENGTH);
188+
const hasTitle = this._model ? !!this._model.title : !!this._initialData.title;
189+
const name = hasTitle ? this.title! : this.getDescription(Verbosity.SHORT) || BrowserEditorInput.DEFAULT_LABEL;
190+
return truncate(name, MAX_TITLE_LENGTH);
188191
}
189192

190-
override getTitle(): string {
191-
// Use model data if available, otherwise fall back to initial data
192-
if (this._model && this._model.url) {
193-
if (this._model.title) {
194-
return this._model.title;
195-
}
196-
// Model exists, use its URL for authority
197-
const authority = URI.parse(this._model.url).authority;
198-
return authority || BrowserEditorInput.DEFAULT_LABEL;
199-
}
200-
// Model not created yet, use initial data
201-
if (this._initialData.title) {
202-
return this._initialData.title;
203-
}
204-
const url = this._initialData.url ?? '';
205-
const authority = URI.parse(url).authority;
206-
return authority || BrowserEditorInput.DEFAULT_LABEL;
193+
override getTitle(verbosity = Verbosity.MEDIUM): string {
194+
const hasTitle = this._model ? !!this._model.title : !!this._initialData.title;
195+
const description = this.getDescription(verbosity);
196+
const title = hasTitle ? `${this.title} (${description})` : description;
197+
return title || BrowserEditorInput.DEFAULT_LABEL;
207198
}
208199

209-
override getDescription(): string | undefined {
210-
return this.url;
200+
override getDescription(verbosity = Verbosity.MEDIUM): string | undefined {
201+
return this.url && this.getURLTitles.get(this.url)[verbosity];
211202
}
212203

204+
private readonly getURLTitles = new LRUCachedFunction((url: string) => {
205+
let _parsed: URI | undefined = undefined;
206+
let _short: string | undefined = undefined;
207+
let _medium: string | undefined = undefined;
208+
let _long: string | undefined = undefined;
209+
function getParsed() {
210+
if (!_parsed) {
211+
_parsed = URI.parse(url);
212+
}
213+
return _parsed;
214+
}
215+
return {
216+
get [Verbosity.SHORT]() {
217+
if (!_short) {
218+
_short = getParsed().authority;
219+
}
220+
return _short;
221+
},
222+
get [Verbosity.MEDIUM]() {
223+
if (!_medium) {
224+
_medium = getParsed().with({ query: '', fragment: '' }).toString();
225+
}
226+
return _medium;
227+
},
228+
get [Verbosity.LONG]() {
229+
if (!_long) {
230+
_long = getParsed().with({ fragment: '' }).toString();
231+
}
232+
return _long;
233+
}
234+
};
235+
});
236+
213237
override canReopen(): boolean {
214238
return true;
215239
}

0 commit comments

Comments
 (0)