mirror of
https://github.com/Microsoft/vscode
synced 2024-11-05 18:29:38 +00:00
web - first cut user data provider storage service
This commit is contained in:
parent
c212dda010
commit
a65e9ca420
14 changed files with 650 additions and 543 deletions
318
src/vs/base/parts/storage/common/storage.ts
Normal file
318
src/vs/base/parts/storage/common/storage.ts
Normal file
|
@ -0,0 +1,318 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
|
||||
export enum StorageHint {
|
||||
|
||||
// A hint to the storage that the storage
|
||||
// does not exist on disk yet. This allows
|
||||
// the storage library to improve startup
|
||||
// time by not checking the storage for data.
|
||||
STORAGE_DOES_NOT_EXIST
|
||||
}
|
||||
|
||||
export interface IStorageOptions {
|
||||
hint?: StorageHint;
|
||||
}
|
||||
|
||||
export interface IUpdateRequest {
|
||||
insert?: Map<string, string>;
|
||||
delete?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IStorageItemsChangeEvent {
|
||||
items: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
|
||||
|
||||
getItems(): Promise<Map<string, string>>;
|
||||
updateItems(request: IUpdateRequest): Promise<void>;
|
||||
|
||||
close(recovery?: () => Map<string, string>): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IStorage extends IDisposable {
|
||||
|
||||
readonly items: Map<string, string>;
|
||||
readonly size: number;
|
||||
readonly onDidChangeStorage: Event<string>;
|
||||
|
||||
init(): Promise<void>;
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
|
||||
getBoolean(key: string, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
|
||||
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
|
||||
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
enum StorageState {
|
||||
None,
|
||||
Initialized,
|
||||
Closed
|
||||
}
|
||||
|
||||
export class Storage extends Disposable implements IStorage {
|
||||
|
||||
private static readonly DEFAULT_FLUSH_DELAY = 100;
|
||||
|
||||
private readonly _onDidChangeStorage: Emitter<string> = this._register(new Emitter<string>());
|
||||
get onDidChangeStorage(): Event<string> { return this._onDidChangeStorage.event; }
|
||||
|
||||
private state = StorageState.None;
|
||||
|
||||
private cache: Map<string, string> = new Map<string, string>();
|
||||
|
||||
private flushDelayer: ThrottledDelayer<void>;
|
||||
|
||||
private pendingDeletes: Set<string> = new Set<string>();
|
||||
private pendingInserts: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
protected database: IStorageDatabase,
|
||||
private options: IStorageOptions = Object.create(null)
|
||||
) {
|
||||
super();
|
||||
|
||||
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
|
||||
}
|
||||
|
||||
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
|
||||
// items that change external require us to update our
|
||||
// caches with the values. we just accept the value and
|
||||
// emit an event if there is a change.
|
||||
e.items.forEach((value, key) => this.accept(key, value));
|
||||
}
|
||||
|
||||
private accept(key: string, value: string): void {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return; // Return early if we are already closed
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
|
||||
// Item got removed, check for deletion
|
||||
if (isUndefinedOrNull(value)) {
|
||||
changed = this.cache.delete(key);
|
||||
}
|
||||
|
||||
// Item got updated, check for change
|
||||
else {
|
||||
const currentValue = this.cache.get(key);
|
||||
if (currentValue !== value) {
|
||||
this.cache.set(key, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Signal to outside listeners
|
||||
if (changed) {
|
||||
this._onDidChangeStorage.fire(key);
|
||||
}
|
||||
}
|
||||
|
||||
get items(): Map<string, string> {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.state !== StorageState.None) {
|
||||
return Promise.resolve(); // either closed or already initialized
|
||||
}
|
||||
|
||||
this.state = StorageState.Initialized;
|
||||
|
||||
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
|
||||
// return early if we know the storage file does not exist. this is a performance
|
||||
// optimization to not load all items of the underlying storage if we know that
|
||||
// there can be no items because the storage does not exist.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.cache = await this.database.getItems();
|
||||
}
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
get(key: string, fallbackValue?: string): string | undefined {
|
||||
const value = this.cache.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getBoolean(key: string, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
|
||||
const value = this.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined {
|
||||
const value = this.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
|
||||
// We remove the key for undefined/null values
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return this.delete(key);
|
||||
}
|
||||
|
||||
// Otherwise, convert to String and store
|
||||
const valueStr = String(value);
|
||||
|
||||
// Return early if value already set
|
||||
const currentValue = this.cache.get(key);
|
||||
if (currentValue === valueStr) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Update in cache and pending
|
||||
this.cache.set(key, valueStr);
|
||||
this.pendingInserts.set(key, valueStr);
|
||||
this.pendingDeletes.delete(key);
|
||||
|
||||
// Event
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
delete(key: string): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
|
||||
// Remove from cache and add to pending
|
||||
const wasDeleted = this.cache.delete(key);
|
||||
if (!wasDeleted) {
|
||||
return Promise.resolve(); // Return early if value already deleted
|
||||
}
|
||||
|
||||
if (!this.pendingDeletes.has(key)) {
|
||||
this.pendingDeletes.add(key);
|
||||
}
|
||||
|
||||
this.pendingInserts.delete(key);
|
||||
|
||||
// Event
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // return if already closed
|
||||
}
|
||||
|
||||
// Update state
|
||||
this.state = StorageState.Closed;
|
||||
|
||||
// Trigger new flush to ensure data is persisted and then close
|
||||
// even if there is an error flushing. We must always ensure
|
||||
// the DB is closed to avoid corruption.
|
||||
//
|
||||
// Recovery: we pass our cache over as recovery option in case
|
||||
// the DB is not healthy.
|
||||
try {
|
||||
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
await this.database.close(() => this.cache);
|
||||
}
|
||||
|
||||
private flushPending(): Promise<void> {
|
||||
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
|
||||
return Promise.resolve(); // return early if nothing to do
|
||||
}
|
||||
|
||||
// Get pending data
|
||||
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
|
||||
|
||||
// Reset pending data for next run
|
||||
this.pendingDeletes = new Set<string>();
|
||||
this.pendingInserts = new Map<string, string>();
|
||||
|
||||
// Update in storage
|
||||
return this.database.updateItems(updateRequest);
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryStorageDatabase implements IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal = Event.None;
|
||||
|
||||
private items = new Map<string, string>();
|
||||
|
||||
getItems(): Promise<Map<string, string>> {
|
||||
return Promise.resolve(this.items);
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
if (request.insert) {
|
||||
request.insert.forEach((value, key) => this.items.set(key, value));
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
request.delete.forEach(key => this.items.delete(key));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
|
@ -4,305 +4,13 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Database, Statement } from 'vscode-sqlite3';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ThrottledDelayer, timeout } from 'vs/base/common/async';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { mapToString, setToString } from 'vs/base/common/map';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
|
||||
import { fill } from 'vs/base/common/arrays';
|
||||
|
||||
export enum StorageHint {
|
||||
|
||||
// A hint to the storage that the storage
|
||||
// does not exist on disk yet. This allows
|
||||
// the storage library to improve startup
|
||||
// time by not checking the storage for data.
|
||||
STORAGE_DOES_NOT_EXIST
|
||||
}
|
||||
|
||||
export interface IStorageOptions {
|
||||
hint?: StorageHint;
|
||||
}
|
||||
|
||||
export interface IUpdateRequest {
|
||||
insert?: Map<string, string>;
|
||||
delete?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IStorageItemsChangeEvent {
|
||||
items: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
|
||||
|
||||
getItems(): Promise<Map<string, string>>;
|
||||
updateItems(request: IUpdateRequest): Promise<void>;
|
||||
|
||||
close(recovery?: () => Map<string, string>): Promise<void>;
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
export interface IStorage extends IDisposable {
|
||||
|
||||
readonly items: Map<string, string>;
|
||||
readonly size: number;
|
||||
readonly onDidChangeStorage: Event<string>;
|
||||
|
||||
init(): Promise<void>;
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
|
||||
getBoolean(key: string, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
|
||||
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
|
||||
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
enum StorageState {
|
||||
None,
|
||||
Initialized,
|
||||
Closed
|
||||
}
|
||||
|
||||
export class Storage extends Disposable implements IStorage {
|
||||
_serviceBrand: any;
|
||||
|
||||
private static readonly DEFAULT_FLUSH_DELAY = 100;
|
||||
|
||||
private readonly _onDidChangeStorage: Emitter<string> = this._register(new Emitter<string>());
|
||||
get onDidChangeStorage(): Event<string> { return this._onDidChangeStorage.event; }
|
||||
|
||||
private state = StorageState.None;
|
||||
|
||||
private cache: Map<string, string> = new Map<string, string>();
|
||||
|
||||
private flushDelayer: ThrottledDelayer<void>;
|
||||
|
||||
private pendingDeletes: Set<string> = new Set<string>();
|
||||
private pendingInserts: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
protected database: IStorageDatabase,
|
||||
private options: IStorageOptions = Object.create(null)
|
||||
) {
|
||||
super();
|
||||
|
||||
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
|
||||
}
|
||||
|
||||
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
|
||||
// items that change external require us to update our
|
||||
// caches with the values. we just accept the value and
|
||||
// emit an event if there is a change.
|
||||
e.items.forEach((value, key) => this.accept(key, value));
|
||||
}
|
||||
|
||||
private accept(key: string, value: string): void {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return; // Return early if we are already closed
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
|
||||
// Item got removed, check for deletion
|
||||
if (isUndefinedOrNull(value)) {
|
||||
changed = this.cache.delete(key);
|
||||
}
|
||||
|
||||
// Item got updated, check for change
|
||||
else {
|
||||
const currentValue = this.cache.get(key);
|
||||
if (currentValue !== value) {
|
||||
this.cache.set(key, value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Signal to outside listeners
|
||||
if (changed) {
|
||||
this._onDidChangeStorage.fire(key);
|
||||
}
|
||||
}
|
||||
|
||||
get items(): Map<string, string> {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.state !== StorageState.None) {
|
||||
return Promise.resolve(); // either closed or already initialized
|
||||
}
|
||||
|
||||
this.state = StorageState.Initialized;
|
||||
|
||||
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
|
||||
// return early if we know the storage file does not exist. this is a performance
|
||||
// optimization to not load all items of the underlying storage if we know that
|
||||
// there can be no items because the storage does not exist.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.cache = await this.database.getItems();
|
||||
}
|
||||
|
||||
get(key: string, fallbackValue: string): string;
|
||||
get(key: string, fallbackValue?: string): string | undefined;
|
||||
get(key: string, fallbackValue?: string): string | undefined {
|
||||
const value = this.cache.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getBoolean(key: string, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
|
||||
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
|
||||
const value = this.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
getNumber(key: string, fallbackValue: number): number;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined;
|
||||
getNumber(key: string, fallbackValue?: number): number | undefined {
|
||||
const value = this.get(key);
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
|
||||
// We remove the key for undefined/null values
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return this.delete(key);
|
||||
}
|
||||
|
||||
// Otherwise, convert to String and store
|
||||
const valueStr = String(value);
|
||||
|
||||
// Return early if value already set
|
||||
const currentValue = this.cache.get(key);
|
||||
if (currentValue === valueStr) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Update in cache and pending
|
||||
this.cache.set(key, valueStr);
|
||||
this.pendingInserts.set(key, valueStr);
|
||||
this.pendingDeletes.delete(key);
|
||||
|
||||
// Event
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
delete(key: string): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // Return early if we are already closed
|
||||
}
|
||||
|
||||
// Remove from cache and add to pending
|
||||
const wasDeleted = this.cache.delete(key);
|
||||
if (!wasDeleted) {
|
||||
return Promise.resolve(); // Return early if value already deleted
|
||||
}
|
||||
|
||||
if (!this.pendingDeletes.has(key)) {
|
||||
this.pendingDeletes.add(key);
|
||||
}
|
||||
|
||||
this.pendingInserts.delete(key);
|
||||
|
||||
// Event
|
||||
this._onDidChangeStorage.fire(key);
|
||||
|
||||
// Accumulate work by scheduling after timeout
|
||||
return this.flushDelayer.trigger(() => this.flushPending());
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.state === StorageState.Closed) {
|
||||
return Promise.resolve(); // return if already closed
|
||||
}
|
||||
|
||||
// Update state
|
||||
this.state = StorageState.Closed;
|
||||
|
||||
// Trigger new flush to ensure data is persisted and then close
|
||||
// even if there is an error flushing. We must always ensure
|
||||
// the DB is closed to avoid corruption.
|
||||
//
|
||||
// Recovery: we pass our cache over as recovery option in case
|
||||
// the DB is not healthy.
|
||||
try {
|
||||
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
await this.database.close(() => this.cache);
|
||||
}
|
||||
|
||||
private flushPending(): Promise<void> {
|
||||
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
|
||||
return Promise.resolve(); // return early if nothing to do
|
||||
}
|
||||
|
||||
// Get pending data
|
||||
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
|
||||
|
||||
// Reset pending data for next run
|
||||
this.pendingDeletes = new Set<string>();
|
||||
this.pendingInserts = new Map<string, string>();
|
||||
|
||||
// Update in storage
|
||||
return this.database.updateItems(updateRequest);
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return this.database.checkIntegrity(full);
|
||||
}
|
||||
}
|
||||
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
|
||||
|
||||
interface IDatabaseConnection {
|
||||
db: Database;
|
||||
|
@ -753,34 +461,3 @@ class SQLiteStorageDatabaseLogger {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryStorageDatabase implements IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal = Event.None;
|
||||
|
||||
private items = new Map<string, string>();
|
||||
|
||||
getItems(): Promise<Map<string, string>> {
|
||||
return Promise.resolve(this.items);
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
if (request.insert) {
|
||||
request.insert.forEach((value, key) => this.items.set(key, value));
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
request.delete.forEach(key => this.items.delete(key));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return Promise.resolve('ok');
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Storage, SQLiteStorageDatabase, IStorageDatabase, ISQLiteStorageDatabaseOptions, IStorageItemsChangeEvent } from 'vs/base/node/storage';
|
||||
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/parts/storage/node/storage';
|
||||
import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { tmpdir } from 'os';
|
|
@ -67,7 +67,8 @@ suite('Multicursor selection', () => {
|
|||
getBoolean: (key: string) => !!queryState[key],
|
||||
getNumber: (key: string) => undefined!,
|
||||
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
|
||||
remove: (key) => undefined
|
||||
remove: (key) => undefined,
|
||||
logStorage: () => undefined
|
||||
} as IStorageService);
|
||||
|
||||
test('issue #8817: Cursor position changes when you cancel multicursor', () => {
|
||||
|
|
207
src/vs/platform/storage/browser/storageService.ts
Normal file
207
src/vs/platform/storage/browser/storageService.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files';
|
||||
import { IStorage, IStorageDatabase, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
|
||||
export class BrowserStorageService extends Disposable implements IStorageService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
|
||||
|
||||
private readonly _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
get onWillSaveState(): Event<IWillSaveStateEvent> { return this._onWillSaveState.event; }
|
||||
|
||||
private globalStorage: IStorage;
|
||||
private workspaceStorage: IStorage;
|
||||
|
||||
private globalStorageFile: URI;
|
||||
private workspaceStorageFile: URI;
|
||||
|
||||
private initializePromise: Promise<void>;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
|
||||
if (!this.initializePromise) {
|
||||
this.initializePromise = this.doInitialize(payload);
|
||||
}
|
||||
|
||||
return this.initializePromise;
|
||||
}
|
||||
|
||||
private async doInitialize(payload: IWorkspaceInitializationPayload): Promise<void> {
|
||||
|
||||
// Workspace Storage
|
||||
this.workspaceStorageFile = joinPath(this.environmentService.userRoamingDataHome, 'workspaceStorage', `${payload.id}.json`);
|
||||
this.workspaceStorage = new Storage(this._register(new FileStorageDatabase(this.workspaceStorageFile, this.fileService)));
|
||||
this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE })));
|
||||
|
||||
// Global Storage
|
||||
this.globalStorageFile = joinPath(this.environmentService.userRoamingDataHome, 'globalStorage', 'global.json');
|
||||
this.globalStorage = new Storage(this._register(new FileStorageDatabase(this.globalStorageFile, this.fileService)));
|
||||
this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL })));
|
||||
|
||||
// Init both
|
||||
await Promise.all([
|
||||
this.workspaceStorage.init(),
|
||||
this.globalStorage.init()
|
||||
]);
|
||||
}
|
||||
|
||||
//#region
|
||||
|
||||
get(key: string, scope: StorageScope, fallbackValue: string): string;
|
||||
get(key: string, scope: StorageScope): string | undefined;
|
||||
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
|
||||
return this.getStorage(scope).get(key, fallbackValue);
|
||||
}
|
||||
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, scope: StorageScope): boolean | undefined;
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
|
||||
return this.getStorage(scope).getBoolean(key, fallbackValue);
|
||||
}
|
||||
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
|
||||
getNumber(key: string, scope: StorageScope): number | undefined;
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
|
||||
return this.getStorage(scope).getNumber(key, fallbackValue);
|
||||
}
|
||||
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
|
||||
this.getStorage(scope).set(key, value);
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): void {
|
||||
this.getStorage(scope).delete(key);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
|
||||
// Signal as event so that clients can still store data
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
|
||||
|
||||
// Do it
|
||||
await Promise.all([
|
||||
this.globalStorage.close(),
|
||||
this.workspaceStorage.close()
|
||||
]);
|
||||
}
|
||||
|
||||
private getStorage(scope: StorageScope): IStorage {
|
||||
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
|
||||
}
|
||||
|
||||
async logStorage(): Promise<void> {
|
||||
const result = await Promise.all([
|
||||
this.globalStorage.items,
|
||||
this.workspaceStorage.items
|
||||
]);
|
||||
|
||||
return logStorage(result[0], result[1], this.globalStorageFile.toString(), this.workspaceStorageFile.toString());
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal = Event.None; // TODO@Ben implement global UI storage events
|
||||
|
||||
private cache: Map<string, string> | undefined;
|
||||
|
||||
private pendingUpdate: Promise<void> = Promise.resolve();
|
||||
|
||||
constructor(
|
||||
private readonly file: URI,
|
||||
private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.fileService.watch(this.file));
|
||||
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
|
||||
}
|
||||
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
if (!this.cache) {
|
||||
try {
|
||||
this.cache = await this.doGetItemsFromFile();
|
||||
} catch (error) {
|
||||
this.cache = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
private async doGetItemsFromFile(): Promise<Map<string, string>> {
|
||||
await this.pendingUpdate;
|
||||
|
||||
const itemsRaw = await this.fileService.readFile(this.file);
|
||||
|
||||
return new Map(JSON.parse(itemsRaw.value.toString()));
|
||||
}
|
||||
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
let updateCount = 0;
|
||||
if (request.insert) {
|
||||
updateCount += request.insert.size;
|
||||
}
|
||||
if (request.delete) {
|
||||
updateCount += request.delete.size;
|
||||
}
|
||||
|
||||
if (updateCount === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const items = await this.getItems();
|
||||
|
||||
if (request.insert) {
|
||||
request.insert.forEach((value, key) => items.set(key, value));
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
request.delete.forEach(key => items.delete(key));
|
||||
}
|
||||
|
||||
const itemsRaw = JSON.stringify([...items]);
|
||||
|
||||
await this.pendingUpdate;
|
||||
|
||||
this.pendingUpdate = this.fileService.writeFile(this.file, VSBuffer.fromString(itemsRaw)).then();
|
||||
|
||||
return this.pendingUpdate;
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.pendingUpdate;
|
||||
}
|
||||
}
|
|
@ -86,6 +86,11 @@ export interface IStorageService {
|
|||
* operation to either the current workspace only or all workspaces.
|
||||
*/
|
||||
remove(key: string, scope: StorageScope): void;
|
||||
|
||||
/**
|
||||
* Log the contents of the storage to the console.
|
||||
*/
|
||||
logStorage(): void;
|
||||
}
|
||||
|
||||
export const enum StorageScope {
|
||||
|
@ -107,6 +112,7 @@ export interface IWorkspaceStorageChangeEvent {
|
|||
}
|
||||
|
||||
export class InMemoryStorageService extends Disposable implements IStorageService {
|
||||
|
||||
_serviceBrand = undefined;
|
||||
|
||||
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
|
@ -190,4 +196,52 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
|
|||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logStorage(): void {
|
||||
logStorage(this.globalCache, this.workspaceCache, 'inMemory', 'inMemory');
|
||||
}
|
||||
}
|
||||
|
||||
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {
|
||||
const safeParse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const globalItems = new Map<string, string>();
|
||||
const globalItemsParsed = new Map<string, string>();
|
||||
global.forEach((value, key) => {
|
||||
globalItems.set(key, value);
|
||||
globalItemsParsed.set(key, safeParse(value));
|
||||
});
|
||||
|
||||
const workspaceItems = new Map<string, string>();
|
||||
const workspaceItemsParsed = new Map<string, string>();
|
||||
workspace.forEach((value, key) => {
|
||||
workspaceItems.set(key, value);
|
||||
workspaceItemsParsed.set(key, safeParse(value));
|
||||
});
|
||||
|
||||
console.group(`Storage: Global (path: ${globalPath})`);
|
||||
let globalValues: { key: string, value: string }[] = [];
|
||||
globalItems.forEach((value, key) => {
|
||||
globalValues.push({ key, value });
|
||||
});
|
||||
console.table(globalValues);
|
||||
console.groupEnd();
|
||||
|
||||
console.log(globalItemsParsed);
|
||||
|
||||
console.group(`Storage: Workspace (path: ${workspacePath})`);
|
||||
let workspaceValues: { key: string, value: string }[] = [];
|
||||
workspaceItems.forEach((value, key) => {
|
||||
workspaceValues.push({ key, value });
|
||||
});
|
||||
console.table(workspaceValues);
|
||||
console.groupEnd();
|
||||
|
||||
console.log(workspaceItemsParsed);
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { StorageMainService, IStorageChangeEvent } from 'vs/platform/storage/node/storageMainService';
|
||||
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/node/storage';
|
||||
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
|
||||
import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
@ -139,10 +139,6 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
|
|||
break;
|
||||
}
|
||||
|
||||
case 'checkIntegrity': {
|
||||
return this.storageMainService.checkIntegrity(arg);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Call not found: ${command}`);
|
||||
}
|
||||
|
@ -201,10 +197,6 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
|
|||
return this.channel.call('updateItems', serializableRequest);
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return this.channel.call('checkIntegrity', full);
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
|
||||
// when we are about to close, we start to ignore main-side changes since we close anyway
|
||||
|
|
|
@ -8,7 +8,8 @@ import { Event, Emitter } from 'vs/base/common/event';
|
|||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IStorage, Storage, SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions, InMemoryStorageDatabase } from 'vs/base/node/storage';
|
||||
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
|
||||
import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
|
||||
import { join } from 'vs/base/common/path';
|
||||
|
||||
export const IStorageMainService = createDecorator<IStorageMainService>('storageMainService');
|
||||
|
@ -164,8 +165,4 @@ export class StorageMainService extends Disposable implements IStorageMainServic
|
|||
// Do it
|
||||
return this.storage.close();
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return this.storage.checkIntegrity(full);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||
import { Storage, ISQLiteStorageDatabaseLoggingOptions, IStorage, StorageHint, IStorageDatabase, SQLiteStorageDatabase } from 'vs/base/node/storage';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { localize } from 'vs/nls';
|
||||
import { mark, getDuration } from 'vs/base/common/performance';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
|
||||
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
|
||||
import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class StorageService extends Disposable implements IStorageService {
|
||||
_serviceBrand: any;
|
||||
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
private static WORKSPACE_STORAGE_NAME = 'state.vscdb';
|
||||
private static WORKSPACE_META_NAME = 'workspace.json';
|
||||
|
@ -201,63 +201,13 @@ export class StorageService extends Disposable implements IStorageService {
|
|||
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
|
||||
}
|
||||
|
||||
getSize(scope: StorageScope): number {
|
||||
return scope === StorageScope.GLOBAL ? this.globalStorage.size : this.workspaceStorage.size;
|
||||
}
|
||||
|
||||
checkIntegrity(scope: StorageScope, full: boolean): Promise<string> {
|
||||
return scope === StorageScope.GLOBAL ? this.globalStorage.checkIntegrity(full) : this.workspaceStorage.checkIntegrity(full);
|
||||
}
|
||||
|
||||
async logStorage(): Promise<void> {
|
||||
const result = await Promise.all([
|
||||
this.globalStorage.items,
|
||||
this.workspaceStorage.items,
|
||||
this.globalStorage.checkIntegrity(true /* full */),
|
||||
this.workspaceStorage.checkIntegrity(true /* full */)
|
||||
this.workspaceStorage.items
|
||||
]);
|
||||
|
||||
const safeParse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const globalItems = new Map<string, string>();
|
||||
const globalItemsParsed = new Map<string, string>();
|
||||
result[0].forEach((value, key) => {
|
||||
globalItems.set(key, value);
|
||||
globalItemsParsed.set(key, safeParse(value));
|
||||
});
|
||||
|
||||
const workspaceItems = new Map<string, string>();
|
||||
const workspaceItemsParsed = new Map<string, string>();
|
||||
result[1].forEach((value, key) => {
|
||||
workspaceItems.set(key, value);
|
||||
workspaceItemsParsed.set(key, safeParse(value));
|
||||
});
|
||||
|
||||
console.group(`Storage: Global (integrity: ${result[2]}, path: ${this.environmentService.globalStorageHome})`);
|
||||
let globalValues: { key: string, value: string }[] = [];
|
||||
globalItems.forEach((value, key) => {
|
||||
globalValues.push({ key, value });
|
||||
});
|
||||
console.table(globalValues);
|
||||
console.groupEnd();
|
||||
|
||||
console.log(globalItemsParsed);
|
||||
|
||||
console.group(`Storage: Workspace (integrity: ${result[3]}, load: ${getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage')}, path: ${this.workspaceStoragePath})`);
|
||||
let workspaceValues: { key: string, value: string }[] = [];
|
||||
workspaceItems.forEach((value, key) => {
|
||||
workspaceValues.push({ key, value });
|
||||
});
|
||||
console.table(workspaceValues);
|
||||
console.groupEnd();
|
||||
|
||||
console.log(workspaceItemsParsed);
|
||||
logStorage(result[0], result[1], this.environmentService.globalStorageHome, this.workspaceStoragePath);
|
||||
}
|
||||
|
||||
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
|
||||
|
@ -280,24 +230,3 @@ export class StorageService extends Disposable implements IStorageService {
|
|||
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
|
||||
}
|
||||
}
|
||||
|
||||
export class LogStorageAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.logStorage';
|
||||
static LABEL = localize({ key: 'logStorage', comment: ['A developer only action to log the contents of the storage for the current window.'] }, "Log Storage Database Contents");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IStorageService private readonly storageService: StorageService,
|
||||
@IWindowService private readonly windowService: IWindowService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
this.storageService.logStorage();
|
||||
|
||||
return this.windowService.openDevTools();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
|
|||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { InMemoryStorageDatabase } from 'vs/base/node/storage';
|
||||
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
|
||||
|
||||
suite('StorageService', () => {
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la
|
|||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export class InspectContextKeysAction extends Action {
|
||||
|
||||
|
@ -193,7 +194,29 @@ export class ToggleScreencastModeAction extends Action {
|
|||
}
|
||||
}
|
||||
|
||||
export class LogStorageAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.logStorage';
|
||||
static LABEL = nls.localize({ key: 'logStorage', comment: ['A developer only action to log the contents of the storage for the current window.'] }, "Log Storage Database Contents");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IWindowService private readonly windowService: IWindowService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
this.storageService.logStorage();
|
||||
|
||||
return this.windowService.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
const developerCategory = nls.localize('developer', "Developer");
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(InspectContextKeysAction, InspectContextKeysAction.ID, InspectContextKeysAction.LABEL), 'Developer: Inspect Context Keys', developerCategory);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleScreencastModeAction, ToggleScreencastModeAction.ID, ToggleScreencastModeAction.LABEL), 'Developer: Toggle Screencast Mode', developerCategory);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(LogStorageAction, LogStorageAction.ID, LogStorageAction.LABEL), 'Developer: Log Storage Database Contents', developerCategory);
|
|
@ -35,8 +35,10 @@ import { hash } from 'vs/base/common/hash';
|
|||
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
|
||||
import { ProductService } from 'vs/platform/product/browser/productService';
|
||||
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { BACKUPS } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { BrowserStorageService } from 'vs/platform/storage/browser/storageService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
class CodeRendererMain extends Disposable {
|
||||
|
||||
|
@ -146,11 +148,34 @@ class CodeRendererMain extends Disposable {
|
|||
|
||||
return service;
|
||||
}),
|
||||
|
||||
this.createStorageService(payload, environmentService, fileService, logService).then(service => {
|
||||
|
||||
// Storage
|
||||
serviceCollection.set(IStorageService, service);
|
||||
|
||||
return service;
|
||||
})
|
||||
]);
|
||||
|
||||
return { serviceCollection, logService };
|
||||
}
|
||||
|
||||
private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise<IStorageService> {
|
||||
const storageService = new BrowserStorageService(environmentService, fileService);
|
||||
|
||||
try {
|
||||
await storageService.initialize(payload);
|
||||
|
||||
return storageService;
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
logService.error(error);
|
||||
|
||||
return storageService;
|
||||
}
|
||||
}
|
||||
|
||||
private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise<WorkspaceService> {
|
||||
const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
// tslint:disable-next-line: import-patterns no-standalone-editor
|
||||
|
@ -18,9 +18,8 @@ import { IExtensionManifest, ExtensionType, ExtensionIdentifier, IExtension } fr
|
|||
import { IURLHandler, IURLService } from 'vs/platform/url/common/url';
|
||||
import { ITelemetryService, ITelemetryData, ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ConsoleLogService } from 'vs/platform/log/common/log';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope, IWillSaveStateEvent, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IUpdateService, State } from 'vs/platform/update/common/update';
|
||||
import { IWindowService, INativeOpenDialogOptions, IEnterWorkspaceResult, IURIToOpen, IMessageBoxResult, IWindowsService, IOpenSettings, IWindowSettings } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
|
@ -39,7 +38,6 @@ import { ICommentService, IResourceCommentThreadEvent, IWorkspaceCommentThreadsE
|
|||
import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
|
||||
import { CommentingRanges } from 'vs/editor/common/modes';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
@ -86,14 +84,6 @@ registerSingleton(IClipboardService, SimpleClipboardService, true);
|
|||
|
||||
//#endregion
|
||||
|
||||
//#region Dialog
|
||||
|
||||
// export class SimpleDialogService extends StandaloneEditorDialogService { }
|
||||
|
||||
// registerSingleton(IDialogService, SimpleDialogService, true);
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Download
|
||||
|
||||
export class SimpleDownloadService implements IDownloadService {
|
||||
|
@ -482,111 +472,6 @@ export class SimpleRequestService implements IRequestService {
|
|||
|
||||
//#endregion
|
||||
|
||||
//#region Storage
|
||||
|
||||
export class LocalStorageService extends Disposable implements IStorageService {
|
||||
_serviceBrand = undefined;
|
||||
|
||||
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
get onDidChangeStorage(): Event<IWorkspaceStorageChangeEvent> { return this._onDidChangeStorage.event; }
|
||||
|
||||
private readonly _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
|
||||
get onWillSaveState(): Event<IWillSaveStateEvent> { return this._onWillSaveState.event; }
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
|
||||
@ILifecycleService lifecycleService: ILifecycleService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(lifecycleService.onBeforeShutdown(() => this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN })));
|
||||
}
|
||||
|
||||
private toKey(key: string, scope: StorageScope): string {
|
||||
if (scope === StorageScope.GLOBAL) {
|
||||
return `global://${key}`;
|
||||
}
|
||||
|
||||
return `workspace://${this.workspaceContextService.getWorkspace().id}/${key}`;
|
||||
}
|
||||
|
||||
get(key: string, scope: StorageScope, fallbackValue: string): string;
|
||||
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined {
|
||||
const value = window.localStorage.getItem(this.toKey(key, scope));
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
|
||||
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
|
||||
const value = window.localStorage.getItem(this.toKey(key, scope));
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
|
||||
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined {
|
||||
const value = window.localStorage.getItem(this.toKey(key, scope));
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
|
||||
|
||||
// We remove the key for undefined/null values
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return this.remove(key, scope);
|
||||
}
|
||||
|
||||
// Otherwise, convert to String and store
|
||||
const valueStr = String(value);
|
||||
|
||||
// Return early if value already set
|
||||
const currentValue = window.localStorage.getItem(this.toKey(key, scope));
|
||||
if (currentValue === valueStr) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Update in cache
|
||||
window.localStorage.setItem(this.toKey(key, scope), valueStr);
|
||||
|
||||
// Events
|
||||
this._onDidChangeStorage.fire({ scope, key });
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string, scope: StorageScope): Promise<void> {
|
||||
const wasDeleted = window.localStorage.getItem(this.toKey(key, scope));
|
||||
window.localStorage.removeItem(this.toKey(key, scope));
|
||||
|
||||
if (!wasDeleted) {
|
||||
return Promise.resolve(); // Return early if value already deleted
|
||||
}
|
||||
|
||||
// Events
|
||||
this._onDidChangeStorage.fire({ scope, key });
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IStorageService, LocalStorageService);
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Telemetry
|
||||
|
||||
export class SimpleTelemetryService implements ITelemetryService {
|
||||
|
|
|
@ -23,7 +23,6 @@ import { ADD_ROOT_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspa
|
|||
import { SupportsWorkspacesContext, IsMacContext, HasMacNativeTabsContext, IsDevelopmentContext, WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys';
|
||||
import { NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor';
|
||||
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { LogStorageAction } from 'vs/platform/storage/node/storageService';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
|
||||
// Actions
|
||||
|
@ -125,7 +124,6 @@ import product from 'vs/platform/product/node/product';
|
|||
const developerCategory = nls.localize('developer', "Developer");
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleSharedProcessAction, ToggleSharedProcessAction.ID, ToggleSharedProcessAction.LABEL), 'Developer: Toggle Shared Process', developerCategory);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ReloadWindowWithExtensionsDisabledAction, ReloadWindowWithExtensionsDisabledAction.ID, ReloadWindowWithExtensionsDisabledAction.LABEL), 'Developer: Reload Window With Extensions Disabled', developerCategory);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(LogStorageAction, LogStorageAction.ID, LogStorageAction.LABEL), 'Developer: Log Storage Database Contents', developerCategory);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleDevToolsAction, ToggleDevToolsAction.ID, ToggleDevToolsAction.LABEL), 'Developer: Toggle Developer Tools', developerCategory);
|
||||
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
|
|
Loading…
Reference in a new issue