src/vs/platform/browserView/common/browserHistory.ts
364 LOC · 350 covered · 14 uncovered · 97 ranges · 58 concepts · 43 introducers · 26 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
browserHistory.ts ×26
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../base/common/event.js';
import { StringSHA1 } from '../../../base/common/hash.js';
import { Disposable } from '../../../base/common/lifecycle.js';
/**
* On-disk shape of a single history entry.
* BACKWARDS COMPATIBILE. When evolving this interface, ensure older versions can still be handled gracefully.
*/
export interface ISerializedBrowserHistoryEntry {
readonly id: number;
readonly url: string;
/** Epoch ms when the entry was most recently visited. */
readonly time: number;
readonly title: string;
/** Content hash key into the sibling favicons map. */
readonly icon?: string;
/**
* Set when the navigation was initiated by the user (typing in the URL
* bar, picking a suggestion, opening a new tab with a URL) rather than by
* page script or link clicks. Always omitted when false to keep entries
* small.
*/
readonly explicit?: true;
}
/**
* In-memory representation of a history entry. Currently identical to the
* on-disk shape; the split exists so future in-memory-only fields can be
* added here without changing the wire format.
*/
export interface IBrowserHistoryEntry extends ISerializedBrowserHistoryEntry { }
export interface IBrowserHistoryUpdate {
/** URL may be updated e.g. during a redirect or in-page navigation. */
readonly url?: string;
readonly title?: string;
/** Favicon data URI; hashed and deduped against the sibling favicons store. Pass `null` to explicitly clear. */
readonly favicon?: string | null;
}
/**
* Handle returned by {@link BrowserHistoryStore.add}. `update` and `delete`
* are no-ops once the underlying entry has been evicted.
*/
export interface IBrowserHistoryItemHandle {
readonly id: number;
update(patch: IBrowserHistoryUpdate): void;
delete(): void;
}
/** Returned by {@link BrowserHistoryStore.add} when the store is disabled (max entries = 0). */
const NOOP_HANDLE: IBrowserHistoryItemHandle = Object.freeze({
id: -1,
update: () => { },
delete: () => { },
});
/**
* On-disk shape of an entries snapshot. See {@link ISerializedBrowserHistoryEntry}
* for the backwards-compatibility rules; the same constraints apply here.
*/
export interface ISerializedBrowserHistoryEntriesSnapshot {
readonly items: readonly ISerializedBrowserHistoryEntry[];
}
/**
* On-disk shape of a favicons snapshot. See {@link ISerializedBrowserHistoryEntry}
* for the backwards-compatibility rules; the same constraints apply here.
*/
export interface ISerializedBrowserFaviconsSnapshot {
/** Map from content hash to data URI. */
readonly map: Readonly<Record<string, string>>;
}
const DEFAULT_MAX_ENTRIES = 200;
export class BrowserHistoryEntriesStore extends Disposable {
private _nextId: number = 1;
private _items: IBrowserHistoryEntry[] = [];
private _maxEntries: number;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(maxEntries: number = DEFAULT_MAX_ENTRIES) {
this._maxEntries = maxEntries;
}
get items(): readonly IBrowserHistoryEntry[] {
}
get maxEntries(): number {
}
setMaxEntries(max: number): void {
if (max < 0 || max === this._maxEntries) {
return;
}
this._maxEntries = max;
if (this._evictIfNeeded()) {
this._onDidChange.fire();
}
}
add(url: string, title: string, faviconHash: string | undefined, userInitiated: boolean): IBrowserHistoryEntry {
? { id: this._nextId++, url, time: Date.now(), title, icon: faviconHash, explicit: true }
browserHistory.ts ×1
: { id: this._nextId++, url, time: Date.now(), title, icon: faviconHash };
browserHistory.ts ×2
this._items.push(entry);
this._evictIfNeeded();
this._onDidChange.fire();
return entry;
}
update(id: number, patch: { url?: string; title?: string; faviconHash?: string | null }): boolean {
if (idx === -1) {
}
const nextTitle = patch.title && patch.title.length > 0 ? patch.title : existing.title;
browserHistory.ts ×4
const nextUrl = patch.url && patch.url.length > 0 ? patch.url : existing.url;
// Distinguish "leave alone" (undefined) from explicit clear (null).
const nextFaviconHash = patch.faviconHash === undefined
const nextTime = patch.url ? Date.now() : existing.time;
if (nextUrl === existing.url && nextTitle === existing.title && nextFaviconHash === existing.icon && nextTime === existing.time) {
}
this._items[idx] = { ...existing, url: nextUrl, title: nextTitle, icon: nextFaviconHash, time: nextTime };
browserHistory.ts ×1
this._onDidChange.fire();
return true;
delete(id: number): boolean {
if (idx === -1) {
}
this._onDidChange.fire();
return true;
}
clear(): void {
}
this._nextId = 1;
this._onDidChange.fire();
}
serialize(): ISerializedBrowserHistoryEntriesSnapshot {
}
hydrate(snapshot: ISerializedBrowserHistoryEntriesSnapshot | undefined): void {
this._nextId = 1;
if (snapshot && Array.isArray(snapshot.items)) {
if (isValidEntry(e)) {
this._items.push(e);
}
}
// Restored ids must not collide with future adds.
for (const e of this._items) {
if (e.id >= this._nextId) {
this._nextId = e.id + 1;
}
}
this._evictIfNeeded();
}
}
private _indexOf(id: number): number {
// Walk newest-first; mutations target the just-added entry in the common case.
browserHistory.ts ×3
for (let i = this._items.length - 1; i >= 0; i--) {
if (this._items[i].id === id) {
}
private _evictIfNeeded(): boolean {
return true;
}
}
/**
* Lives separately from {@link BrowserHistoryEntriesStore} so the (large)
* favicon map is only rewritten when an image is added or removed, not on
* every navigation.
*/
export class BrowserFaviconsStore extends Disposable {
private readonly _byHash = new Map<string, string>();
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
get(hash: string): string | undefined {
}
register(dataUri: string): string {
sha.update(dataUri);
const hash = sha.digest();
if (!this._byHash.has(hash)) {
this._byHash.set(hash, dataUri);
this._onDidChange.fire();
}
return hash;
}
gc(referenced: ReadonlySet<string>): void {
}
for (const hash of this._byHash.keys()) {
if (!referenced.has(hash)) {
changed = true;
}
if (changed) {
}
clear(): void {
return;
}
this._byHash.clear();
this._onDidChange.fire();
serialize(): ISerializedBrowserFaviconsSnapshot {
}
hydrate(snapshot: ISerializedBrowserFaviconsSnapshot | undefined): void {
if (snapshot?.map && typeof snapshot.map === 'object') {
for (const [k, v] of Object.entries(snapshot.map)) {
if (typeof v === 'string') {
this._byHash.set(k, v);
}
}
}
this._onDidChange.fire();
}
/**
* Per-session browser history. The two sub-stores are exposed directly so
* persistence layers can flush them independently.
*/
export class BrowserHistoryStore extends Disposable {
readonly entries: BrowserHistoryEntriesStore;
readonly favicons: BrowserFaviconsStore;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(maxEntries?: number) {
this.entries = this._register(new BrowserHistoryEntriesStore(maxEntries));
this.favicons = this._register(new BrowserFaviconsStore());
this._register(this.entries.onDidChange(() => {
this._onDidChange.fire();
this._register(this.favicons.onDidChange(() => this._onDidChange.fire()));
}
add(url: string, title: string, favicon?: string, userInitiated = false): IBrowserHistoryItemHandle {
return NOOP_HANDLE;
}
const faviconHash = favicon ? this.favicons.register(favicon) : undefined;
browserHistory.ts ×5
const entry = this.entries.add(url, title, faviconHash, userInitiated);
return this._handleFor(entry.id);
}
setMaxEntries(max: number): void {
this.entries.setMaxEntries(max);
}
clear(): void {
this.favicons.clear();
}
private _handleFor(id: number): IBrowserHistoryItemHandle {
id,
update: patch => {
const next: { url?: string; title?: string; faviconHash?: string | null } = {};
browserHistory.ts ×4
if (patch.url !== undefined) {
next.url = patch.url;
}
}
// null is the explicit-clear sentinel; a data URI registers and references it.
browserHistory.ts ×1
next.faviconHash = patch.favicon === null ? null : this.favicons.register(patch.favicon);
}
},
},
}
private _gcFavicons(): void {
for (const e of this.entries.items) {
if (e.icon) {
}
this.favicons.gc(referenced);
}
function isValidEntry(value: unknown): value is ISerializedBrowserHistoryEntry {
browserHistory.ts ×3
if (!value || typeof value !== 'object') {
}
return typeof e.id === 'number'
&& typeof e.url === 'string'
&& typeof e.time === 'number'
&& typeof e.title === 'string'
&& (e.icon === undefined || typeof e.icon === 'string')
&& (e.explicit === undefined || e.explicit === true);
}