mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
This reverts commit 5d44b6a6a7.
This commit is contained in:
@@ -9,11 +9,6 @@ import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
|
||||
|
||||
export const IBackupFileService = createDecorator<IBackupFileService>('backupFileService');
|
||||
|
||||
export interface IResolvedBackup<T extends object> {
|
||||
value: ITextBufferFactory;
|
||||
meta?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A service that handles any I/O and state associated with the backup system.
|
||||
*/
|
||||
@@ -47,10 +42,8 @@ export interface IBackupFileService {
|
||||
* @param resource The resource to back up.
|
||||
* @param content The content of the resource as snapshot.
|
||||
* @param versionId The version id of the resource to backup.
|
||||
* @param meta The (optional) meta data of the resource to backup. This information
|
||||
* can be restored later when loading the backup again.
|
||||
*/
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void>;
|
||||
backupResource(resource: URI, content: ITextSnapshot, versionId?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets a list of file backups for the current workspace.
|
||||
@@ -62,10 +55,10 @@ export interface IBackupFileService {
|
||||
/**
|
||||
* Resolves the backup for the given resource.
|
||||
*
|
||||
* @param resource The resource to get the backup for.
|
||||
* @return The backup file's backed up content and metadata if available.
|
||||
* @param value The contents from a backup resource as stream.
|
||||
* @return The backup file's backed up content as text buffer factory.
|
||||
*/
|
||||
resolveBackupContent<T extends object>(resource: URI): Promise<IResolvedBackup<T>>;
|
||||
resolveBackupContent(backup: URI): Promise<ITextBufferFactory | undefined>;
|
||||
|
||||
/**
|
||||
* Discards the backup associated with a resource if it exists..
|
||||
|
||||
@@ -3,112 +3,94 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { createHash } from 'crypto';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { equals, deepClone } from 'vs/base/common/objects';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as crypto from 'crypto';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { URI as Uri } from 'vs/base/common/uri';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { readToMatchingString } from 'vs/base/node/stream';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { keys, ResourceMap } from 'vs/base/common/map';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { TextSnapshotReadable } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IBackupFilesModel {
|
||||
resolve(backupRoot: URI): Promise<IBackupFilesModel>;
|
||||
resolve(backupRoot: string): Promise<IBackupFilesModel>;
|
||||
|
||||
add(resource: URI, versionId?: number, meta?: object): void;
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean;
|
||||
get(): URI[];
|
||||
remove(resource: URI): void;
|
||||
add(resource: Uri, versionId?: number): void;
|
||||
has(resource: Uri, versionId?: number): boolean;
|
||||
get(): Uri[];
|
||||
remove(resource: Uri): void;
|
||||
count(): number;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
interface IBackupCacheEntry {
|
||||
versionId?: number;
|
||||
meta?: object;
|
||||
}
|
||||
|
||||
export class BackupFilesModel implements IBackupFilesModel {
|
||||
private cache: ResourceMap<IBackupCacheEntry> = new ResourceMap();
|
||||
private cache: { [resource: string]: number /* version ID */ } = Object.create(null);
|
||||
|
||||
constructor(private fileService: IFileService) { }
|
||||
resolve(backupRoot: string): Promise<IBackupFilesModel> {
|
||||
return pfs.readDirsInDir(backupRoot).then(backupSchemas => {
|
||||
|
||||
async resolve(backupRoot: URI): Promise<IBackupFilesModel> {
|
||||
try {
|
||||
const backupRootStat = await this.fileService.resolve(backupRoot);
|
||||
if (backupRootStat.children) {
|
||||
await Promise.all(backupRootStat.children
|
||||
.filter(child => child.isDirectory)
|
||||
.map(async backupSchema => {
|
||||
// For all supported schemas
|
||||
return Promise.all(backupSchemas.map(backupSchema => {
|
||||
|
||||
// Read backup directory for backups
|
||||
const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
|
||||
// Read backup directory for backups
|
||||
const backupSchemaPath = path.join(backupRoot, backupSchema);
|
||||
return pfs.readdir(backupSchemaPath).then(backupHashes => {
|
||||
|
||||
// Remember known backups in our caches
|
||||
if (backupSchemaStat.children) {
|
||||
backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore any errors
|
||||
}
|
||||
|
||||
return this;
|
||||
// Remember known backups in our caches
|
||||
backupHashes.forEach(backupHash => {
|
||||
const backupResource = Uri.file(path.join(backupSchemaPath, backupHash));
|
||||
this.add(backupResource);
|
||||
});
|
||||
});
|
||||
}));
|
||||
}).then(() => this, error => this);
|
||||
}
|
||||
|
||||
add(resource: URI, versionId = 0, meta?: object): void {
|
||||
this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache...
|
||||
add(resource: Uri, versionId = 0): void {
|
||||
this.cache[resource.toString()] = versionId;
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.cache.size;
|
||||
return Object.keys(this.cache).length;
|
||||
}
|
||||
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean {
|
||||
const entry = this.cache.get(resource);
|
||||
if (!entry) {
|
||||
has(resource: Uri, versionId?: number): boolean {
|
||||
const cachedVersionId = this.cache[resource.toString()];
|
||||
if (typeof cachedVersionId !== 'number') {
|
||||
return false; // unknown resource
|
||||
}
|
||||
|
||||
if (typeof versionId === 'number' && versionId !== entry.versionId) {
|
||||
return false; // different versionId
|
||||
}
|
||||
|
||||
if (meta && !equals(meta, entry.meta)) {
|
||||
return false; // different metadata
|
||||
if (typeof versionId === 'number') {
|
||||
return versionId === cachedVersionId; // if we are asked with a specific version ID, make sure to test for it
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get(): URI[] {
|
||||
return this.cache.keys();
|
||||
get(): Uri[] {
|
||||
return Object.keys(this.cache).map(k => Uri.parse(k));
|
||||
}
|
||||
|
||||
remove(resource: URI): void {
|
||||
this.cache.delete(resource);
|
||||
remove(resource: Uri): void {
|
||||
delete this.cache[resource.toString()];
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
_serviceBrand: any;
|
||||
|
||||
private impl: IBackupFileService;
|
||||
|
||||
@@ -134,15 +116,15 @@ export class BackupFileService implements IBackupFileService {
|
||||
return this.impl.hasBackups();
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
loadBackupResource(resource: Uri): Promise<Uri | undefined> {
|
||||
return this.impl.loadBackupResource(resource);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
return this.impl.backupResource(resource, content, versionId, meta);
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
|
||||
return this.impl.backupResource(resource, content, versionId);
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
discardResourceBackup(resource: Uri): Promise<void> {
|
||||
return this.impl.discardResourceBackup(resource);
|
||||
}
|
||||
|
||||
@@ -150,28 +132,26 @@ export class BackupFileService implements IBackupFileService {
|
||||
return this.impl.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
getWorkspaceFileBackups(): Promise<Uri[]> {
|
||||
return this.impl.getWorkspaceFileBackups();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
resolveBackupContent(backup: Uri): Promise<ITextBufferFactory | undefined> {
|
||||
return this.impl.resolveBackupContent(backup);
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
toBackupResource(resource: Uri): Uri {
|
||||
return this.impl.toBackupResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
class BackupFileServiceImpl implements IBackupFileService {
|
||||
|
||||
private static readonly PREAMBLE_END_MARKER = '\n';
|
||||
private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator
|
||||
private static readonly PREAMBLE_MAX_LENGTH = 10000;
|
||||
private static readonly META_MARKER = '\n';
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private backupWorkspacePath: URI;
|
||||
private backupWorkspacePath: string;
|
||||
|
||||
private isShuttingDown: boolean;
|
||||
private ready: Promise<IBackupFilesModel>;
|
||||
@@ -188,165 +168,115 @@ class BackupFileServiceImpl implements IBackupFileService {
|
||||
}
|
||||
|
||||
initialize(backupWorkspacePath: string): void {
|
||||
this.backupWorkspacePath = URI.file(backupWorkspacePath);
|
||||
this.backupWorkspacePath = backupWorkspacePath;
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
private init(): Promise<IBackupFilesModel> {
|
||||
const model = new BackupFilesModel(this.fileService);
|
||||
const model = new BackupFilesModel();
|
||||
|
||||
return model.resolve(this.backupWorkspacePath);
|
||||
}
|
||||
|
||||
async hasBackups(): Promise<boolean> {
|
||||
const model = await this.ready;
|
||||
|
||||
return model.count() > 0;
|
||||
hasBackups(): Promise<boolean> {
|
||||
return this.ready.then(model => {
|
||||
return model.count() > 0;
|
||||
});
|
||||
}
|
||||
|
||||
async loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const model = await this.ready;
|
||||
loadBackupResource(resource: Uri): Promise<Uri | undefined> {
|
||||
return this.ready.then(model => {
|
||||
|
||||
// Return directly if we have a known backup with that resource
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource)) {
|
||||
return backupResource;
|
||||
}
|
||||
// Return directly if we have a known backup with that resource
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource)) {
|
||||
return backupResource;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
async backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource, versionId, meta)) {
|
||||
return; // return early if backup version id matches requested one
|
||||
}
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
let preamble: string | undefined = undefined;
|
||||
|
||||
// With Metadata: URI + META-START + Meta + END
|
||||
if (meta) {
|
||||
const preambleWithMeta = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify(meta)}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
if (preambleWithMeta.length < BackupFileServiceImpl.PREAMBLE_MAX_LENGTH) {
|
||||
preamble = preambleWithMeta;
|
||||
}
|
||||
return this.ready.then(model => {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource, versionId)) {
|
||||
return undefined; // return early if backup version id matches requested one
|
||||
}
|
||||
|
||||
// Without Metadata: URI + END
|
||||
if (!preamble) {
|
||||
preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
}
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(() => {
|
||||
const preamble = `${resource.toString()}${BackupFileServiceImpl.META_MARKER}`;
|
||||
|
||||
// Update content with value
|
||||
await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
|
||||
|
||||
// Update model
|
||||
model.add(backupResource, versionId, meta);
|
||||
// Update content with value
|
||||
return this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble)).then(() => model.add(backupResource, versionId));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async discardResourceBackup(resource: URI): Promise<void> {
|
||||
const model = await this.ready;
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
discardResourceBackup(resource: Uri): Promise<void> {
|
||||
return this.ready.then(model => {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
await this.fileService.del(backupResource, { recursive: true });
|
||||
|
||||
model.remove(backupResource);
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(() => {
|
||||
return pfs.rimraf(backupResource.fsPath, pfs.RimRafMode.MOVE).then(() => model.remove(backupResource));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async discardAllWorkspaceBackups(): Promise<void> {
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
await this.fileService.del(this.backupWorkspacePath, { recursive: true });
|
||||
|
||||
model.clear();
|
||||
return this.ready.then(model => {
|
||||
return pfs.rimraf(this.backupWorkspacePath, pfs.RimRafMode.MOVE).then(() => model.clear());
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
const model = await this.ready;
|
||||
getWorkspaceFileBackups(): Promise<Uri[]> {
|
||||
return this.ready.then(model => {
|
||||
const readPromises: Promise<Uri>[] = [];
|
||||
|
||||
const backups = await Promise.all(model.get().map(async fileBackup => {
|
||||
const backupPreamble = await readToMatchingString(fileBackup.fsPath, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH / 5, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH);
|
||||
if (!backupPreamble) {
|
||||
return undefined;
|
||||
}
|
||||
model.get().forEach(fileBackup => {
|
||||
readPromises.push(
|
||||
readToMatchingString(fileBackup.fsPath, BackupFileServiceImpl.META_MARKER, 2000, 10000).then(Uri.parse)
|
||||
);
|
||||
});
|
||||
|
||||
// Preamble with metadata: URI + META-START + Meta + END
|
||||
const metaStartIndex = backupPreamble.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
||||
if (metaStartIndex > 0) {
|
||||
return URI.parse(backupPreamble.substring(0, metaStartIndex));
|
||||
}
|
||||
|
||||
// Preamble without metadata: URI + END
|
||||
else {
|
||||
return URI.parse(backupPreamble);
|
||||
}
|
||||
}));
|
||||
|
||||
return coalesce(backups);
|
||||
return Promise.all(readPromises);
|
||||
});
|
||||
}
|
||||
|
||||
async resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
resolveBackupContent(backup: Uri): Promise<ITextBufferFactory> {
|
||||
return this.fileService.readFileStream(backup).then(content => {
|
||||
|
||||
// Metadata extraction
|
||||
let metaRaw = '';
|
||||
let metaEndFound = false;
|
||||
// Add a filter method to filter out everything until the meta marker
|
||||
let metaFound = false;
|
||||
const metaPreambleFilter = (chunk: VSBuffer) => {
|
||||
const chunkString = chunk.toString();
|
||||
|
||||
// Add a filter method to filter out everything until the meta end marker
|
||||
const metaPreambleFilter = (chunk: VSBuffer) => {
|
||||
const chunkString = chunk.toString();
|
||||
if (!metaFound && chunk) {
|
||||
const metaIndex = chunkString.indexOf(BackupFileServiceImpl.META_MARKER);
|
||||
if (metaIndex === -1) {
|
||||
return VSBuffer.fromString(''); // meta not yet found, return empty string
|
||||
}
|
||||
|
||||
if (!metaEndFound) {
|
||||
const metaEndIndex = chunkString.indexOf(BackupFileServiceImpl.PREAMBLE_END_MARKER);
|
||||
if (metaEndIndex === -1) {
|
||||
metaRaw += chunkString;
|
||||
|
||||
return VSBuffer.fromString(''); // meta not yet found, return empty string
|
||||
metaFound = true;
|
||||
return VSBuffer.fromString(chunkString.substr(metaIndex + 1)); // meta found, return everything after
|
||||
}
|
||||
|
||||
metaEndFound = true;
|
||||
metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
|
||||
return chunk;
|
||||
};
|
||||
|
||||
return VSBuffer.fromString(chunkString.substr(metaEndIndex + 1)); // meta found, return everything after
|
||||
}
|
||||
|
||||
return chunk;
|
||||
};
|
||||
|
||||
// Read backup into factory
|
||||
const content = await this.fileService.readFileStream(backup);
|
||||
const factory = await createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
|
||||
|
||||
// Trigger read for meta data extraction from the filter above
|
||||
factory.getFirstLineText(1);
|
||||
|
||||
let meta: T | undefined;
|
||||
const metaStartIndex = metaRaw.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
||||
if (metaStartIndex !== -1) {
|
||||
try {
|
||||
meta = JSON.parse(metaRaw.substr(metaStartIndex + 1));
|
||||
} catch (error) {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return { value: factory, meta };
|
||||
return createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
|
||||
});
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return joinPath(this.backupWorkspacePath, resource.scheme, hashPath(resource));
|
||||
toBackupResource(resource: Uri): Uri {
|
||||
return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, hashPath(resource)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +290,7 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
return Promise.resolve(this.backups.size > 0);
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
loadBackupResource(resource: Uri): Promise<Uri | undefined> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (this.backups.has(backupResource.toString())) {
|
||||
return Promise.resolve(backupResource);
|
||||
@@ -369,27 +299,27 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
this.backups.set(backupResource.toString(), content);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backupResource: URI): Promise<IResolvedBackup<T>> {
|
||||
resolveBackupContent(backupResource: Uri): Promise<ITextBufferFactory | undefined> {
|
||||
const snapshot = this.backups.get(backupResource.toString());
|
||||
if (snapshot) {
|
||||
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
|
||||
return Promise.resolve(createTextBufferFactoryFromSnapshot(snapshot));
|
||||
}
|
||||
|
||||
return Promise.reject('Unexpected backup resource to resolve');
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
|
||||
getWorkspaceFileBackups(): Promise<Uri[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => Uri.parse(key)));
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
discardResourceBackup(resource: Uri): Promise<void> {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
|
||||
return Promise.resolve();
|
||||
@@ -401,17 +331,17 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return URI.file(join(resource.scheme, hashPath(resource)));
|
||||
toBackupResource(resource: Uri): Uri {
|
||||
return Uri.file(path.join(resource.scheme, hashPath(resource)));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported only for testing
|
||||
*/
|
||||
export function hashPath(resource: URI): string {
|
||||
export function hashPath(resource: Uri): string {
|
||||
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
|
||||
return createHash('md5').update(str).digest('hex');
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
|
||||
registerSingleton(IBackupFileService, BackupFileService);
|
||||
@@ -0,0 +1,402 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as crypto from 'crypto';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { URI as Uri } from 'vs/base/common/uri';
|
||||
import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService';
|
||||
import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { DefaultEndOfLine } from 'vs/editor/common/model';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
|
||||
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
|
||||
const backupHome = path.join(parentDir, 'Backups');
|
||||
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
|
||||
|
||||
const workspaceResource = Uri.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
|
||||
const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource));
|
||||
const fooFile = Uri.file(platform.isWindows ? 'c:\\Foo' : '/Foo');
|
||||
const barFile = Uri.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
|
||||
const untitledFile = Uri.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
|
||||
const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile));
|
||||
const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile));
|
||||
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile));
|
||||
|
||||
class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
|
||||
|
||||
private config: IWindowConfiguration;
|
||||
|
||||
constructor(workspaceBackupPath: string) {
|
||||
super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
|
||||
|
||||
this.config = Object.create(null);
|
||||
this.config.backupPath = workspaceBackupPath;
|
||||
}
|
||||
|
||||
get configuration(): IWindowConfiguration {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
|
||||
class TestBackupFileService extends BackupFileService {
|
||||
constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
|
||||
const fileService = new FileService(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
|
||||
const environmentService = new TestBackupEnvironmentService(workspaceBackupPath);
|
||||
|
||||
super(environmentService, fileService);
|
||||
}
|
||||
|
||||
public toBackupResource(resource: Uri): Uri {
|
||||
return super.toBackupResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
suite('BackupFileService', () => {
|
||||
let service: TestBackupFileService;
|
||||
|
||||
setup(() => {
|
||||
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
|
||||
|
||||
// Delete any existing backups completely and then re-create it.
|
||||
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => {
|
||||
return pfs.mkdirp(backupHome).then(() => {
|
||||
return pfs.writeFile(workspacesJsonPath, '');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
|
||||
});
|
||||
|
||||
suite('hashPath', () => {
|
||||
test('should correctly hash the path for untitled scheme URIs', () => {
|
||||
const uri = Uri.from({
|
||||
scheme: 'untitled',
|
||||
path: 'Untitled-1'
|
||||
});
|
||||
const actual = hashPath(uri);
|
||||
// If these hashes change people will lose their backed up files!
|
||||
assert.equal(actual, '13264068d108c6901b3592ea654fcd57');
|
||||
assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
|
||||
});
|
||||
|
||||
test('should correctly hash the path for file scheme URIs', () => {
|
||||
const uri = Uri.file('/foo');
|
||||
const actual = hashPath(uri);
|
||||
// If these hashes change people will lose their backed up files!
|
||||
if (platform.isWindows) {
|
||||
assert.equal(actual, 'dec1a583f52468a020bd120c3f01d812');
|
||||
} else {
|
||||
assert.equal(actual, '1effb2475fcfba4f9e8b8a1dbc8f3caf');
|
||||
}
|
||||
assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('getBackupResource', () => {
|
||||
test('should get the correct backup path for text files', () => {
|
||||
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
|
||||
const backupResource = fooFile;
|
||||
const workspaceHash = hashPath(workspaceResource);
|
||||
const filePathHash = hashPath(backupResource);
|
||||
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath;
|
||||
assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
|
||||
});
|
||||
|
||||
test('should get the correct backup path for untitled files', () => {
|
||||
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePath>
|
||||
const backupResource = Uri.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
|
||||
const workspaceHash = hashPath(workspaceResource);
|
||||
const filePathHash = hashPath(backupResource);
|
||||
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
|
||||
assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
suite('loadBackupResource', () => {
|
||||
test('should return whether a backup resource exists', () => {
|
||||
return pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
|
||||
fs.writeFileSync(fooBackupPath, 'foo');
|
||||
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
|
||||
return service.loadBackupResource(fooFile).then(resource => {
|
||||
assert.ok(resource);
|
||||
assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath));
|
||||
return service.hasBackups().then(hasBackups => {
|
||||
assert.ok(hasBackups);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('backupResource', () => {
|
||||
test('text file', function () {
|
||||
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
});
|
||||
});
|
||||
|
||||
test('untitled file', function () {
|
||||
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
});
|
||||
});
|
||||
|
||||
test('text file (ITextSnapshot)', function () {
|
||||
const model = TextModel.createFromString('test');
|
||||
|
||||
return service.backupResource(fooFile, model.createSnapshot()).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('untitled file (ITextSnapshot)', function () {
|
||||
const model = TextModel.createFromString('test');
|
||||
|
||||
return service.backupResource(untitledFile, model.createSnapshot()).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('text file (large file, ITextSnapshot)', function () {
|
||||
const largeString = (new Array(10 * 1024)).join('Large String\n');
|
||||
const model = TextModel.createFromString(largeString);
|
||||
|
||||
return service.backupResource(fooFile, model.createSnapshot()).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`);
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('untitled file (large file, ITextSnapshot)', function () {
|
||||
const largeString = (new Array(10 * 1024)).join('Large String\n');
|
||||
const model = TextModel.createFromString(largeString);
|
||||
|
||||
return service.backupResource(untitledFile, model.createSnapshot()).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`);
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('discardResourceBackup', () => {
|
||||
test('text file', function () {
|
||||
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
return service.discardResourceBackup(fooFile).then(() => {
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('untitled file', function () {
|
||||
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
return service.discardResourceBackup(untitledFile).then(() => {
|
||||
assert.equal(fs.existsSync(untitledBackupPath), false);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('discardAllWorkspaceBackups', () => {
|
||||
test('text file', function () {
|
||||
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
return service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
|
||||
return service.discardAllWorkspaceBackups().then(() => {
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.existsSync(barBackupPath), false);
|
||||
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('untitled file', function () {
|
||||
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
return service.discardAllWorkspaceBackups().then(() => {
|
||||
assert.equal(fs.existsSync(untitledBackupPath), false);
|
||||
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should disable further backups', function () {
|
||||
return service.discardAllWorkspaceBackups().then(() => {
|
||||
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
assert.equal(fs.existsSync(workspaceBackupPath), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('getWorkspaceFileBackups', () => {
|
||||
test('("file") - text file', () => {
|
||||
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
return service.getWorkspaceFileBackups().then(textFiles => {
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
|
||||
return service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
return service.getWorkspaceFileBackups().then(textFiles => {
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('("file") - untitled file', () => {
|
||||
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
return service.getWorkspaceFileBackups().then(textFiles => {
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('("untitled") - untitled file', () => {
|
||||
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
return service.getWorkspaceFileBackups().then(textFiles => {
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveBackupContent', () => {
|
||||
test('should restore the original contents (untitled file)', () => {
|
||||
const contents = 'test\nand more stuff';
|
||||
service.backupResource(untitledFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
service.resolveBackupContent(service.toBackupResource(untitledFile)).then(factory => {
|
||||
assert.equal(contents, snapshotToString(factory!.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file)', () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'consectetur ',
|
||||
'adipiscing ßß elit',
|
||||
].join('');
|
||||
|
||||
service.backupResource(fooFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
|
||||
service.resolveBackupContent(service.toBackupResource(untitledFile)).then(factory => {
|
||||
assert.equal(contents, snapshotToString(factory!.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('BackupFilesModel', () => {
|
||||
test('simple', () => {
|
||||
const model = new BackupFilesModel();
|
||||
|
||||
const resource1 = Uri.file('test.html');
|
||||
|
||||
assert.equal(model.has(resource1), false);
|
||||
|
||||
model.add(resource1);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource1, 0), true);
|
||||
assert.equal(model.has(resource1, 1), false);
|
||||
|
||||
model.remove(resource1);
|
||||
|
||||
assert.equal(model.has(resource1), false);
|
||||
|
||||
model.add(resource1);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource1, 0), true);
|
||||
assert.equal(model.has(resource1, 1), false);
|
||||
|
||||
model.clear();
|
||||
|
||||
assert.equal(model.has(resource1), false);
|
||||
|
||||
model.add(resource1, 1);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource1, 0), false);
|
||||
assert.equal(model.has(resource1, 1), true);
|
||||
|
||||
const resource2 = Uri.file('test1.html');
|
||||
const resource3 = Uri.file('test2.html');
|
||||
const resource4 = Uri.file('test3.html');
|
||||
|
||||
model.add(resource2);
|
||||
model.add(resource3);
|
||||
model.add(resource4);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource2), true);
|
||||
assert.equal(model.has(resource3), true);
|
||||
assert.equal(model.has(resource4), true);
|
||||
});
|
||||
|
||||
test('resolve', () => {
|
||||
return pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
|
||||
fs.writeFileSync(fooBackupPath, 'foo');
|
||||
|
||||
const model = new BackupFilesModel();
|
||||
|
||||
return model.resolve(workspaceBackupPath).then(model => {
|
||||
assert.equal(model.has(Uri.file(fooBackupPath)), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('get', () => {
|
||||
const model = new BackupFilesModel();
|
||||
|
||||
assert.deepEqual(model.get(), []);
|
||||
|
||||
const file1 = Uri.file('/root/file/foo.html');
|
||||
const file2 = Uri.file('/root/file/bar.html');
|
||||
const untitled = Uri.file('/root/untitled/bar.html');
|
||||
|
||||
model.add(file1);
|
||||
model.add(file2);
|
||||
model.add(untitled);
|
||||
|
||||
assert.deepEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]);
|
||||
});
|
||||
});
|
||||
@@ -1,579 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as crypto from 'crypto';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService';
|
||||
import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { DefaultEndOfLine } from 'vs/editor/common/model';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
|
||||
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
|
||||
const backupHome = path.join(parentDir, 'Backups');
|
||||
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
|
||||
|
||||
const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
|
||||
const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource));
|
||||
const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo');
|
||||
const customFile = URI.parse('customScheme://some/path');
|
||||
const customFileWithFragment = URI.parse('customScheme2://some/path#fragment');
|
||||
const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
|
||||
const fooBarFile = URI.file(platform.isWindows ? 'c:\\Foo Bar' : '/Foo Bar');
|
||||
const untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
|
||||
const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile));
|
||||
const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile));
|
||||
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile));
|
||||
|
||||
class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
|
||||
|
||||
private config: IWindowConfiguration;
|
||||
|
||||
constructor(workspaceBackupPath: string) {
|
||||
super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
|
||||
|
||||
this.config = Object.create(null);
|
||||
this.config.backupPath = workspaceBackupPath;
|
||||
}
|
||||
|
||||
get configuration(): IWindowConfiguration {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
|
||||
class TestBackupFileService extends BackupFileService {
|
||||
|
||||
readonly fileService: IFileService;
|
||||
|
||||
constructor(workspace: URI, backupHome: string, workspacesJsonPath: string) {
|
||||
const fileService = new FileService(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
|
||||
const environmentService = new TestBackupEnvironmentService(workspaceBackupPath);
|
||||
|
||||
super(environmentService, fileService);
|
||||
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return super.toBackupResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
suite('BackupFileService', () => {
|
||||
let service: TestBackupFileService;
|
||||
|
||||
setup(async () => {
|
||||
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
|
||||
|
||||
// Delete any existing backups completely and then re-create it.
|
||||
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
|
||||
await pfs.mkdirp(backupHome);
|
||||
|
||||
return pfs.writeFile(workspacesJsonPath, '');
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
|
||||
});
|
||||
|
||||
suite('hashPath', () => {
|
||||
test('should correctly hash the path for untitled scheme URIs', () => {
|
||||
const uri = URI.from({
|
||||
scheme: 'untitled',
|
||||
path: 'Untitled-1'
|
||||
});
|
||||
const actual = hashPath(uri);
|
||||
// If these hashes change people will lose their backed up files!
|
||||
assert.equal(actual, '13264068d108c6901b3592ea654fcd57');
|
||||
assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
|
||||
});
|
||||
|
||||
test('should correctly hash the path for file scheme URIs', () => {
|
||||
const uri = URI.file('/foo');
|
||||
const actual = hashPath(uri);
|
||||
// If these hashes change people will lose their backed up files!
|
||||
if (platform.isWindows) {
|
||||
assert.equal(actual, 'dec1a583f52468a020bd120c3f01d812');
|
||||
} else {
|
||||
assert.equal(actual, '1effb2475fcfba4f9e8b8a1dbc8f3caf');
|
||||
}
|
||||
assert.equal(actual, crypto.createHash('md5').update(uri.fsPath).digest('hex'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('getBackupResource', () => {
|
||||
test('should get the correct backup path for text files', () => {
|
||||
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
|
||||
const backupResource = fooFile;
|
||||
const workspaceHash = hashPath(workspaceResource);
|
||||
const filePathHash = hashPath(backupResource);
|
||||
const expectedPath = URI.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath;
|
||||
assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
|
||||
});
|
||||
|
||||
test('should get the correct backup path for untitled files', () => {
|
||||
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePath>
|
||||
const backupResource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
|
||||
const workspaceHash = hashPath(workspaceResource);
|
||||
const filePathHash = hashPath(backupResource);
|
||||
const expectedPath = URI.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
|
||||
assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
suite('loadBackupResource', () => {
|
||||
test('should return whether a backup resource exists', async () => {
|
||||
await pfs.mkdirp(path.dirname(fooBackupPath));
|
||||
fs.writeFileSync(fooBackupPath, 'foo');
|
||||
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
|
||||
const resource = await service.loadBackupResource(fooFile);
|
||||
assert.ok(resource);
|
||||
assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath));
|
||||
const hasBackups = await service.hasBackups();
|
||||
assert.ok(hasBackups);
|
||||
});
|
||||
});
|
||||
|
||||
suite('backupResource', () => {
|
||||
test('text file', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
});
|
||||
|
||||
test('text file (with meta)', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false), undefined, { etag: '678', orphaned: true });
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath).toString(), `${fooFile.toString()} {"etag":"678","orphaned":true}\ntest`);
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
});
|
||||
|
||||
test('text file (ITextSnapshot)', async () => {
|
||||
const model = TextModel.createFromString('test');
|
||||
|
||||
await service.backupResource(fooFile, model.createSnapshot());
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('untitled file (ITextSnapshot)', async () => {
|
||||
const model = TextModel.createFromString('test');
|
||||
|
||||
await service.backupResource(untitledFile, model.createSnapshot());
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('text file (large file, ITextSnapshot)', async () => {
|
||||
const largeString = (new Array(10 * 1024)).join('Large String\n');
|
||||
const model = TextModel.createFromString(largeString);
|
||||
|
||||
await service.backupResource(fooFile, model.createSnapshot());
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`);
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('untitled file (large file, ITextSnapshot)', async () => {
|
||||
const largeString = (new Array(10 * 1024)).join('Large String\n');
|
||||
const model = TextModel.createFromString(largeString);
|
||||
|
||||
await service.backupResource(untitledFile, model.createSnapshot());
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`);
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
suite('discardResourceBackup', () => {
|
||||
test('text file', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
await service.discardResourceBackup(fooFile);
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
await service.discardResourceBackup(untitledFile);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), false);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('discardAllWorkspaceBackups', () => {
|
||||
test('text file', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
await service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
|
||||
await service.discardAllWorkspaceBackups();
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.existsSync(barBackupPath), false);
|
||||
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
await service.discardAllWorkspaceBackups();
|
||||
assert.equal(fs.existsSync(untitledBackupPath), false);
|
||||
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
|
||||
});
|
||||
|
||||
test('should disable further backups', async () => {
|
||||
await service.discardAllWorkspaceBackups();
|
||||
await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.existsSync(workspaceBackupPath), false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getWorkspaceFileBackups', () => {
|
||||
test('("file") - text file', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
const textFiles = await service.getWorkspaceFileBackups();
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
|
||||
await service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
const textFiles_1 = await service.getWorkspaceFileBackups();
|
||||
assert.deepEqual(textFiles_1.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
|
||||
});
|
||||
|
||||
test('("file") - untitled file', async () => {
|
||||
await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
const textFiles = await service.getWorkspaceFileBackups();
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]);
|
||||
});
|
||||
|
||||
test('("untitled") - untitled file', async () => {
|
||||
await service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
const textFiles = await service.getWorkspaceFileBackups();
|
||||
assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
|
||||
});
|
||||
});
|
||||
|
||||
suite('resolveBackupContent', () => {
|
||||
|
||||
interface IBackupTestMetaData {
|
||||
mtime?: number;
|
||||
size?: number;
|
||||
etag?: string;
|
||||
orphaned?: boolean;
|
||||
}
|
||||
|
||||
test('should restore the original contents (untitled file)', async () => {
|
||||
const contents = 'test\nand more stuff';
|
||||
|
||||
await testResolveBackup(untitledFile, contents);
|
||||
});
|
||||
|
||||
test('should restore the original contents (untitled file with metadata)', async () => {
|
||||
const contents = 'test\nand more stuff';
|
||||
|
||||
const meta = {
|
||||
etag: 'the Etag',
|
||||
size: 666,
|
||||
mtime: Date.now(),
|
||||
orphaned: true
|
||||
};
|
||||
|
||||
await testResolveBackup(untitledFile, contents, meta);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'consectetur ',
|
||||
'adipiscing ßß elit'
|
||||
].join('');
|
||||
|
||||
await testResolveBackup(fooFile, contents);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file - custom scheme)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'consectetur ',
|
||||
'adipiscing ßß elit'
|
||||
].join('');
|
||||
|
||||
await testResolveBackup(customFile, contents);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file with metadata)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'adipiscing ßß elit',
|
||||
'consectetur '
|
||||
].join('');
|
||||
|
||||
const meta = {
|
||||
etag: 'theEtag',
|
||||
size: 888,
|
||||
mtime: Date.now(),
|
||||
orphaned: false
|
||||
};
|
||||
|
||||
await testResolveBackup(fooFile, contents, meta);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file with metadata changed once)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'adipiscing ßß elit',
|
||||
'consectetur '
|
||||
].join('');
|
||||
|
||||
const meta = {
|
||||
etag: 'theEtag',
|
||||
size: 888,
|
||||
mtime: Date.now(),
|
||||
orphaned: false
|
||||
};
|
||||
|
||||
await testResolveBackup(fooFile, contents, meta);
|
||||
|
||||
// Change meta and test again
|
||||
meta.size = 999;
|
||||
await testResolveBackup(fooFile, contents, meta);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file with broken metadata)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'adipiscing ßß elit',
|
||||
'consectetur '
|
||||
].join('');
|
||||
|
||||
const meta = {
|
||||
etag: 'theEtag',
|
||||
size: 888,
|
||||
mtime: Date.now(),
|
||||
orphaned: false
|
||||
};
|
||||
|
||||
await service.backupResource(fooFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false), 1, meta);
|
||||
|
||||
assert.ok(await service.loadBackupResource(fooFile));
|
||||
|
||||
const fileContents = fs.readFileSync(fooBackupPath).toString();
|
||||
assert.equal(fileContents.indexOf(fooFile.toString()), 0);
|
||||
|
||||
const metaIndex = fileContents.indexOf('{');
|
||||
const newFileContents = fileContents.substring(0, metaIndex) + '{{' + fileContents.substr(metaIndex);
|
||||
fs.writeFileSync(fooBackupPath, newFileContents);
|
||||
|
||||
const backup = await service.resolveBackupContent(service.toBackupResource(fooFile));
|
||||
assert.equal(contents, snapshotToString(backup.value.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
|
||||
assert.ok(!backup.meta);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file with metadata and fragment URI)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'adipiscing ßß elit',
|
||||
'consectetur '
|
||||
].join('');
|
||||
|
||||
const meta = {
|
||||
etag: 'theEtag',
|
||||
size: 888,
|
||||
mtime: Date.now(),
|
||||
orphaned: false
|
||||
};
|
||||
|
||||
await testResolveBackup(customFileWithFragment, contents, meta);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file with space in name with metadata)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'adipiscing ßß elit',
|
||||
'consectetur '
|
||||
].join('');
|
||||
|
||||
const meta = {
|
||||
etag: 'theEtag',
|
||||
size: 888,
|
||||
mtime: Date.now(),
|
||||
orphaned: false
|
||||
};
|
||||
|
||||
await testResolveBackup(fooBarFile, contents, meta);
|
||||
});
|
||||
|
||||
test('should restore the original contents (text file with too large metadata to persist)', async () => {
|
||||
const contents = [
|
||||
'Lorem ipsum ',
|
||||
'dolor öäü sit amet ',
|
||||
'adipiscing ßß elit',
|
||||
'consectetur '
|
||||
].join('');
|
||||
|
||||
const meta = {
|
||||
etag: (new Array(100 * 1024)).join('Large String'),
|
||||
size: 888,
|
||||
mtime: Date.now(),
|
||||
orphaned: false
|
||||
};
|
||||
|
||||
await testResolveBackup(fooBarFile, contents, meta, null);
|
||||
});
|
||||
|
||||
async function testResolveBackup(resource: URI, contents: string, meta?: IBackupTestMetaData, expectedMeta?: IBackupTestMetaData | null) {
|
||||
if (typeof expectedMeta === 'undefined') {
|
||||
expectedMeta = meta;
|
||||
}
|
||||
|
||||
await service.backupResource(resource, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).createSnapshot(false), 1, meta);
|
||||
|
||||
assert.ok(await service.loadBackupResource(resource));
|
||||
|
||||
const backup = await service.resolveBackupContent<IBackupTestMetaData>(service.toBackupResource(resource));
|
||||
assert.equal(contents, snapshotToString(backup.value.create(platform.isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(true)));
|
||||
|
||||
if (expectedMeta) {
|
||||
assert.equal(backup.meta!.etag, expectedMeta.etag);
|
||||
assert.equal(backup.meta!.size, expectedMeta.size);
|
||||
assert.equal(backup.meta!.mtime, expectedMeta.mtime);
|
||||
assert.equal(backup.meta!.orphaned, expectedMeta.orphaned);
|
||||
} else {
|
||||
assert.ok(!backup.meta);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('BackupFilesModel', () => {
|
||||
|
||||
let service: TestBackupFileService;
|
||||
|
||||
setup(async () => {
|
||||
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
|
||||
|
||||
// Delete any existing backups completely and then re-create it.
|
||||
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
|
||||
await pfs.mkdirp(backupHome);
|
||||
|
||||
return pfs.writeFile(workspacesJsonPath, '');
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
|
||||
});
|
||||
|
||||
test('simple', () => {
|
||||
const model = new BackupFilesModel(service.fileService);
|
||||
|
||||
const resource1 = URI.file('test.html');
|
||||
|
||||
assert.equal(model.has(resource1), false);
|
||||
|
||||
model.add(resource1);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource1, 0), true);
|
||||
assert.equal(model.has(resource1, 1), false);
|
||||
assert.equal(model.has(resource1, 1, { foo: 'bar' }), false);
|
||||
|
||||
model.remove(resource1);
|
||||
|
||||
assert.equal(model.has(resource1), false);
|
||||
|
||||
model.add(resource1);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource1, 0), true);
|
||||
assert.equal(model.has(resource1, 1), false);
|
||||
|
||||
model.clear();
|
||||
|
||||
assert.equal(model.has(resource1), false);
|
||||
|
||||
model.add(resource1, 1);
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource1, 0), false);
|
||||
assert.equal(model.has(resource1, 1), true);
|
||||
|
||||
const resource2 = URI.file('test1.html');
|
||||
const resource3 = URI.file('test2.html');
|
||||
const resource4 = URI.file('test3.html');
|
||||
|
||||
model.add(resource2);
|
||||
model.add(resource3);
|
||||
model.add(resource4, undefined, { foo: 'bar' });
|
||||
|
||||
assert.equal(model.has(resource1), true);
|
||||
assert.equal(model.has(resource2), true);
|
||||
assert.equal(model.has(resource3), true);
|
||||
|
||||
assert.equal(model.has(resource4), true);
|
||||
assert.equal(model.has(resource4, undefined, { foo: 'bar' }), true);
|
||||
assert.equal(model.has(resource4, undefined, { bar: 'foo' }), false);
|
||||
});
|
||||
|
||||
test('resolve', async () => {
|
||||
await pfs.mkdirp(path.dirname(fooBackupPath));
|
||||
fs.writeFileSync(fooBackupPath, 'foo');
|
||||
const model = new BackupFilesModel(service.fileService);
|
||||
|
||||
const resolvedModel = await model.resolve(URI.file(workspaceBackupPath));
|
||||
assert.equal(resolvedModel.has(URI.file(fooBackupPath)), true);
|
||||
});
|
||||
|
||||
test('get', () => {
|
||||
const model = new BackupFilesModel(service.fileService);
|
||||
|
||||
assert.deepEqual(model.get(), []);
|
||||
|
||||
const file1 = URI.file('/root/file/foo.html');
|
||||
const file2 = URI.file('/root/file/bar.html');
|
||||
const untitled = URI.file('/root/untitled/bar.html');
|
||||
|
||||
model.add(file1);
|
||||
model.add(file2);
|
||||
model.add(untitled);
|
||||
|
||||
assert.deepEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]);
|
||||
});
|
||||
});
|
||||
@@ -666,7 +666,7 @@ class CachedFolderConfiguration extends Disposable implements IFolderConfigurati
|
||||
private readonly configurationCache: IConfigurationCache
|
||||
) {
|
||||
super();
|
||||
this.key = createSHA1(join(folder.path, configFolderRelativePath)).then(key => ({ type: 'folder', key }));
|
||||
this.key = createSHA1(join(folder.path, configFolderRelativePath)).then(key => (<ConfigurationKey>{ type: 'folder', key }));
|
||||
this.configurationModel = new ConfigurationModel();
|
||||
}
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
}
|
||||
|
||||
private compareFolders(currentFolders: IWorkspaceFolder[], newFolders: IWorkspaceFolder[]): IWorkspaceFoldersChangeEvent {
|
||||
const result: IWorkspaceFoldersChangeEvent = { added: [], removed: [], changed: [] };
|
||||
const result = { added: [], removed: [], changed: [] } as IWorkspaceFoldersChangeEvent;
|
||||
result.added = newFolders.filter(newFolder => !currentFolders.some(currentFolder => newFolder.uri.toString() === currentFolder.uri.toString()));
|
||||
for (let currentIndex = 0; currentIndex < currentFolders.length; currentIndex++) {
|
||||
let currentFolder = currentFolders[currentIndex];
|
||||
|
||||
@@ -97,7 +97,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
|
||||
x = elementPosition.left;
|
||||
y = elementPosition.top + elementPosition.height;
|
||||
} else {
|
||||
const pos: { x: number; y: number; } = anchor;
|
||||
const pos = <{ x: number; y: number; }>anchor;
|
||||
x = pos.x + 1; /* prevent first item from being selected automatically under mouse */
|
||||
y = pos.y;
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ export class FileDialogService implements IFileDialogService {
|
||||
}
|
||||
|
||||
private shouldUseSimplified(schema: string): boolean {
|
||||
const setting = this.configurationService.getValue('files.simpleDialog.enable');
|
||||
return (schema !== Schemas.file) || (setting === true);
|
||||
const setting = this.configurationService.getValue('workbench.dialogs.useSimplified');
|
||||
return (schema !== Schemas.file) || ((setting === 'true') || (setting === true));
|
||||
}
|
||||
|
||||
private ensureFileSchema(schema: string): string[] {
|
||||
|
||||
@@ -22,12 +22,11 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { equalsIgnoreCase, format, startsWithIgnoreCase } from 'vs/base/common/strings';
|
||||
import { RemoteFileDialogContext } from 'vs/workbench/common/contextkeys';
|
||||
import { equalsIgnoreCase, format } from 'vs/base/common/strings';
|
||||
import { OpenLocalFileAction, OpenLocalFileFolderAction, OpenLocalFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { isValidBasename } from 'vs/base/common/extpath';
|
||||
import { RemoteFileDialogContext } from 'vs/workbench/browser/contextkeys';
|
||||
|
||||
interface FileQuickPickItem extends IQuickPickItem {
|
||||
uri: URI;
|
||||
@@ -41,6 +40,11 @@ enum UpdateResult {
|
||||
InvalidPath
|
||||
}
|
||||
|
||||
// Reference: https://en.wikipedia.org/wiki/Filename
|
||||
const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g;
|
||||
const UNIX_INVALID_FILE_CHARS = /[\\/]/g;
|
||||
const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])$/i;
|
||||
|
||||
export class RemoteFileDialog {
|
||||
private options: IOpenDialogOptions;
|
||||
private currentFolder: URI;
|
||||
@@ -59,7 +63,6 @@ export class RemoteFileDialog {
|
||||
private userHome: URI;
|
||||
private badPath: string | undefined;
|
||||
private remoteAgentEnvironment: IRemoteAgentEnvironment | null;
|
||||
private separator: string;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@@ -156,7 +159,6 @@ export class RemoteFileDialog {
|
||||
private async pickResource(isSave: boolean = false): Promise<URI | undefined> {
|
||||
this.allowFolderSelection = !!this.options.canSelectFolders;
|
||||
this.allowFileSelection = !!this.options.canSelectFiles;
|
||||
this.separator = this.labelService.getSeparator(this.scheme, this.remoteAuthority);
|
||||
this.hidden = false;
|
||||
let homedir: URI = this.options.defaultUri ? this.options.defaultUri : this.workspaceContextService.getWorkspace().folders[0].uri;
|
||||
let stat: IFileStat | undefined;
|
||||
@@ -278,11 +280,10 @@ export class RemoteFileDialog {
|
||||
// If the user has just entered more bad path, don't change anything
|
||||
if (!equalsIgnoreCase(value, this.constructFullUserPath()) && !this.isBadSubpath(value)) {
|
||||
this.filePickBox.validationMessage = undefined;
|
||||
const filePickBoxUri = this.filePickBoxValue();
|
||||
const valueUri = resources.removeTrailingPathSeparator(filePickBoxUri);
|
||||
const valueUri = this.remoteUriFrom(this.trimTrailingSlash(this.filePickBox.value));
|
||||
let updated: UpdateResult = UpdateResult.NotUpdated;
|
||||
if (!resources.isEqual(resources.removeTrailingPathSeparator(this.currentFolder), valueUri, true)) {
|
||||
updated = await this.tryUpdateItems(value, filePickBoxUri);
|
||||
if (!resources.isEqual(this.remoteUriFrom(this.trimTrailingSlash(this.pathFromUri(this.currentFolder))), valueUri, true)) {
|
||||
updated = await this.tryUpdateItems(value, this.remoteUriFrom(this.filePickBox.value));
|
||||
}
|
||||
if (updated === UpdateResult.NotUpdated) {
|
||||
this.setActiveItems(value);
|
||||
@@ -304,7 +305,7 @@ export class RemoteFileDialog {
|
||||
|
||||
this.filePickBox.show();
|
||||
this.contextKey.set(true);
|
||||
await this.updateItems(homedir, true, this.trailing);
|
||||
await this.updateItems(homedir, false, this.trailing);
|
||||
if (this.trailing) {
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length];
|
||||
} else {
|
||||
@@ -330,81 +331,72 @@ export class RemoteFileDialog {
|
||||
return this.pathAppend(this.currentFolder, this.userEnteredPathSegment);
|
||||
}
|
||||
|
||||
private filePickBoxValue(): URI {
|
||||
// The file pick box can't render everything, so we use the current folder to create the uri so that it is an existing path.
|
||||
const directUri = this.remoteUriFrom(this.filePickBox.value);
|
||||
const currentPath = this.pathFromUri(this.currentFolder);
|
||||
if (equalsIgnoreCase(this.filePickBox.value, currentPath)) {
|
||||
return this.currentFolder;
|
||||
}
|
||||
const currentDisplayUri = this.remoteUriFrom(currentPath);
|
||||
const relativePath = resources.relativePath(currentDisplayUri, directUri);
|
||||
const isSameRoot = (this.filePickBox.value.length > 1 && currentPath.length > 1) ? equalsIgnoreCase(this.filePickBox.value.substr(0, 2), currentPath.substr(0, 2)) : false;
|
||||
if (relativePath && isSameRoot) {
|
||||
return resources.joinPath(this.currentFolder, relativePath);
|
||||
} else {
|
||||
return directUri;
|
||||
}
|
||||
}
|
||||
|
||||
private async onDidAccept(): Promise<URI | undefined> {
|
||||
this.filePickBox.busy = true;
|
||||
if (this.filePickBox.activeItems.length === 1) {
|
||||
const item = this.filePickBox.selectedItems[0];
|
||||
if (item.isFolder) {
|
||||
if (this.trailing) {
|
||||
await this.updateItems(item.uri, true, this.trailing);
|
||||
} else {
|
||||
// When possible, cause the update to happen by modifying the input box.
|
||||
// This allows all input box updates to happen first, and uses the same code path as the user typing.
|
||||
const newPath = this.pathFromUri(item.uri);
|
||||
if (startsWithIgnoreCase(newPath, this.filePickBox.value)) {
|
||||
const insertValue = newPath.substring(this.filePickBox.value.length, newPath.length);
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
|
||||
this.insertText(newPath, insertValue);
|
||||
} else if ((item.label === '..') && startsWithIgnoreCase(this.filePickBox.value, newPath)) {
|
||||
this.filePickBox.valueSelection = [newPath.length, this.filePickBox.value.length];
|
||||
this.insertText(newPath, '');
|
||||
} else {
|
||||
await this.updateItems(item.uri, true);
|
||||
}
|
||||
}
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
let resolveValue: URI | undefined;
|
||||
let navigateValue: URI | undefined;
|
||||
let inputUri: URI | undefined;
|
||||
let inputUriDirname: URI | undefined;
|
||||
let stat: IFileStat | undefined;
|
||||
let statDirname: IFileStat | undefined;
|
||||
try {
|
||||
inputUri = resources.removeTrailingPathSeparator(this.remoteUriFrom(this.filePickBox.value));
|
||||
inputUriDirname = resources.dirname(inputUri);
|
||||
statDirname = await this.fileService.resolve(inputUriDirname);
|
||||
stat = await this.fileService.resolve(inputUri);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Find resolve value
|
||||
if (this.filePickBox.activeItems.length === 0) {
|
||||
if (!this.requiresTrailing && resources.isEqual(this.currentFolder, inputUri, true)) {
|
||||
resolveValue = inputUri;
|
||||
} else if (statDirname && statDirname.isDirectory) {
|
||||
resolveValue = inputUri;
|
||||
} else if (stat && stat.isDirectory) {
|
||||
navigateValue = inputUri;
|
||||
}
|
||||
} else {
|
||||
// If the items have updated, don't try to resolve
|
||||
if ((await this.tryUpdateItems(this.filePickBox.value, this.filePickBoxValue())) !== UpdateResult.NotUpdated) {
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
} else if (this.filePickBox.activeItems.length === 1) {
|
||||
const item = this.filePickBox.selectedItems[0];
|
||||
if (item) {
|
||||
if (!item.isFolder) {
|
||||
resolveValue = item.uri;
|
||||
} else {
|
||||
navigateValue = item.uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resolveValue: URI | undefined;
|
||||
// Find resolve value
|
||||
if (this.filePickBox.activeItems.length === 0) {
|
||||
resolveValue = this.filePickBoxValue();
|
||||
} else if (this.filePickBox.activeItems.length === 1) {
|
||||
resolveValue = this.filePickBox.selectedItems[0].uri;
|
||||
}
|
||||
if (resolveValue) {
|
||||
resolveValue = this.addPostfix(resolveValue);
|
||||
}
|
||||
if (await this.validate(resolveValue)) {
|
||||
this.filePickBox.busy = false;
|
||||
return resolveValue;
|
||||
|
||||
if (navigateValue) {
|
||||
// Try to navigate into the folder.
|
||||
await this.updateItems(navigateValue, true, this.trailing);
|
||||
} else {
|
||||
if (resolveValue) {
|
||||
resolveValue = this.addPostfix(resolveValue);
|
||||
}
|
||||
if (await this.validate(resolveValue)) {
|
||||
this.filePickBox.busy = false;
|
||||
return resolveValue;
|
||||
}
|
||||
}
|
||||
this.filePickBox.busy = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async tryUpdateItems(value: string, valueUri: URI): Promise<UpdateResult> {
|
||||
if ((value.length > 0) && ((value[value.length - 1] === '~') || (value[0] === '~'))) {
|
||||
if (this.filePickBox.busy) {
|
||||
this.badPath = undefined;
|
||||
return UpdateResult.Updating;
|
||||
} else if ((value.length > 0) && ((value[value.length - 1] === '~') || (value[0] === '~'))) {
|
||||
let newDir = this.userHome;
|
||||
if ((value[0] === '~') && (value.length > 1)) {
|
||||
newDir = resources.joinPath(newDir, value.substring(1));
|
||||
}
|
||||
await this.updateItems(newDir, true);
|
||||
return UpdateResult.Updated;
|
||||
} else if (!resources.isEqual(this.currentFolder, valueUri, true) && (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true)))) {
|
||||
} else if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) {
|
||||
let stat: IFileStat | undefined;
|
||||
try {
|
||||
stat = await this.fileService.resolve(valueUri);
|
||||
@@ -423,7 +415,7 @@ export class RemoteFileDialog {
|
||||
return UpdateResult.InvalidPath;
|
||||
} else {
|
||||
const inputUriDirname = resources.dirname(valueUri);
|
||||
if (!resources.isEqual(resources.removeTrailingPathSeparator(this.currentFolder), inputUriDirname, true)) {
|
||||
if (!resources.isEqual(this.remoteUriFrom(this.trimTrailingSlash(this.pathFromUri(this.currentFolder))), inputUriDirname, true)) {
|
||||
let statWithoutTrailing: IFileStat | undefined;
|
||||
try {
|
||||
statWithoutTrailing = await this.fileService.resolve(inputUriDirname);
|
||||
@@ -614,7 +606,7 @@ export class RemoteFileDialog {
|
||||
// Show a yes/no prompt
|
||||
const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri));
|
||||
return this.yesNoPrompt(uri, message);
|
||||
} else if (!(isValidBasename(resources.basename(uri), await this.isWindowsOS()))) {
|
||||
} else if (!(await this.isValidBaseName(resources.basename(uri)))) {
|
||||
// Filename not allowed
|
||||
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.');
|
||||
return Promise.resolve(false);
|
||||
@@ -646,16 +638,24 @@ export class RemoteFileDialog {
|
||||
this.userEnteredPathSegment = trailing ? trailing : '';
|
||||
this.autoCompletePathSegment = '';
|
||||
const newValue = trailing ? this.pathFromUri(resources.joinPath(newFolder, trailing)) : this.pathFromUri(newFolder, true);
|
||||
this.currentFolder = resources.addTrailingPathSeparator(newFolder, this.separator);
|
||||
const oldFolder = this.currentFolder;
|
||||
const newFolderPath = this.pathFromUri(newFolder, true);
|
||||
this.currentFolder = this.remoteUriFrom(newFolderPath);
|
||||
return this.createItems(this.currentFolder).then(items => {
|
||||
this.filePickBox.items = items;
|
||||
if (this.allowFolderSelection) {
|
||||
this.filePickBox.activeItems = [];
|
||||
}
|
||||
// the user might have continued typing while we were updating. Only update the input box if it doesn't match the directory.
|
||||
if (!equalsIgnoreCase(this.filePickBox.value, newValue) && force) {
|
||||
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
|
||||
this.insertText(newValue, newValue);
|
||||
if (!equalsIgnoreCase(this.filePickBox.value, newValue)) {
|
||||
// the user might have continued typing while we were updating. Only update the input box if it doesn't match the directory.
|
||||
if (!equalsIgnoreCase(this.filePickBox.value.substring(0, newValue.length), newValue)) {
|
||||
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
|
||||
this.insertText(newValue, newValue);
|
||||
} else if (force || equalsIgnoreCase(this.pathFromUri(resources.dirname(oldFolder), true), newFolderPath)) {
|
||||
// This is the case where the user went up one dir or is clicking on dirs. We need to make sure that we remove the final dir.
|
||||
this.filePickBox.valueSelection = [newFolderPath.length, this.filePickBox.value.length];
|
||||
this.insertText(newValue, '');
|
||||
}
|
||||
}
|
||||
if (force && trailing) {
|
||||
// Keep the cursor position in front of the save as name.
|
||||
@@ -668,14 +668,15 @@ export class RemoteFileDialog {
|
||||
}
|
||||
|
||||
private pathFromUri(uri: URI, endWithSeparator: boolean = false): string {
|
||||
let result: string = uri.fsPath.replace(/\n/g, '');
|
||||
if (this.separator === '/') {
|
||||
result = result.replace(/\\/g, this.separator);
|
||||
const sep = this.labelService.getSeparator(uri.scheme, uri.authority);
|
||||
let result: string;
|
||||
if (sep === '/') {
|
||||
result = uri.fsPath.replace(/\\/g, sep);
|
||||
} else {
|
||||
result = result.replace(/\//g, this.separator);
|
||||
result = uri.fsPath.replace(/\//g, sep);
|
||||
}
|
||||
if (endWithSeparator && !this.endsWithSlash(result)) {
|
||||
result = result + this.separator;
|
||||
result = result + sep;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -683,7 +684,7 @@ export class RemoteFileDialog {
|
||||
private pathAppend(uri: URI, additional: string): string {
|
||||
if ((additional === '..') || (additional === '.')) {
|
||||
const basePath = this.pathFromUri(uri);
|
||||
return basePath + (this.endsWithSlash(basePath) ? '' : this.separator) + additional;
|
||||
return basePath + (this.endsWithSlash(basePath) ? '' : this.labelService.getSeparator(uri.scheme, uri.authority)) + additional;
|
||||
} else {
|
||||
return this.pathFromUri(resources.joinPath(uri, additional));
|
||||
}
|
||||
@@ -698,6 +699,37 @@ export class RemoteFileDialog {
|
||||
return isWindowsOS;
|
||||
}
|
||||
|
||||
private async isValidBaseName(name: string): Promise<boolean> {
|
||||
if (!name || name.length === 0 || /^\s+$/.test(name)) {
|
||||
return false; // require a name that is not just whitespace
|
||||
}
|
||||
|
||||
const isWindowsOS = await this.isWindowsOS();
|
||||
const INVALID_FILE_CHARS = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS;
|
||||
INVALID_FILE_CHARS.lastIndex = 0; // the holy grail of software development
|
||||
if (INVALID_FILE_CHARS.test(name)) {
|
||||
return false; // check for certain invalid file characters
|
||||
}
|
||||
|
||||
if (isWindowsOS && WINDOWS_FORBIDDEN_NAMES.test(name)) {
|
||||
return false; // check for certain invalid file names
|
||||
}
|
||||
|
||||
if (name === '.' || name === '..') {
|
||||
return false; // check for reserved values
|
||||
}
|
||||
|
||||
if (isWindowsOS && name[name.length - 1] === '.') {
|
||||
return false; // Windows: file cannot end with a "."
|
||||
}
|
||||
|
||||
if (isWindowsOS && name.length !== name.trim().length) {
|
||||
return false; // Windows: file cannot end with a whitespace
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private endsWithSlash(s: string) {
|
||||
return /[\/\\]$/.test(s);
|
||||
}
|
||||
@@ -711,7 +743,7 @@ export class RemoteFileDialog {
|
||||
private createBackItem(currFolder: URI): FileQuickPickItem | null {
|
||||
const parentFolder = resources.dirname(currFolder)!;
|
||||
if (!resources.isEqual(currFolder, parentFolder, true)) {
|
||||
return { label: '..', uri: resources.addTrailingPathSeparator(parentFolder, this.separator), isFolder: true };
|
||||
return { label: '..', uri: resources.dirname(currFolder), isFolder: true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -769,7 +801,6 @@ export class RemoteFileDialog {
|
||||
const stat = await this.fileService.resolve(fullPath);
|
||||
if (stat.isDirectory) {
|
||||
filename = this.basenameWithTrailingSlash(fullPath);
|
||||
fullPath = resources.addTrailingPathSeparator(fullPath, this.separator);
|
||||
return { label: filename, uri: fullPath, isFolder: true, iconClasses: getIconClasses(this.modelService, this.modeService, fullPath || undefined, FileKind.FOLDER) };
|
||||
} else if (!stat.isDirectory && this.allowFileSelection && this.filterFile(fullPath)) {
|
||||
return { label: filename, uri: fullPath, isFolder: false, iconClasses: getIconClasses(this.modelService, this.modeService, fullPath || undefined) };
|
||||
|
||||
@@ -532,7 +532,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
const untitledInput = <IUntitledResourceInput>input;
|
||||
if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
return convertEditorInput(this.untitledEditorService.createOrGet(untitledInput.resource, untitledInput.mode, untitledInput.contents, untitledInput.encoding), undefined, this.instantiationService);
|
||||
|
||||
let modeId: string = untitledInput.language ? untitledInput.language : getFileMode(this.instantiationService, untitledInput.resource);
|
||||
return convertEditorInput(this.untitledEditorService.createOrGet(untitledInput.resource, modeId, untitledInput.contents, untitledInput.encoding), undefined, this.instantiationService);
|
||||
}
|
||||
|
||||
// Resource Editor Support
|
||||
@@ -544,14 +546,14 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
return convertEditorInput(this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.mode, resourceInput.forceFile) as EditorInput,
|
||||
return convertEditorInput(this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.forceFile) as EditorInput,
|
||||
undefined, this.instantiationService);
|
||||
}
|
||||
|
||||
throw new Error('Unknown input type');
|
||||
}
|
||||
|
||||
private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, mode: string | undefined, forceFile: boolean | undefined): ICachedEditorInput {
|
||||
private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string | undefined, description: string | undefined, encoding: string | undefined, forceFile: boolean | undefined): ICachedEditorInput {
|
||||
if (EditorService.CACHE.has(resource)) {
|
||||
const input = EditorService.CACHE.get(resource)!;
|
||||
if (input instanceof ResourceEditorInput) {
|
||||
@@ -562,18 +564,10 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
if (description) {
|
||||
input.setDescription(description);
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
input.setPreferredMode(mode);
|
||||
}
|
||||
} else if (!(input instanceof DataUriEditorInput)) {
|
||||
if (encoding) {
|
||||
input.setPreferredEncoding(encoding);
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
input.setPreferredMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
@@ -583,7 +577,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
// File
|
||||
if (forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resource)) {
|
||||
input = this.fileInputFactory.createFileInput(resource, encoding, mode, instantiationService);
|
||||
input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService);
|
||||
}
|
||||
|
||||
// Data URI
|
||||
@@ -593,12 +587,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
// Resource
|
||||
else {
|
||||
input = instantiationService.createInstance(ResourceEditorInput, label, description, resource, mode);
|
||||
input = instantiationService.createInstance(ResourceEditorInput, label, description, resource);
|
||||
}
|
||||
|
||||
// Add to cache and remove when input gets disposed
|
||||
EditorService.CACHE.set(resource, input);
|
||||
Event.once(input.onDispose)(() => EditorService.CACHE.delete(resource));
|
||||
Event.once(input.onDispose)(() => {
|
||||
EditorService.CACHE.delete(resource);
|
||||
});
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ export class TestEditorControl extends BaseEditor {
|
||||
|
||||
constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyFileEditorForEditorGroupService', NullTelemetryService, new TestThemeService(), new TestStorageService()); }
|
||||
|
||||
async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
super.setInput(input, options, token);
|
||||
|
||||
await input.resolve();
|
||||
return input.resolve().then(() => undefined);
|
||||
}
|
||||
|
||||
getId(): string { return 'MyFileEditorForEditorGroupService'; }
|
||||
@@ -45,8 +45,6 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput {
|
||||
setEncoding(encoding: string) { }
|
||||
getEncoding(): string { return null!; }
|
||||
setPreferredEncoding(encoding: string) { }
|
||||
setMode(mode: string) { }
|
||||
setPreferredMode(mode: string) { }
|
||||
getResource(): URI { return this.resource; }
|
||||
setForceOpenAsBinary(): void { }
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ import { timeout } from 'vs/base/common/async';
|
||||
import { toResource } from 'vs/base/test/common/utils';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
|
||||
// {{SQL CARBON EDIT}} - Disable editor tests
|
||||
/*
|
||||
@@ -38,10 +36,10 @@ export class TestEditorControl extends BaseEditor {
|
||||
|
||||
constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyTestEditorForEditorService', NullTelemetryService, new TestThemeService(), new TestStorageService()); }
|
||||
|
||||
async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
super.setInput(input, options, token);
|
||||
|
||||
await input.resolve();
|
||||
return input.resolve().then(() => undefined);
|
||||
}
|
||||
|
||||
getId(): string { return 'MyTestEditorForEditorService'; }
|
||||
@@ -60,8 +58,6 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput {
|
||||
setEncoding(encoding: string) { }
|
||||
getEncoding(): string { return null!; }
|
||||
setPreferredEncoding(encoding: string) { }
|
||||
setMode(mode: string) { }
|
||||
setPreferredMode(mode: string) { }
|
||||
getResource(): URI { return this.resource; }
|
||||
setForceOpenAsBinary(): void { }
|
||||
setFailToOpen(): void {
|
||||
@@ -81,7 +77,7 @@ class FileServiceProvider extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
*/suite('EditorService', () => {/*
|
||||
*/suite('Editor service', () => {/*
|
||||
|
||||
function registerTestEditorInput(): void {
|
||||
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), new SyncDescriptor(TestEditorInput));
|
||||
@@ -89,7 +85,7 @@ class FileServiceProvider extends Disposable {
|
||||
|
||||
registerTestEditorInput();
|
||||
|
||||
test('basics', async () => {
|
||||
test('basics', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart);
|
||||
@@ -118,49 +114,51 @@ class FileServiceProvider extends Disposable {
|
||||
didCloseEditorListenerCounter++;
|
||||
});
|
||||
|
||||
await part.whenRestored;
|
||||
return part.whenRestored.then(() => {
|
||||
|
||||
// Open input
|
||||
let editor = await service.openEditor(input, { pinned: true });
|
||||
// Open input
|
||||
return service.openEditor(input, { pinned: true }).then(editor => {
|
||||
assert.ok(editor instanceof TestEditorControl);
|
||||
assert.equal(editor, service.activeControl);
|
||||
assert.equal(input, service.activeEditor);
|
||||
assert.equal(service.visibleControls.length, 1);
|
||||
assert.equal(service.visibleControls[0], editor);
|
||||
assert.ok(!service.activeTextEditorWidget);
|
||||
assert.equal(service.visibleTextEditorWidgets.length, 0);
|
||||
assert.equal(service.isOpen(input), true);
|
||||
assert.equal(service.getOpened({ resource: input.getResource() }), input);
|
||||
assert.equal(service.isOpen(input, part.activeGroup), true);
|
||||
assert.equal(activeEditorChangeEventCounter, 1);
|
||||
assert.equal(visibleEditorChangeEventCounter, 1);
|
||||
|
||||
assert.ok(editor instanceof TestEditorControl);
|
||||
assert.equal(editor, service.activeControl);
|
||||
assert.equal(input, service.activeEditor);
|
||||
assert.equal(service.visibleControls.length, 1);
|
||||
assert.equal(service.visibleControls[0], editor);
|
||||
assert.ok(!service.activeTextEditorWidget);
|
||||
assert.equal(service.visibleTextEditorWidgets.length, 0);
|
||||
assert.equal(service.isOpen(input), true);
|
||||
assert.equal(service.getOpened({ resource: input.getResource() }), input);
|
||||
assert.equal(service.isOpen(input, part.activeGroup), true);
|
||||
assert.equal(activeEditorChangeEventCounter, 1);
|
||||
assert.equal(visibleEditorChangeEventCounter, 1);
|
||||
// Close input
|
||||
return editor!.group!.closeEditor(input).then(() => {
|
||||
assert.equal(didCloseEditorListenerCounter, 1);
|
||||
assert.equal(activeEditorChangeEventCounter, 2);
|
||||
assert.equal(visibleEditorChangeEventCounter, 2);
|
||||
assert.ok(input.gotDisposed);
|
||||
|
||||
// Close input
|
||||
await editor!.group!.closeEditor(input);
|
||||
// Open again 2 inputs
|
||||
return service.openEditor(input, { pinned: true }).then(editor => {
|
||||
return service.openEditor(otherInput, { pinned: true }).then(editor => {
|
||||
assert.equal(service.visibleControls.length, 1);
|
||||
assert.equal(service.isOpen(input), true);
|
||||
assert.equal(service.isOpen(otherInput), true);
|
||||
|
||||
assert.equal(didCloseEditorListenerCounter, 1);
|
||||
assert.equal(activeEditorChangeEventCounter, 2);
|
||||
assert.equal(visibleEditorChangeEventCounter, 2);
|
||||
assert.ok(input.gotDisposed);
|
||||
assert.equal(activeEditorChangeEventCounter, 4);
|
||||
assert.equal(visibleEditorChangeEventCounter, 4);
|
||||
|
||||
// Open again 2 inputs
|
||||
await service.openEditor(input, { pinned: true });
|
||||
editor = await service.openEditor(otherInput, { pinned: true });
|
||||
|
||||
assert.equal(service.visibleControls.length, 1);
|
||||
assert.equal(service.isOpen(input), true);
|
||||
assert.equal(service.isOpen(otherInput), true);
|
||||
|
||||
assert.equal(activeEditorChangeEventCounter, 4);
|
||||
assert.equal(visibleEditorChangeEventCounter, 4);
|
||||
|
||||
activeEditorChangeListener.dispose();
|
||||
visibleEditorChangeListener.dispose();
|
||||
didCloseEditorListener.dispose();
|
||||
activeEditorChangeListener.dispose();
|
||||
visibleEditorChangeListener.dispose();
|
||||
didCloseEditorListener.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('openEditors() / replaceEditors()', async () => {
|
||||
test('openEditors() / replaceEditors()', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart);
|
||||
@@ -175,16 +173,18 @@ class FileServiceProvider extends Disposable {
|
||||
const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openEditors'));
|
||||
const replaceInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-openEditors'));
|
||||
|
||||
await part.whenRestored;
|
||||
return part.whenRestored.then(() => {
|
||||
|
||||
// Open editors
|
||||
await service.openEditors([{ editor: input }, { editor: otherInput }]);
|
||||
assert.equal(part.activeGroup.count, 2);
|
||||
// Open editors
|
||||
return service.openEditors([{ editor: input }, { editor: otherInput }]).then(() => {
|
||||
assert.equal(part.activeGroup.count, 2);
|
||||
|
||||
// Replace editors
|
||||
await service.replaceEditors([{ editor: input, replacement: replaceInput }], part.activeGroup);
|
||||
assert.equal(part.activeGroup.count, 2);
|
||||
assert.equal(part.activeGroup.getIndexOfEditor(replaceInput), 0);
|
||||
return service.replaceEditors([{ editor: input, replacement: replaceInput }], part.activeGroup).then(() => {
|
||||
assert.equal(part.activeGroup.count, 2);
|
||||
assert.equal(part.activeGroup.getIndexOfEditor(replaceInput), 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('caching', function () {
|
||||
@@ -236,15 +236,10 @@ class FileServiceProvider extends Disposable {
|
||||
assert.ok(!input1AgainAndAgain!.isDisposed());
|
||||
});
|
||||
|
||||
test('createInput', async function () {
|
||||
test('createInput', function () {
|
||||
const instantiationService = workbenchInstantiationService();
|
||||
const service: EditorService = <any>instantiationService.createInstance(EditorService);
|
||||
|
||||
const mode = 'create-input-test';
|
||||
ModesRegistry.registerLanguage({
|
||||
id: mode,
|
||||
});
|
||||
|
||||
// Untyped Input (file)
|
||||
let input = service.createInput({ resource: toResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof FileEditorInput);
|
||||
@@ -257,18 +252,6 @@ class FileServiceProvider extends Disposable {
|
||||
contentInput = <FileEditorInput>input;
|
||||
assert.equal(contentInput.getPreferredEncoding(), 'utf16le');
|
||||
|
||||
// Untyped Input (file, mode)
|
||||
input = service.createInput({ resource: toResource.call(this, '/index.html'), mode });
|
||||
assert(input instanceof FileEditorInput);
|
||||
contentInput = <FileEditorInput>input;
|
||||
assert.equal(contentInput.getPreferredMode(), mode);
|
||||
|
||||
// Untyped Input (file, different mode)
|
||||
input = service.createInput({ resource: toResource.call(this, '/index.html'), mode: 'text' });
|
||||
assert(input instanceof FileEditorInput);
|
||||
contentInput = <FileEditorInput>input;
|
||||
assert.equal(contentInput.getPreferredMode(), 'text');
|
||||
|
||||
// Untyped Input (untitled)
|
||||
input = service.createInput({ options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof UntitledEditorInput);
|
||||
@@ -276,14 +259,6 @@ class FileServiceProvider extends Disposable {
|
||||
// Untyped Input (untitled with contents)
|
||||
input = service.createInput({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof UntitledEditorInput);
|
||||
let model = await input.resolve() as UntitledEditorModel;
|
||||
assert.equal(model.textEditorModel!.getValue(), 'Hello Untitled');
|
||||
|
||||
// Untyped Input (untitled with mode)
|
||||
input = service.createInput({ mode, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof UntitledEditorInput);
|
||||
model = await input.resolve() as UntitledEditorModel;
|
||||
assert.equal(model.getMode(), mode);
|
||||
|
||||
// Untyped Input (untitled with file path)
|
||||
input = service.createInput({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
@@ -303,10 +278,6 @@ class FileServiceProvider extends Disposable {
|
||||
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
|
||||
|
||||
provider.dispose();
|
||||
|
||||
// Untyped Input (resource)
|
||||
input = service.createInput({ resource: URI.parse('custom:resource') });
|
||||
assert(input instanceof ResourceEditorInput);
|
||||
});
|
||||
|
||||
test('delegate', function (done) {
|
||||
@@ -329,7 +300,7 @@ class FileServiceProvider extends Disposable {
|
||||
|
||||
const ed = instantiationService.createInstance(MyEditor, 'my.editor');
|
||||
|
||||
const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'), undefined);
|
||||
const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'));
|
||||
const delegate = instantiationService.createInstance(DelegatingEditorService);
|
||||
delegate.setEditorOpenHandler((group: IEditorGroup, input: IEditorInput, options?: EditorOptions) => {
|
||||
assert.strictEqual(input, inp);
|
||||
@@ -342,7 +313,7 @@ class FileServiceProvider extends Disposable {
|
||||
delegate.openEditor(inp);
|
||||
});
|
||||
|
||||
test('close editor does not dispose when editor opened in other group', async () => {
|
||||
test('close editor does not dispose when editor opened in other group', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart);
|
||||
@@ -358,26 +329,30 @@ class FileServiceProvider extends Disposable {
|
||||
const rootGroup = part.activeGroup;
|
||||
const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
|
||||
|
||||
await part.whenRestored;
|
||||
return part.whenRestored.then(() => {
|
||||
|
||||
// Open input
|
||||
await service.openEditor(input, { pinned: true });
|
||||
await service.openEditor(input, { pinned: true }, rightGroup);
|
||||
// Open input
|
||||
return service.openEditor(input, { pinned: true }).then(editor => {
|
||||
return service.openEditor(input, { pinned: true }, rightGroup).then(editor => {
|
||||
const editors = service.editors;
|
||||
assert.equal(editors.length, 2);
|
||||
assert.equal(editors[0], input);
|
||||
assert.equal(editors[1], input);
|
||||
|
||||
const editors = service.editors;
|
||||
assert.equal(editors.length, 2);
|
||||
assert.equal(editors[0], input);
|
||||
assert.equal(editors[1], input);
|
||||
// Close input
|
||||
return rootGroup.closeEditor(input).then(() => {
|
||||
assert.equal(input.isDisposed(), false);
|
||||
|
||||
// Close input
|
||||
await rootGroup.closeEditor(input);
|
||||
assert.equal(input.isDisposed(), false);
|
||||
|
||||
await rightGroup.closeEditor(input);
|
||||
assert.equal(input.isDisposed(), true);
|
||||
return rightGroup.closeEditor(input).then(() => {
|
||||
assert.equal(input.isDisposed(), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('open to the side', async () => {
|
||||
test('open to the side', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart);
|
||||
@@ -393,20 +368,22 @@ class FileServiceProvider extends Disposable {
|
||||
|
||||
const rootGroup = part.activeGroup;
|
||||
|
||||
await part.whenRestored;
|
||||
return part.whenRestored.then(() => {
|
||||
return service.openEditor(input1, { pinned: true }, rootGroup).then(editor => {
|
||||
return service.openEditor(input1, { pinned: true, preserveFocus: true }, SIDE_GROUP).then(editor => {
|
||||
assert.equal(part.activeGroup, rootGroup);
|
||||
assert.equal(part.count, 2);
|
||||
assert.equal(editor!.group, part.groups[1]);
|
||||
|
||||
await service.openEditor(input1, { pinned: true }, rootGroup);
|
||||
let editor = await service.openEditor(input1, { pinned: true, preserveFocus: true }, SIDE_GROUP);
|
||||
|
||||
assert.equal(part.activeGroup, rootGroup);
|
||||
assert.equal(part.count, 2);
|
||||
assert.equal(editor!.group, part.groups[1]);
|
||||
|
||||
// Open to the side uses existing neighbour group if any
|
||||
editor = await service.openEditor(input2, { pinned: true, preserveFocus: true }, SIDE_GROUP);
|
||||
assert.equal(part.activeGroup, rootGroup);
|
||||
assert.equal(part.count, 2);
|
||||
assert.equal(editor!.group, part.groups[1]);
|
||||
// Open to the side uses existing neighbour group if any
|
||||
return service.openEditor(input2, { pinned: true, preserveFocus: true }, SIDE_GROUP).then(editor => {
|
||||
assert.equal(part.activeGroup, rootGroup);
|
||||
assert.equal(part.count, 2);
|
||||
assert.equal(editor!.group, part.groups[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('active editor change / visible editor change events', async function () {
|
||||
|
||||
@@ -255,7 +255,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
* This routine makes the following assumptions:
|
||||
* The root element is an object literal
|
||||
*/
|
||||
private static _replaceNLStrings<T extends object>(nlsConfig: NlsConfiguration, literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string } | null, log: ILog, messageScope: string): void {
|
||||
private static _replaceNLStrings<T>(nlsConfig: NlsConfiguration, literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string } | null, log: ILog, messageScope: string): void {
|
||||
function processEntry(obj: any, key: string | number, command?: boolean) {
|
||||
let value = obj[key];
|
||||
if (types.isString(value)) {
|
||||
|
||||
@@ -1523,10 +1523,6 @@ suite('Disk File Service', () => {
|
||||
});
|
||||
|
||||
test('watch - file - rename file', done => {
|
||||
if (isWindows) {
|
||||
return done(); // watch tests are flaky on other platforms
|
||||
}
|
||||
|
||||
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
|
||||
const toWatchRenamed = URI.file(join(testDir, 'index-watch1-renamed.html'));
|
||||
writeFileSync(toWatch.fsPath, 'Init');
|
||||
|
||||
@@ -958,7 +958,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
getLastActiveFile(filterByScheme: string): URI | undefined {
|
||||
const history = this.getHistory();
|
||||
for (const input of history) {
|
||||
let resource: URI | undefined;
|
||||
let resource: URI | null;
|
||||
if (input instanceof EditorInput) {
|
||||
resource = toResource(input, { filterByScheme });
|
||||
} else {
|
||||
|
||||
@@ -210,7 +210,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
private resolveModelReference(): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
return this.fileService.exists(this.resource)
|
||||
.then(exists => {
|
||||
const EOL = this.configurationService.getValue<{}>('files', { overrideIdentifier: 'json' })['eol'];
|
||||
const EOL = this.configurationService.getValue('files', { overrideIdentifier: 'json' })['eol'];
|
||||
const result: Promise<any> = exists ? Promise.resolve(null) : this.textFileService.write(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' });
|
||||
return result.then(() => this.textModelResolverService.createModelReference(this.resource));
|
||||
});
|
||||
|
||||
@@ -396,7 +396,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
const keybinding = item.keybinding;
|
||||
if (!keybinding) {
|
||||
// This might be a removal keybinding item in user settings => accept it
|
||||
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault);
|
||||
result[resultLen++] = new ResolvedKeybindingItem(null, item.command, item.commandArgs, when, isDefault);
|
||||
} else {
|
||||
const resolvedKeybindings = this.resolveKeybinding(keybinding);
|
||||
for (const resolvedKeybinding of resolvedKeybindings) {
|
||||
@@ -415,7 +415,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
const parts = item.parts;
|
||||
if (parts.length === 0) {
|
||||
// This might be a removal keybinding item in user settings => accept it
|
||||
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault);
|
||||
result[resultLen++] = new ResolvedKeybindingItem(null, item.command, item.commandArgs, when, isDefault);
|
||||
} else {
|
||||
const resolvedKeybindings = this._keyboardMapper.resolveUserBinding(parts);
|
||||
for (const resolvedKeybinding of resolvedKeybindings) {
|
||||
|
||||
@@ -274,7 +274,7 @@ suite('KeybindingsEditing', () => {
|
||||
parts.push(aSimpleKeybinding(chordPart));
|
||||
}
|
||||
}
|
||||
const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined;
|
||||
let keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : null;
|
||||
return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ export class KeybindingsEditorModel extends EditorModel {
|
||||
|
||||
const commandsWithDefaultKeybindings = this.keybindingsService.getDefaultKeybindings().map(keybinding => keybinding.command);
|
||||
for (const command of KeybindingResolver.getAllUnboundCommands(boundCommands)) {
|
||||
const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1);
|
||||
const keybindingItem = new ResolvedKeybindingItem(null, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1);
|
||||
this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, workbenchActionsRegistry, editorActionsLabels));
|
||||
}
|
||||
this._keybindingItems = this._keybindingItemsSortedByPrecedence.slice(0).sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b));
|
||||
|
||||
@@ -31,7 +31,7 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput {
|
||||
constructor(defaultSettingsResource: URI,
|
||||
@ITextModelService textModelResolverService: ITextModelService
|
||||
) {
|
||||
super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, undefined, textModelResolverService);
|
||||
super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, textModelResolverService);
|
||||
}
|
||||
|
||||
getTypeId(): string {
|
||||
|
||||
@@ -617,7 +617,7 @@ suite('KeybindingsEditorModel test', () => {
|
||||
parts.push(aSimpleKeybinding(chordPart));
|
||||
}
|
||||
}
|
||||
const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined;
|
||||
let keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : null;
|
||||
return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IAction, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { IAction, IActionItem } from 'vs/base/common/actions';
|
||||
import { IEditorControl } from 'vs/workbench/common/editor';
|
||||
import { ScopedProgressService, ScopedService } from 'vs/workbench/services/progress/browser/progressService';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
@@ -51,7 +51,7 @@ class TestViewlet implements IViewlet {
|
||||
/**
|
||||
* Returns the action item for a specific action.
|
||||
*/
|
||||
getActionViewItem(action: IAction): IActionViewItem {
|
||||
getActionItem(action: IAction): IActionItem {
|
||||
return null!;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { readdir } from 'vs/base/node/pfs';
|
||||
import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search';
|
||||
import { spawnRipgrepCmd } from './ripgrepFileSearch';
|
||||
import { prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
|
||||
|
||||
interface IDirectoryEntry {
|
||||
base: string;
|
||||
@@ -77,7 +76,7 @@ export class FileWalker {
|
||||
this.errors = [];
|
||||
|
||||
if (this.filePattern) {
|
||||
this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).lowercase;
|
||||
this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
|
||||
}
|
||||
|
||||
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer';
|
||||
import { MAX_FILE_SIZE } from 'vs/base/node/pfs';
|
||||
import { MAX_FILE_SIZE } from 'vs/platform/files/node/fileConstants';
|
||||
import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search';
|
||||
import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
|
||||
import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter';
|
||||
@@ -312,7 +312,7 @@ export class SearchService implements IRawSearchService {
|
||||
|
||||
// Pattern match on results
|
||||
const results: IRawFileMatch[] = [];
|
||||
const normalizedSearchValueLowercase = prepareQuery(searchValue).value;
|
||||
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
|
||||
for (const entry of cachedEntries) {
|
||||
|
||||
// Check if this entry is a match for the search value
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { guessMimeTypes } from 'vs/base/common/mime';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { isUndefinedOrNull, withUndefinedAsNull } from 'vs/base/common/types';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
@@ -18,12 +18,13 @@ import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { RunOnceScheduler, timeout } from 'vs/base/common/async';
|
||||
import { ITextBufferFactory } from 'vs/editor/common/model';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -32,13 +33,6 @@ import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/reso
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export interface IBackupMetaData {
|
||||
mtime: number;
|
||||
size: number;
|
||||
etag: string;
|
||||
orphaned: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
|
||||
*/
|
||||
@@ -63,16 +57,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
private resource: URI;
|
||||
|
||||
private contentEncoding: string; // encoding as reported from disk
|
||||
private preferredEncoding: string; // encoding as chosen by the user
|
||||
|
||||
private preferredMode: string; // mode as chosen by the user
|
||||
private contentEncoding: string; // encoding as reported from disk
|
||||
private preferredEncoding: string; // encoding as chosen by the user
|
||||
|
||||
private versionId: number;
|
||||
private bufferSavedVersionId: number;
|
||||
private blockModelContentChange: boolean;
|
||||
|
||||
private lastResolvedFileStat: IFileStatWithMetadata;
|
||||
private createTextEditorModelPromise: Promise<TextFileEditorModel> | null;
|
||||
|
||||
private lastResolvedDiskStat: IFileStatWithMetadata;
|
||||
|
||||
private autoSaveAfterMillies?: number;
|
||||
private autoSaveAfterMilliesEnabled: boolean;
|
||||
@@ -94,7 +88,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
constructor(
|
||||
resource: URI,
|
||||
preferredEncoding: string,
|
||||
preferredMode: string,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IModeService modeService: IModeService,
|
||||
@IModelService modelService: IModelService,
|
||||
@@ -111,7 +104,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
this.resource = resource;
|
||||
this.preferredEncoding = preferredEncoding;
|
||||
this.preferredMode = preferredMode;
|
||||
this.inOrphanMode = false;
|
||||
this.dirty = false;
|
||||
this.versionId = 0;
|
||||
@@ -207,40 +199,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
private onFilesAssociationChange(): void {
|
||||
if (!this.isResolved()) {
|
||||
if (!this.textEditorModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstLineText = this.getFirstLineText(this.textEditorModel);
|
||||
const languageSelection = this.getOrCreateMode(this.resource, this.modeService, this.preferredMode, firstLineText);
|
||||
const languageSelection = this.getOrCreateMode(this.modeService, undefined, firstLineText);
|
||||
|
||||
this.modelService.setMode(this.textEditorModel, languageSelection);
|
||||
}
|
||||
|
||||
setMode(mode: string): void {
|
||||
super.setMode(mode);
|
||||
|
||||
this.preferredMode = mode;
|
||||
}
|
||||
|
||||
async backup(target = this.resource): Promise<void> {
|
||||
if (this.isResolved()) {
|
||||
|
||||
// Only fill in model metadata if resource matches
|
||||
let meta: IBackupMetaData | undefined = undefined;
|
||||
if (isEqual(target, this.resource) && this.lastResolvedFileStat) {
|
||||
meta = {
|
||||
mtime: this.lastResolvedFileStat.mtime,
|
||||
size: this.lastResolvedFileStat.size,
|
||||
etag: this.lastResolvedFileStat.etag,
|
||||
orphaned: this.inOrphanMode
|
||||
};
|
||||
}
|
||||
|
||||
return this.backupFileService.backupResource<IBackupMetaData>(target, this.createSnapshot(), this.versionId, meta);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
getVersionId(): number {
|
||||
return this.versionId;
|
||||
}
|
||||
|
||||
async revert(soft?: boolean): Promise<void> {
|
||||
@@ -275,7 +245,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
}
|
||||
|
||||
async load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
|
||||
load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
|
||||
this.logService.trace('load() - enter', this.resource);
|
||||
|
||||
// It is very important to not reload the model when the model is dirty.
|
||||
@@ -284,57 +254,44 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
if (this.dirty || this.saveSequentializer.hasPendingSave()) {
|
||||
this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource);
|
||||
|
||||
return this;
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
// Only for new models we support to load from backup
|
||||
if (!this.isResolved()) {
|
||||
const backup = await this.backupFileService.loadBackupResource(this.resource);
|
||||
|
||||
if (this.isResolved()) {
|
||||
return this; // Make sure meanwhile someone else did not suceed in loading
|
||||
}
|
||||
|
||||
if (backup) {
|
||||
try {
|
||||
return await this.loadFromBackup(backup, options);
|
||||
} catch (error) {
|
||||
// ignore error and continue to load as file below
|
||||
}
|
||||
}
|
||||
if (!this.textEditorModel && !this.createTextEditorModelPromise) {
|
||||
return this.loadFromBackup(options);
|
||||
}
|
||||
|
||||
// Otherwise load from file resource
|
||||
return this.loadFromFile(options);
|
||||
}
|
||||
|
||||
private async loadFromBackup(backup: URI, options?: ILoadOptions): Promise<TextFileEditorModel> {
|
||||
private async loadFromBackup(options?: ILoadOptions): Promise<TextFileEditorModel> {
|
||||
const backup = await this.backupFileService.loadBackupResource(this.resource);
|
||||
|
||||
// Resolve actual backup contents
|
||||
const resolvedBackup = await this.backupFileService.resolveBackupContent<IBackupMetaData>(backup);
|
||||
|
||||
if (this.isResolved()) {
|
||||
return this; // Make sure meanwhile someone else did not suceed in loading
|
||||
// Make sure meanwhile someone else did not suceed or start loading
|
||||
if (this.createTextEditorModelPromise || this.textEditorModel) {
|
||||
return this.createTextEditorModelPromise || this;
|
||||
}
|
||||
|
||||
// Load with backup
|
||||
this.loadFromContent({
|
||||
resource: this.resource,
|
||||
name: basename(this.resource),
|
||||
mtime: resolvedBackup.meta ? resolvedBackup.meta.mtime : Date.now(),
|
||||
size: resolvedBackup.meta ? resolvedBackup.meta.size : 0,
|
||||
etag: resolvedBackup.meta ? resolvedBackup.meta.etag : ETAG_DISABLED, // etag disabled if unknown!
|
||||
value: resolvedBackup.value,
|
||||
encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
|
||||
isReadonly: false
|
||||
}, options, true /* from backup */);
|
||||
// If we have a backup, continue loading with it
|
||||
if (!!backup) {
|
||||
const content: ITextFileStreamContent = {
|
||||
resource: this.resource,
|
||||
name: basename(this.resource),
|
||||
mtime: Date.now(),
|
||||
size: 0,
|
||||
etag: ETAG_DISABLED, // always allow to save content restored from a backup (see https://github.com/Microsoft/vscode/issues/72343)
|
||||
value: createTextBufferFactory(''), // will be filled later from backup
|
||||
encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
|
||||
isReadonly: false
|
||||
};
|
||||
|
||||
// Restore orphaned flag based on state
|
||||
if (resolvedBackup.meta && resolvedBackup.meta.orphaned) {
|
||||
this.setOrphaned(true);
|
||||
return this.loadWithContent(content, options, backup);
|
||||
}
|
||||
|
||||
return this;
|
||||
// Otherwise load from file
|
||||
return this.loadFromFile(options);
|
||||
}
|
||||
|
||||
private async loadFromFile(options?: ILoadOptions): Promise<TextFileEditorModel> {
|
||||
@@ -345,8 +302,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
let etag: string | undefined;
|
||||
if (forceReadFromDisk) {
|
||||
etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk
|
||||
} else if (this.lastResolvedFileStat) {
|
||||
etag = this.lastResolvedFileStat.etag; // otherwise respect etag to support caching
|
||||
} else if (this.lastResolvedDiskStat) {
|
||||
etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
|
||||
}
|
||||
|
||||
// Ensure to track the versionId before doing a long running operation
|
||||
@@ -364,11 +321,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// Clear orphaned state when loading was successful
|
||||
this.setOrphaned(false);
|
||||
|
||||
if (currentVersionId !== this.versionId) {
|
||||
return this; // Make sure meanwhile someone else did not suceed loading
|
||||
// Guard against the model having changed in the meantime
|
||||
if (currentVersionId === this.versionId) {
|
||||
return this.loadWithContent(content, options);
|
||||
}
|
||||
|
||||
return this.loadFromContent(content, options);
|
||||
return this;
|
||||
} catch (error) {
|
||||
const result = error.fileOperationResult;
|
||||
|
||||
@@ -398,11 +356,37 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
}
|
||||
|
||||
private loadFromContent(content: ITextFileStreamContent, options?: ILoadOptions, fromBackup?: boolean): TextFileEditorModel {
|
||||
private async loadWithContent(content: ITextFileStreamContent, options?: ILoadOptions, backup?: URI): Promise<TextFileEditorModel> {
|
||||
const model = await this.doLoadWithContent(content, backup);
|
||||
|
||||
// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
|
||||
const settingsType = this.getTypeIfSettings();
|
||||
if (settingsType) {
|
||||
/* __GDPR__
|
||||
"settingsRead" : {
|
||||
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
|
||||
} else {
|
||||
/* __GDPR__
|
||||
"fileGet" : {
|
||||
"${include}": [
|
||||
"${FileTelemetryData}"
|
||||
]
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private doLoadWithContent(content: ITextFileStreamContent, backup?: URI): Promise<TextFileEditorModel> {
|
||||
this.logService.trace('load() - resolved content', this.resource);
|
||||
|
||||
// Update our resolved disk stat model
|
||||
this.updateLastResolvedFileStat({
|
||||
this.updateLastResolvedDiskStat({
|
||||
resource: this.resource,
|
||||
name: content.name,
|
||||
mtime: content.mtime,
|
||||
@@ -425,61 +409,21 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
// Update Existing Model
|
||||
if (this.isResolved()) {
|
||||
if (this.textEditorModel) {
|
||||
this.doUpdateTextModel(content.value);
|
||||
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
// Join an existing request to create the editor model to avoid race conditions
|
||||
else if (this.createTextEditorModelPromise) {
|
||||
this.logService.trace('load() - join existing text editor model promise', this.resource);
|
||||
|
||||
return this.createTextEditorModelPromise;
|
||||
}
|
||||
|
||||
// Create New Model
|
||||
else {
|
||||
this.doCreateTextModel(content.resource, content.value, !!fromBackup);
|
||||
}
|
||||
|
||||
// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
|
||||
const settingsType = this.getTypeIfSettings();
|
||||
if (settingsType) {
|
||||
/* __GDPR__
|
||||
"settingsRead" : {
|
||||
"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
|
||||
} else {
|
||||
/* __GDPR__
|
||||
"fileGet" : {
|
||||
"${include}": [
|
||||
"${FileTelemetryData}"
|
||||
]
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void {
|
||||
this.logService.trace('load() - created text editor model', this.resource);
|
||||
|
||||
// Create model
|
||||
this.createTextEditorModel(value, resource, this.preferredMode);
|
||||
|
||||
// We restored a backup so we have to set the model as being dirty
|
||||
// We also want to trigger auto save if it is enabled to simulate the exact same behaviour
|
||||
// you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977)
|
||||
if (fromBackup) {
|
||||
this.makeDirty();
|
||||
if (this.autoSaveAfterMilliesEnabled) {
|
||||
this.doAutoSave(this.versionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we are not tracking a stale state
|
||||
else {
|
||||
this.setDirty(false);
|
||||
}
|
||||
|
||||
// Model Listeners
|
||||
this.installModelListeners();
|
||||
return this.doCreateTextModel(content.resource, content.value, backup);
|
||||
}
|
||||
|
||||
private doUpdateTextModel(value: ITextBufferFactory): void {
|
||||
@@ -491,7 +435,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// Update model value in a block that ignores model content change events
|
||||
this.blockModelContentChange = true;
|
||||
try {
|
||||
this.updateTextEditorModel(value, this.preferredMode);
|
||||
this.updateTextEditorModel(value);
|
||||
} finally {
|
||||
this.blockModelContentChange = false;
|
||||
}
|
||||
@@ -500,6 +444,44 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
this.updateSavedVersionId();
|
||||
}
|
||||
|
||||
private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI | undefined): Promise<TextFileEditorModel> {
|
||||
this.logService.trace('load() - created text editor model', this.resource);
|
||||
|
||||
this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
|
||||
this.createTextEditorModelPromise = null;
|
||||
|
||||
// Create model
|
||||
const hasBackupContent = !!backupContent;
|
||||
this.createTextEditorModel(backupContent ? backupContent : value, resource);
|
||||
|
||||
// We restored a backup so we have to set the model as being dirty
|
||||
// We also want to trigger auto save if it is enabled to simulate the exact same behaviour
|
||||
// you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977)
|
||||
if (hasBackupContent) {
|
||||
this.makeDirty();
|
||||
if (this.autoSaveAfterMilliesEnabled) {
|
||||
this.doAutoSave(this.versionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we are not tracking a stale state
|
||||
else {
|
||||
this.setDirty(false);
|
||||
}
|
||||
|
||||
// Model Listeners
|
||||
this.installModelListeners();
|
||||
|
||||
return this;
|
||||
}, error => {
|
||||
this.createTextEditorModelPromise = null;
|
||||
|
||||
return Promise.reject<TextFileEditorModel>(error);
|
||||
});
|
||||
|
||||
return this.createTextEditorModelPromise;
|
||||
}
|
||||
|
||||
private installModelListeners(): void {
|
||||
|
||||
// See https://github.com/Microsoft/vscode/issues/30189
|
||||
@@ -507,11 +489,27 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// where `value` was captured in the content change listener closure scope.
|
||||
|
||||
// Content Change
|
||||
if (this.isResolved()) {
|
||||
if (this.textEditorModel) {
|
||||
this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
|
||||
}
|
||||
}
|
||||
|
||||
private async doLoadBackup(backup: URI | undefined): Promise<ITextBufferFactory | null> {
|
||||
if (!backup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return withUndefinedAsNull(await this.backupFileService.resolveBackupContent(backup));
|
||||
} catch (error) {
|
||||
return null; // ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
protected getOrCreateMode(modeService: IModeService, preferredModeIds: string | undefined, firstLineText?: string): ILanguageSelection {
|
||||
return modeService.createByFilepathOrFirstLine(this.resource.fsPath, firstLineText);
|
||||
}
|
||||
|
||||
private onModelContentChanged(): void {
|
||||
this.logService.trace(`onModelContentChanged() - enter`, this.resource);
|
||||
|
||||
@@ -528,7 +526,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
|
||||
// Note: we currently only do this check when auto-save is turned off because there you see
|
||||
// a dirty indicator that you want to get rid of when undoing to the saved version.
|
||||
if (!this.autoSaveAfterMilliesEnabled && this.isResolved() && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
|
||||
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
|
||||
this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource);
|
||||
|
||||
// Clear flags
|
||||
@@ -659,7 +657,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
|
||||
// Push all edit operations to the undo stack so that the user has a chance to
|
||||
// Ctrl+Z back to the saved version. We only do this when auto-save is turned off
|
||||
if (!this.autoSaveAfterMilliesEnabled && this.isResolved()) {
|
||||
if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel) {
|
||||
this.textEditorModel.pushStackElement();
|
||||
}
|
||||
|
||||
@@ -689,12 +687,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// saving contents to disk that are stale (see https://github.com/Microsoft/vscode/issues/50942).
|
||||
// To fix this issue, we will not store the contents to disk when we got disposed.
|
||||
if (this.disposed) {
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
}
|
||||
|
||||
// We require a resolved model from this point on, since we are about to write data to disk.
|
||||
if (!this.isResolved()) {
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Under certain conditions we do a short-cut of flushing contents to disk when we can assume that
|
||||
@@ -720,12 +713,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// Save to Disk
|
||||
// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
|
||||
this.logService.trace(`doSave(${versionId}) - before write()`, this.resource);
|
||||
return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedFileStat.resource, this.createSnapshot(), {
|
||||
const snapshot = this.createSnapshot();
|
||||
if (!snapshot) {
|
||||
throw new Error('Invalid snapshot');
|
||||
}
|
||||
return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, {
|
||||
overwriteReadonly: options.overwriteReadonly,
|
||||
overwriteEncoding: options.overwriteEncoding,
|
||||
mtime: this.lastResolvedFileStat.mtime,
|
||||
mtime: this.lastResolvedDiskStat.mtime,
|
||||
encoding: this.getEncoding(),
|
||||
etag: this.lastResolvedFileStat.etag,
|
||||
etag: this.lastResolvedDiskStat.etag,
|
||||
writeElevated: options.writeElevated
|
||||
}).then(stat => {
|
||||
this.logService.trace(`doSave(${versionId}) - after write()`, this.resource);
|
||||
@@ -739,7 +736,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
// Updated resolved stat with updated stat
|
||||
this.updateLastResolvedFileStat(stat);
|
||||
this.updateLastResolvedDiskStat(stat);
|
||||
|
||||
// Cancel any content change event promises as they are no longer valid
|
||||
this.contentChangeEventScheduler.cancel();
|
||||
@@ -855,22 +852,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
private doTouch(versionId: number): Promise<void> {
|
||||
if (!this.isResolved()) {
|
||||
return Promise.resolve();
|
||||
const snapshot = this.createSnapshot();
|
||||
if (!snapshot) {
|
||||
throw new Error('invalid snapshot');
|
||||
}
|
||||
|
||||
return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedFileStat.resource, this.createSnapshot(), {
|
||||
mtime: this.lastResolvedFileStat.mtime,
|
||||
return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, {
|
||||
mtime: this.lastResolvedDiskStat.mtime,
|
||||
encoding: this.getEncoding(),
|
||||
etag: this.lastResolvedFileStat.etag
|
||||
etag: this.lastResolvedDiskStat.etag
|
||||
}).then(stat => {
|
||||
|
||||
// Updated resolved stat with updated stat since touching it might have changed mtime
|
||||
this.updateLastResolvedFileStat(stat);
|
||||
|
||||
// Emit File Saved Event
|
||||
this._onDidStateChange.fire(StateChange.SAVED);
|
||||
|
||||
this.updateLastResolvedDiskStat(stat);
|
||||
}, error => onUnexpectedError(error) /* just log any error but do not notify the user since the file was not dirty */));
|
||||
}
|
||||
|
||||
@@ -904,23 +898,23 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
// in order to find out if the model changed back to a saved version (e.g.
|
||||
// when undoing long enough to reach to a version that is saved and then to
|
||||
// clear the dirty flag)
|
||||
if (this.isResolved()) {
|
||||
if (this.textEditorModel) {
|
||||
this.bufferSavedVersionId = this.textEditorModel.getAlternativeVersionId();
|
||||
}
|
||||
}
|
||||
|
||||
private updateLastResolvedFileStat(newFileStat: IFileStatWithMetadata): void {
|
||||
private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStatWithMetadata): void {
|
||||
|
||||
// First resolve - just take
|
||||
if (!this.lastResolvedFileStat) {
|
||||
this.lastResolvedFileStat = newFileStat;
|
||||
if (!this.lastResolvedDiskStat) {
|
||||
this.lastResolvedDiskStat = newVersionOnDiskStat;
|
||||
}
|
||||
|
||||
// Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced.
|
||||
// This prevents race conditions from loading and saving. If a save comes in late after a revert
|
||||
// was called, the mtime could be out of sync.
|
||||
else if (this.lastResolvedFileStat.mtime <= newFileStat.mtime) {
|
||||
this.lastResolvedFileStat = newFileStat;
|
||||
else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
|
||||
this.lastResolvedDiskStat = newVersionOnDiskStat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,6 +937,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
return this.lastSaveAttemptTime;
|
||||
}
|
||||
|
||||
getETag(): string | null {
|
||||
return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag || null : null;
|
||||
}
|
||||
|
||||
hasState(state: ModelState): boolean {
|
||||
switch (state) {
|
||||
case ModelState.CONFLICT:
|
||||
@@ -1024,12 +1022,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
return true;
|
||||
}
|
||||
|
||||
isResolved(): boolean { // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
return !!this.textEditorModel;
|
||||
isResolved(): boolean {
|
||||
return !isUndefinedOrNull(this.lastResolvedDiskStat);
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return !!(this.lastResolvedFileStat && this.lastResolvedFileStat.isReadonly);
|
||||
return !!(this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly);
|
||||
}
|
||||
|
||||
isDisposed(): boolean {
|
||||
@@ -1041,7 +1039,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
}
|
||||
|
||||
getStat(): IFileStatWithMetadata {
|
||||
return this.lastResolvedFileStat;
|
||||
return this.lastResolvedDiskStat;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -1050,6 +1048,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
||||
this.inOrphanMode = false;
|
||||
this.inErrorMode = false;
|
||||
|
||||
this.createTextEditorModelPromise = null;
|
||||
|
||||
this.cancelPendingAutoSave();
|
||||
|
||||
super.dispose();
|
||||
|
||||
@@ -153,7 +153,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
|
||||
|
||||
// Model does not exist
|
||||
else {
|
||||
const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined, options ? options.mode : undefined);
|
||||
const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined);
|
||||
modelPromise = model.load(options);
|
||||
|
||||
// Install state change listener
|
||||
@@ -204,11 +204,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
|
||||
// Remove from pending loads
|
||||
this.mapResourceToPendingModelLoaders.delete(resource);
|
||||
|
||||
// Apply mode if provided
|
||||
if (options && options.mode) {
|
||||
resolvedModel.setMode(options.mode);
|
||||
}
|
||||
|
||||
return resolvedModel;
|
||||
} catch (error) {
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import { trim } from 'vs/base/common/strings';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
|
||||
/**
|
||||
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
|
||||
@@ -239,44 +238,59 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise<void> {
|
||||
|
||||
// Handle file resources first
|
||||
await Promise.all(dirtyFileModels.map(model => model.backup()));
|
||||
await Promise.all(dirtyFileModels.map(async model => {
|
||||
const snapshot = model.createSnapshot();
|
||||
if (snapshot) {
|
||||
await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
|
||||
}
|
||||
}));
|
||||
|
||||
// Handle untitled resources
|
||||
await Promise.all(untitledResources
|
||||
const untitledModelPromises = untitledResources
|
||||
.filter(untitled => this.untitledEditorService.exists(untitled))
|
||||
.map(async untitled => (await this.untitledEditorService.loadOrCreate({ resource: untitled })).backup()));
|
||||
.map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled }));
|
||||
|
||||
const untitledModels = await Promise.all(untitledModelPromises);
|
||||
|
||||
await Promise.all(untitledModels.map(async model => {
|
||||
const snapshot = model.createSnapshot();
|
||||
if (snapshot) {
|
||||
await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private async confirmBeforeShutdown(): Promise<boolean> {
|
||||
const confirm = await this.confirmSave();
|
||||
private confirmBeforeShutdown(): boolean | Promise<boolean> {
|
||||
return this.confirmSave().then(confirm => {
|
||||
|
||||
// Save
|
||||
if (confirm === ConfirmResult.SAVE) {
|
||||
const result = await this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true });
|
||||
// Save
|
||||
if (confirm === ConfirmResult.SAVE) {
|
||||
return this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true }).then(result => {
|
||||
if (result.results.some(r => !r.success)) {
|
||||
return true; // veto if some saves failed
|
||||
}
|
||||
|
||||
if (result.results.some(r => !r.success)) {
|
||||
return true; // veto if some saves failed
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
});
|
||||
}
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
// Don't Save
|
||||
else if (confirm === ConfirmResult.DONT_SAVE) {
|
||||
|
||||
// Don't Save
|
||||
else if (confirm === ConfirmResult.DONT_SAVE) {
|
||||
// Make sure to revert untitled so that they do not restore
|
||||
// see https://github.com/Microsoft/vscode/issues/29572
|
||||
this.untitledEditorService.revertAll();
|
||||
|
||||
// Make sure to revert untitled so that they do not restore
|
||||
// see https://github.com/Microsoft/vscode/issues/29572
|
||||
this.untitledEditorService.revertAll();
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
|
||||
return this.noVeto({ cleanUpBackups: true });
|
||||
}
|
||||
// Cancel
|
||||
else if (confirm === ConfirmResult.CANCEL) {
|
||||
return true; // veto
|
||||
}
|
||||
|
||||
// Cancel
|
||||
else if (confirm === ConfirmResult.CANCEL) {
|
||||
return true; // veto
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise<boolean> {
|
||||
@@ -489,7 +503,10 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
dirtyTargetModelUris.push(targetModelResource);
|
||||
|
||||
// Backup dirty source model to the target resource it will become later
|
||||
await sourceModel.backup(targetModelResource);
|
||||
const snapshot = sourceModel.createSnapshot();
|
||||
if (snapshot) {
|
||||
await this.backupFileService.backupResource(targetModelResource, snapshot, sourceModel.getVersionId());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -740,14 +757,14 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] {
|
||||
if (Array.isArray(arg1)) {
|
||||
const models: ITextFileEditorModel[] = [];
|
||||
arg1.forEach(resource => {
|
||||
(<URI[]>arg1).forEach(resource => {
|
||||
models.push(...this.getFileModels(resource));
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
return this._models.getAll(arg1);
|
||||
return this._models.getAll(<URI>arg1);
|
||||
}
|
||||
|
||||
private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] {
|
||||
@@ -856,12 +873,17 @@ export abstract class TextFileService extends Disposable implements ITextFileSer
|
||||
|
||||
// take over encoding, mode and model value from source model
|
||||
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
|
||||
if (sourceModel.isResolved() && targetModel.isResolved()) {
|
||||
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
|
||||
if (targetModel.textEditorModel) {
|
||||
const snapshot = sourceModel.createSnapshot();
|
||||
if (snapshot) {
|
||||
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot));
|
||||
}
|
||||
|
||||
const mode = sourceModel.textEditorModel.getLanguageIdentifier();
|
||||
if (mode.language !== PLAINTEXT_MODE_ID) {
|
||||
targetModel.textEditorModel.setMode(mode); // only use if more specific than plain/text
|
||||
if (sourceModel.textEditorModel) {
|
||||
const language = sourceModel.textEditorModel.getLanguageIdentifier();
|
||||
if (language.id > 1) {
|
||||
targetModel.textEditorModel.setMode(language); // only use if more specific than plain/text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IEncodingSupport, ConfirmResult, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor';
|
||||
import { IEncodingSupport, ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
@@ -367,11 +367,6 @@ export interface IModelLoadOrCreateOptions {
|
||||
*/
|
||||
reason?: LoadReason;
|
||||
|
||||
/**
|
||||
* The language mode to use for the model text content.
|
||||
*/
|
||||
mode?: string;
|
||||
|
||||
/**
|
||||
* The encoding to use when resolving the model text content.
|
||||
*/
|
||||
@@ -448,15 +443,19 @@ export interface ILoadOptions {
|
||||
reason?: LoadReason;
|
||||
}
|
||||
|
||||
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport {
|
||||
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport {
|
||||
|
||||
readonly onDidContentChange: Event<StateChange>;
|
||||
readonly onDidStateChange: Event<StateChange>;
|
||||
|
||||
getVersionId(): number;
|
||||
|
||||
getResource(): URI;
|
||||
|
||||
hasState(state: ModelState): boolean;
|
||||
|
||||
getETag(): string | null;
|
||||
|
||||
updatePreferredEncoding(encoding: string): void;
|
||||
|
||||
save(options?: ISaveOptions): Promise<void>;
|
||||
@@ -465,17 +464,16 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport
|
||||
|
||||
revert(soft?: boolean): Promise<void>;
|
||||
|
||||
backup(target?: URI): Promise<void>;
|
||||
createSnapshot(): ITextSnapshot | null;
|
||||
|
||||
isDirty(): boolean;
|
||||
|
||||
isResolved(): this is IResolvedTextFileEditorModel;
|
||||
isResolved(): boolean;
|
||||
|
||||
isDisposed(): boolean;
|
||||
}
|
||||
|
||||
export interface IResolvedTextFileEditorModel extends ITextFileEditorModel {
|
||||
|
||||
readonly textEditorModel: ITextModel;
|
||||
|
||||
createSnapshot(): ITextSnapshot;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileStatWithMetadata, ICreateFileOptions, FileOperationError, FileOperationResult, IFileStreamContent, IFileService } from 'vs/platform/files/common/files';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { exists, stat, chmod, rimraf, MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs';
|
||||
import { exists, stat, chmod, rimraf } from 'vs/base/node/pfs';
|
||||
import { join, dirname } from 'vs/base/common/path';
|
||||
import { isMacintosh, isLinux } from 'vs/base/common/platform';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
@@ -26,6 +26,7 @@ import { VSBufferReadable, VSBuffer, VSBufferReadableStream } from 'vs/base/comm
|
||||
import { Readable } from 'stream';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
|
||||
import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/fileConstants';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
|
||||
export class NodeTextFileService extends TextFileService {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiatio
|
||||
|
||||
export class TextResourcePropertiesService implements ITextResourcePropertiesService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<ITextResourcePropertiesService>;
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
private remoteEnvironment: IRemoteAgentEnvironment | null = null;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/commo
|
||||
import { FileOperationResult, FileOperationError, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
|
||||
|
||||
class ServiceAccessor {
|
||||
constructor(@ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService, @IFileService public fileService: TestFileService) {
|
||||
@@ -45,53 +44,25 @@ suite('Files - TextFileEditorModel', () => {
|
||||
accessor.fileService.setContent(content);
|
||||
});
|
||||
|
||||
test('save', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
test('Save', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
await model.load();
|
||||
|
||||
model.textEditorModel!.setValue('bar');
|
||||
assert.ok(getLastModifiedTime(model) <= Date.now());
|
||||
|
||||
let savedEvent = false;
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.SAVED) {
|
||||
savedEvent = true;
|
||||
}
|
||||
});
|
||||
|
||||
await model.save();
|
||||
|
||||
assert.ok(model.getLastSaveAttemptTime() <= Date.now());
|
||||
assert.ok(!model.isDirty());
|
||||
assert.ok(savedEvent);
|
||||
|
||||
model.dispose();
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
});
|
||||
|
||||
test('save - touching also emits saved event', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
|
||||
await model.load();
|
||||
|
||||
let savedEvent = false;
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.SAVED) {
|
||||
savedEvent = true;
|
||||
}
|
||||
});
|
||||
|
||||
await model.save({ force: true });
|
||||
|
||||
assert.ok(savedEvent);
|
||||
|
||||
model.dispose();
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
});
|
||||
|
||||
test('setEncoding - encode', function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.setEncoding('utf8', EncodingMode.Encode); // no-op
|
||||
assert.equal(getLastModifiedTime(model), -1);
|
||||
@@ -104,7 +75,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('setEncoding - decode', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.setEncoding('utf16', EncodingMode.Decode);
|
||||
|
||||
@@ -113,24 +84,8 @@ suite('Files - TextFileEditorModel', () => {
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('create with mode', async function () {
|
||||
const mode = 'text-file-model-test';
|
||||
ModesRegistry.registerLanguage({
|
||||
id: mode,
|
||||
});
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', mode);
|
||||
|
||||
await model.load();
|
||||
|
||||
assert.equal(model.textEditorModel!.getModeId(), mode);
|
||||
|
||||
model.dispose();
|
||||
assert.ok(!accessor.modelService.getModel(model.getResource()));
|
||||
});
|
||||
|
||||
test('disposes when underlying model is destroyed', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
await model.load();
|
||||
|
||||
@@ -139,7 +94,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('Load does not trigger save', async function () {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined);
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8');
|
||||
assert.ok(model.hasState(ModelState.SAVED));
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
@@ -153,7 +108,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('Load returns dirty model as long as model is dirty', async function () {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
await model.load();
|
||||
model.textEditorModel!.setValue('foo');
|
||||
@@ -168,7 +123,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
test('Revert', async function () {
|
||||
let eventCounter = 0;
|
||||
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.REVERTED) {
|
||||
@@ -190,7 +145,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
test('Revert (soft)', async function () {
|
||||
let eventCounter = 0;
|
||||
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.REVERTED) {
|
||||
@@ -210,7 +165,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('Load and undo turns model dirty', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
await model.load();
|
||||
accessor.fileService.setContent('Hello Change');
|
||||
|
||||
@@ -220,7 +175,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('File not modified error is handled gracefully', async function () {
|
||||
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
await model.load();
|
||||
|
||||
@@ -235,7 +190,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('Load error is handled gracefully if model already exists', async function () {
|
||||
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
await model.load();
|
||||
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND));
|
||||
@@ -281,7 +236,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
|
||||
test('Save Participant', async function () {
|
||||
let eventCounter = 0;
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
model.onDidStateChange(e => {
|
||||
if (e === StateChange.SAVED) {
|
||||
@@ -311,7 +266,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
|
||||
test('Save Participant, async participant', async function () {
|
||||
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
TextFileEditorModel.setSaveParticipant({
|
||||
participate: (model) => {
|
||||
@@ -329,7 +284,7 @@ suite('Files - TextFileEditorModel', () => {
|
||||
});
|
||||
|
||||
test('Save Participant, bad participant', async function () {
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
|
||||
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
|
||||
|
||||
TextFileEditorModel.setSaveParticipant({
|
||||
participate: (model) => {
|
||||
|
||||
@@ -13,7 +13,6 @@ import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/file
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { toResource } from 'vs/base/test/common/utils';
|
||||
import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
|
||||
export class TestTextFileEditorModelManager extends TextFileEditorModelManager {
|
||||
|
||||
@@ -43,9 +42,9 @@ suite('Files - TextFileEditorModelManager', () => {
|
||||
test('add, remove, clear, get, getAll', function () {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined);
|
||||
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined);
|
||||
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined);
|
||||
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8');
|
||||
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8');
|
||||
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8');
|
||||
|
||||
manager.add(URI.file('/test.html'), model1);
|
||||
manager.add(URI.file('/some/other.html'), model2);
|
||||
@@ -118,9 +117,9 @@ suite('Files - TextFileEditorModelManager', () => {
|
||||
test('removed from cache when model disposed', function () {
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined);
|
||||
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined);
|
||||
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8', undefined);
|
||||
const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8');
|
||||
const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8');
|
||||
const model3: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random3.txt'), 'utf8');
|
||||
|
||||
manager.add(URI.file('/test.html'), model1);
|
||||
manager.add(URI.file('/some/other.html'), model2);
|
||||
@@ -291,24 +290,4 @@ suite('Files - TextFileEditorModelManager', () => {
|
||||
assert.ok(model.isDisposed());
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
test('mode', async function () {
|
||||
const mode = 'text-file-model-manager-test';
|
||||
ModesRegistry.registerLanguage({
|
||||
id: mode,
|
||||
});
|
||||
|
||||
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
|
||||
|
||||
const resource = toResource.call(this, '/path/index_something.txt');
|
||||
|
||||
let model = await manager.loadOrCreate(resource, { mode });
|
||||
assert.equal(model.textEditorModel!.getModeId(), mode);
|
||||
|
||||
model = await manager.loadOrCreate(resource, { mode: 'text' });
|
||||
assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID);
|
||||
|
||||
manager.disposeModel((model as TextFileEditorModel));
|
||||
manager.dispose();
|
||||
});
|
||||
});
|
||||
@@ -65,8 +65,8 @@ suite('Files - TextFileService', () => {
|
||||
accessor.untitledEditorService.revertAll();
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - no veto', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
test('confirm onWillShutdown - no veto', function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const event = new BeforeShutdownEventImpl();
|
||||
@@ -76,12 +76,14 @@ suite('Files - TextFileService', () => {
|
||||
if (typeof veto === 'boolean') {
|
||||
assert.ok(!veto);
|
||||
} else {
|
||||
assert.ok(!(await veto));
|
||||
veto.then(veto => {
|
||||
assert.ok(!veto);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - veto if user cancels', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -97,7 +99,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -123,7 +125,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('confirm onWillShutdown - save (hot.exit: off)', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -142,7 +144,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('isDirty/getDirty - files and untitled', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -169,7 +171,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('save - file', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -185,11 +187,11 @@ suite('Files - TextFileService', () => {
|
||||
|
||||
test('save - UNC path', async function () {
|
||||
const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' });
|
||||
model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const mockedFileUri = untitledUncUri.with({ scheme: Schemas.file });
|
||||
const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8', undefined);
|
||||
const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8');
|
||||
const loadOrCreateStub = sinon.stub(accessor.textFileService.models, 'loadOrCreate', () => Promise.resolve(mockedEditorInput));
|
||||
|
||||
sinon.stub(accessor.untitledEditorService, 'exists', () => true);
|
||||
@@ -209,7 +211,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('saveAll - file', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -226,7 +228,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('saveAs - file', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -242,7 +244,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('revert - file', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -258,7 +260,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('delete - dirty file', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
@@ -272,8 +274,8 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
test('move - dirty file', async function () {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8', undefined);
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(sourceModel.getResource(), sourceModel);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(targetModel.getResource(), targetModel);
|
||||
|
||||
@@ -395,7 +397,7 @@ suite('Files - TextFileService', () => {
|
||||
});
|
||||
|
||||
async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean): Promise<void> {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
const service = accessor.textFileService;
|
||||
|
||||
@@ -53,7 +53,7 @@ suite('Workbench - TextModelResolverService', () => {
|
||||
accessor.untitledEditorService.revertAll();
|
||||
});
|
||||
|
||||
test('resolve resource', async () => {
|
||||
test('resolve resource', function () {
|
||||
const dispose = accessor.textModelResolverService.registerTextModelContentProvider('test', {
|
||||
provideTextContent: function (resource: URI): Promise<ITextModel> {
|
||||
if (resource.scheme === 'test') {
|
||||
@@ -67,60 +67,67 @@ suite('Workbench - TextModelResolverService', () => {
|
||||
});
|
||||
|
||||
let resource = URI.from({ scheme: 'test', authority: null!, path: 'thePath' });
|
||||
let input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource, undefined);
|
||||
let input: ResourceEditorInput = instantiationService.createInstance(ResourceEditorInput, 'The Name', 'The Description', resource);
|
||||
|
||||
const model = await input.resolve();
|
||||
assert.ok(model);
|
||||
assert.equal(snapshotToString(((model as ResourceEditorModel).createSnapshot()!)), 'Hello Test');
|
||||
let disposed = false;
|
||||
let disposedPromise = new Promise(resolve => {
|
||||
Event.once(model.onDispose)(() => {
|
||||
disposed = true;
|
||||
resolve();
|
||||
return input.resolve().then(async model => {
|
||||
assert.ok(model);
|
||||
assert.equal(snapshotToString((model as ResourceEditorModel).createSnapshot()!), 'Hello Test');
|
||||
|
||||
let disposed = false;
|
||||
let disposedPromise = new Promise(resolve => {
|
||||
Event.once(model.onDispose)(() => {
|
||||
disposed = true;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
input.dispose();
|
||||
await disposedPromise;
|
||||
assert.equal(disposed, true);
|
||||
|
||||
dispose.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('resolve file', function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8');
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
|
||||
|
||||
return model.load().then(() => {
|
||||
return accessor.textModelResolverService.createModelReference(model.getResource()).then(ref => {
|
||||
const model = ref.object;
|
||||
const editorModel = model.textEditorModel;
|
||||
|
||||
assert.ok(editorModel);
|
||||
assert.equal(editorModel.getValue(), 'Hello Html');
|
||||
|
||||
let disposed = false;
|
||||
Event.once(model.onDispose)(() => {
|
||||
disposed = true;
|
||||
});
|
||||
|
||||
ref.dispose();
|
||||
return timeout(0).then(() => { // due to the reference resolving the model first which is async
|
||||
assert.equal(disposed, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
input.dispose();
|
||||
|
||||
await disposedPromise;
|
||||
assert.equal(disposed, true);
|
||||
dispose.dispose();
|
||||
});
|
||||
|
||||
test('resolve file', async function () {
|
||||
const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.models).add(textModel.getResource(), textModel);
|
||||
|
||||
await textModel.load();
|
||||
|
||||
const ref = await accessor.textModelResolverService.createModelReference(textModel.getResource());
|
||||
|
||||
const model = ref.object;
|
||||
const editorModel = model.textEditorModel;
|
||||
|
||||
assert.ok(editorModel);
|
||||
assert.equal(editorModel.getValue(), 'Hello Html');
|
||||
|
||||
let disposed = false;
|
||||
Event.once(model.onDispose)(() => {
|
||||
disposed = true;
|
||||
});
|
||||
|
||||
ref.dispose();
|
||||
await timeout(0); // due to the reference resolving the model first which is async
|
||||
assert.equal(disposed, true);
|
||||
});
|
||||
|
||||
test('resolve untitled', async () => {
|
||||
test('resolve untitled', function () {
|
||||
const service = accessor.untitledEditorService;
|
||||
const input = service.createOrGet();
|
||||
|
||||
await input.resolve();
|
||||
const ref = await accessor.textModelResolverService.createModelReference(input.getResource());
|
||||
const model = ref.object;
|
||||
const editorModel = model.textEditorModel;
|
||||
assert.ok(editorModel);
|
||||
ref.dispose();
|
||||
input.dispose();
|
||||
return input.resolve().then(() => {
|
||||
return accessor.textModelResolverService.createModelReference(input.getResource()).then(ref => {
|
||||
const model = ref.object;
|
||||
const editorModel = model.textEditorModel;
|
||||
|
||||
assert.ok(editorModel);
|
||||
ref.dispose();
|
||||
|
||||
input.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('even loading documents should be refcounted', async () => {
|
||||
@@ -128,12 +135,12 @@ suite('Workbench - TextModelResolverService', () => {
|
||||
let waitForIt = new Promise(c => resolveModel = c);
|
||||
|
||||
const disposable = accessor.textModelResolverService.registerTextModelContentProvider('test', {
|
||||
provideTextContent: async (resource: URI): Promise<ITextModel> => {
|
||||
await waitForIt;
|
||||
|
||||
let modelContent = 'Hello Test';
|
||||
let languageSelection = accessor.modeService.create('json');
|
||||
return accessor.modelService.createModel(modelContent, languageSelection, resource);
|
||||
provideTextContent: (resource: URI): Promise<ITextModel> => {
|
||||
return waitForIt.then(_ => {
|
||||
let modelContent = 'Hello Test';
|
||||
let languageSelection = accessor.modeService.create('json');
|
||||
return accessor.modelService.createModel(modelContent, languageSelection, resource);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IUntitledEditorService = createDecorator<IUntitledEditorService>('u
|
||||
|
||||
export interface IModelLoadOrCreateOptions {
|
||||
resource?: URI;
|
||||
mode?: string;
|
||||
modeId?: string;
|
||||
initialValue?: string;
|
||||
encoding?: string;
|
||||
useResourcePath?: boolean;
|
||||
@@ -29,7 +29,7 @@ export interface IModelLoadOrCreateOptions {
|
||||
|
||||
export interface IUntitledEditorService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IUntitledEditorService>;
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* Events for when untitled editors content changes (e.g. any keystroke).
|
||||
@@ -84,7 +84,7 @@ export interface IUntitledEditorService {
|
||||
* It is valid to pass in a file resource. In that case the path will be used as identifier.
|
||||
* The use case is to be able to create a new file with a specific path with VSCode.
|
||||
*/
|
||||
createOrGet(resource?: URI, mode?: string, initialValue?: string, encoding?: string): UntitledEditorInput;
|
||||
createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string): UntitledEditorInput;
|
||||
|
||||
/**
|
||||
* Creates a new untitled model with the optional resource URI or returns an existing one
|
||||
@@ -191,10 +191,10 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor
|
||||
}
|
||||
|
||||
loadOrCreate(options: IModelLoadOrCreateOptions = Object.create(null)): Promise<UntitledEditorModel> {
|
||||
return this.createOrGet(options.resource, options.mode, options.initialValue, options.encoding, options.useResourcePath).resolve();
|
||||
return this.createOrGet(options.resource, options.modeId, options.initialValue, options.encoding, options.useResourcePath).resolve();
|
||||
}
|
||||
|
||||
createOrGet(resource?: URI, mode?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput {
|
||||
createOrGet(resource?: URI, modeId?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledEditorInput {
|
||||
if (resource) {
|
||||
|
||||
// Massage resource if it comes with known file based resource
|
||||
@@ -214,47 +214,44 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor
|
||||
}
|
||||
|
||||
// Create new otherwise
|
||||
return this.doCreate(resource, hasAssociatedFilePath, mode, initialValue, encoding);
|
||||
return this.doCreate(resource, hasAssociatedFilePath, modeId, initialValue, encoding);
|
||||
}
|
||||
|
||||
private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, mode?: string, initialValue?: string, encoding?: string): UntitledEditorInput {
|
||||
let untitledResource: URI;
|
||||
if (resource) {
|
||||
untitledResource = resource;
|
||||
} else {
|
||||
private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, modeId?: string, initialValue?: string, encoding?: string): UntitledEditorInput {
|
||||
if (!resource) {
|
||||
|
||||
// Create new taking a resource URI that is not already taken
|
||||
let counter = this.mapResourceToInput.size + 1;
|
||||
do {
|
||||
untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` });
|
||||
resource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` });
|
||||
counter++;
|
||||
} while (this.mapResourceToInput.has(untitledResource));
|
||||
} while (this.mapResourceToInput.has(resource));
|
||||
}
|
||||
|
||||
// Look up default language from settings if any
|
||||
if (!mode && !hasAssociatedFilePath) {
|
||||
if (!modeId && !hasAssociatedFilePath) {
|
||||
const configuration = this.configurationService.getValue<IFilesConfiguration>();
|
||||
if (configuration.files && configuration.files.defaultLanguage) {
|
||||
mode = configuration.files.defaultLanguage;
|
||||
modeId = configuration.files.defaultLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
const input = this.instantiationService.createInstance(UntitledEditorInput, untitledResource, hasAssociatedFilePath, mode, initialValue, encoding);
|
||||
const input = this.instantiationService.createInstance(UntitledEditorInput, resource, hasAssociatedFilePath, modeId, initialValue, encoding);
|
||||
|
||||
const contentListener = input.onDidModelChangeContent(() => {
|
||||
this._onDidChangeContent.fire(untitledResource);
|
||||
this._onDidChangeContent.fire(resource!);
|
||||
});
|
||||
|
||||
const dirtyListener = input.onDidChangeDirty(() => {
|
||||
this._onDidChangeDirty.fire(untitledResource);
|
||||
this._onDidChangeDirty.fire(resource!);
|
||||
});
|
||||
|
||||
const encodingListener = input.onDidModelChangeEncoding(() => {
|
||||
this._onDidChangeEncoding.fire(untitledResource);
|
||||
this._onDidChangeEncoding.fire(resource!);
|
||||
});
|
||||
|
||||
const disposeListener = input.onDispose(() => {
|
||||
this._onDidDisposeModel.fire(untitledResource);
|
||||
this._onDidDisposeModel.fire(resource!);
|
||||
});
|
||||
|
||||
// Remove from cache on dispose
|
||||
@@ -269,7 +266,7 @@ export class UntitledEditorService extends Disposable implements IUntitledEditor
|
||||
});
|
||||
|
||||
// Add to cache
|
||||
this.mapResourceToInput.set(untitledResource, input);
|
||||
this.mapResourceToInput.set(resource, input);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user