mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 02:48:30 -05:00
Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)
* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 * disable strict null check
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, relativePath } from 'vs/base/common/resources';
|
||||
|
||||
export const IBackupFileService = createDecorator<IBackupFileService>('backupFileService');
|
||||
|
||||
@@ -18,13 +20,19 @@ export interface IResolvedBackup<T extends object> {
|
||||
* A service that handles any I/O and state associated with the backup system.
|
||||
*/
|
||||
export interface IBackupFileService {
|
||||
_serviceBrand: any;
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
/**
|
||||
* Finds out if there are any backups stored.
|
||||
*/
|
||||
hasBackups(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Finds out if the provided resource with the given version is backed up.
|
||||
*/
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean;
|
||||
|
||||
/**
|
||||
* Loads the backup resource for a particular resource within the current workspace.
|
||||
*
|
||||
@@ -80,3 +88,7 @@ export interface IBackupFileService {
|
||||
*/
|
||||
discardAllWorkspaceBackups(): Promise<void>;
|
||||
}
|
||||
|
||||
export function toBackupWorkspaceResource(backupWorkspacePath: string, environmentService: IEnvironmentService): URI {
|
||||
return joinPath(environmentService.userRoamingDataHome, relativePath(URI.file(environmentService.userDataPath), URI.file(backupWorkspacePath))!);
|
||||
}
|
||||
443
src/vs/workbench/services/backup/common/backupFileService.ts
Normal file
443
src/vs/workbench/services/backup/common/backupFileService.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { URI } from 'vs/base/common/uri';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { equals, deepClone } from 'vs/base/common/objects';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { keys, ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
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>;
|
||||
|
||||
add(resource: URI, versionId?: number, meta?: object): void;
|
||||
has(resource: URI, versionId?: number, meta?: object): 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();
|
||||
|
||||
constructor(private fileService: IFileService) { }
|
||||
|
||||
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 => {
|
||||
|
||||
// Read backup directory for backups
|
||||
const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
|
||||
|
||||
// Remember known backups in our caches
|
||||
if (backupSchemaStat.children) {
|
||||
backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore any errors
|
||||
}
|
||||
|
||||
return 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...
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean {
|
||||
const entry = this.cache.get(resource);
|
||||
if (!entry) {
|
||||
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
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get(): URI[] {
|
||||
return this.cache.keys();
|
||||
}
|
||||
|
||||
remove(resource: URI): void {
|
||||
this.cache.delete(resource);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private impl: IBackupFileService;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IFileService fileService: IFileService
|
||||
) {
|
||||
const backupWorkspaceResource = environmentService.configuration.backupWorkspaceResource;
|
||||
if (backupWorkspaceResource) {
|
||||
this.impl = new BackupFileServiceImpl(backupWorkspaceResource, this.hashPath, fileService);
|
||||
} else {
|
||||
this.impl = new InMemoryBackupFileService(this.hashPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected hashPath(resource: URI): string {
|
||||
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
|
||||
|
||||
return hash(str).toString(16);
|
||||
}
|
||||
|
||||
initialize(backupWorkspaceResource: URI): void {
|
||||
if (this.impl instanceof BackupFileServiceImpl) {
|
||||
this.impl.initialize(backupWorkspaceResource);
|
||||
}
|
||||
}
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return this.impl.hasBackups();
|
||||
}
|
||||
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean {
|
||||
return this.impl.hasBackupSync(resource, versionId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
return this.impl.discardResourceBackup(resource);
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
return this.impl.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return this.impl.getWorkspaceFileBackups();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
return this.impl.resolveBackupContent(backup);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private backupWorkspacePath: URI;
|
||||
|
||||
private isShuttingDown: boolean;
|
||||
private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
|
||||
|
||||
private ready: Promise<IBackupFilesModel>;
|
||||
private model: IBackupFilesModel;
|
||||
|
||||
constructor(
|
||||
backupWorkspaceResource: URI,
|
||||
private readonly hashPath: (resource: URI) => string,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
this.isShuttingDown = false;
|
||||
this.ioOperationQueues = new ResourceQueue();
|
||||
|
||||
this.initialize(backupWorkspaceResource);
|
||||
}
|
||||
|
||||
initialize(backupWorkspaceResource: URI): void {
|
||||
this.backupWorkspacePath = backupWorkspaceResource;
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
private init(): Promise<IBackupFilesModel> {
|
||||
this.model = new BackupFilesModel(this.fileService);
|
||||
|
||||
return this.model.resolve(this.backupWorkspacePath);
|
||||
}
|
||||
|
||||
async hasBackups(): Promise<boolean> {
|
||||
const model = await this.ready;
|
||||
|
||||
return model.count() > 0;
|
||||
}
|
||||
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.model.has(backupResource, versionId);
|
||||
}
|
||||
|
||||
async loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const model = await this.ready;
|
||||
|
||||
// Return directly if we have a known backup with that resource
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource)) {
|
||||
return backupResource;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Without Metadata: URI + END
|
||||
if (!preamble) {
|
||||
preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
}
|
||||
|
||||
// Update content with value
|
||||
await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
|
||||
|
||||
// Update model
|
||||
model.add(backupResource, versionId, meta);
|
||||
});
|
||||
}
|
||||
|
||||
async discardResourceBackup(resource: URI): Promise<void> {
|
||||
const model = await this.ready;
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
await this.fileService.del(backupResource, { recursive: true });
|
||||
|
||||
model.remove(backupResource);
|
||||
});
|
||||
}
|
||||
|
||||
async discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
await this.fileService.del(this.backupWorkspacePath, { recursive: true });
|
||||
|
||||
model.clear();
|
||||
}
|
||||
|
||||
async getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
const model = await this.ready;
|
||||
|
||||
const backups = await Promise.all(model.get().map(async fileBackup => {
|
||||
const backupPreamble = await this.readToMatchingString(fileBackup, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH);
|
||||
if (!backupPreamble) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
private async readToMatchingString(file: URI, matchingString: string, maximumBytesToRead: number): Promise<string> {
|
||||
const contents = (await this.fileService.readFile(file, { length: maximumBytesToRead })).value.toString();
|
||||
|
||||
const newLineIndex = contents.indexOf(matchingString);
|
||||
if (newLineIndex >= 0) {
|
||||
return contents.substr(0, newLineIndex);
|
||||
}
|
||||
|
||||
throw new Error(`Could not find ${JSON.stringify(matchingString)} in first ${maximumBytesToRead} bytes of ${file}`);
|
||||
}
|
||||
|
||||
async resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
|
||||
// Metadata extraction
|
||||
let metaRaw = '';
|
||||
let metaEndFound = false;
|
||||
|
||||
// Add a filter method to filter out everything until the meta end marker
|
||||
const metaPreambleFilter = (chunk: VSBuffer) => {
|
||||
const chunkString = chunk.toString();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
metaEndFound = true;
|
||||
metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return joinPath(this.backupWorkspacePath, resource.scheme, this.hashPath(resource));
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryBackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private backups: Map<string, ITextSnapshot> = new Map();
|
||||
|
||||
constructor(private readonly hashPath: (resource: URI) => string) { }
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return Promise.resolve(this.backups.size > 0);
|
||||
}
|
||||
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.backups.has(backupResource.toString());
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (this.backups.has(backupResource.toString())) {
|
||||
return Promise.resolve(backupResource);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): 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>> {
|
||||
const snapshot = this.backups.get(backupResource.toString());
|
||||
if (snapshot) {
|
||||
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
|
||||
}
|
||||
|
||||
return Promise.reject('Unexpected backup resource to resolve');
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.backups.clear();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return URI.file(join(resource.scheme, this.hashPath(resource)));
|
||||
}
|
||||
}
|
||||
@@ -3,407 +3,17 @@
|
||||
* 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 { BackupFileService as CommonBackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { equals, deepClone } from 'vs/base/common/objects';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { IBackupFileService, IResolvedBackup } 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 { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { keys, ResourceMap } 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';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export interface IBackupFilesModel {
|
||||
resolve(backupRoot: URI): Promise<IBackupFilesModel>;
|
||||
export class BackupFileService extends CommonBackupFileService {
|
||||
|
||||
add(resource: URI, versionId?: number, meta?: object): void;
|
||||
has(resource: URI, versionId?: number, meta?: object): 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();
|
||||
|
||||
constructor(private fileService: IFileService) { }
|
||||
|
||||
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 => {
|
||||
|
||||
// Read backup directory for backups
|
||||
const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
|
||||
|
||||
// Remember known backups in our caches
|
||||
if (backupSchemaStat.children) {
|
||||
backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore any errors
|
||||
}
|
||||
|
||||
return this;
|
||||
protected hashPath(resource: URI): string {
|
||||
return hashPath(resource);
|
||||
}
|
||||
|
||||
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...
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean {
|
||||
const entry = this.cache.get(resource);
|
||||
if (!entry) {
|
||||
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
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get(): URI[] {
|
||||
return this.cache.keys();
|
||||
}
|
||||
|
||||
remove(resource: URI): void {
|
||||
this.cache.delete(resource);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private impl: IBackupFileService;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IFileService fileService: IFileService
|
||||
) {
|
||||
const backupWorkspacePath = environmentService.configuration.backupPath;
|
||||
if (backupWorkspacePath) {
|
||||
this.impl = new BackupFileServiceImpl(backupWorkspacePath, fileService);
|
||||
} else {
|
||||
this.impl = new InMemoryBackupFileService();
|
||||
}
|
||||
}
|
||||
|
||||
initialize(backupWorkspacePath: string): void {
|
||||
if (this.impl instanceof BackupFileServiceImpl) {
|
||||
this.impl.initialize(backupWorkspacePath);
|
||||
}
|
||||
}
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return this.impl.hasBackups();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
return this.impl.discardResourceBackup(resource);
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
return this.impl.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return this.impl.getWorkspaceFileBackups();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
return this.impl.resolveBackupContent(backup);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private backupWorkspacePath: URI;
|
||||
|
||||
private isShuttingDown: boolean;
|
||||
private ready: Promise<IBackupFilesModel>;
|
||||
private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
|
||||
|
||||
constructor(
|
||||
backupWorkspacePath: string,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
this.isShuttingDown = false;
|
||||
this.ioOperationQueues = new ResourceQueue();
|
||||
|
||||
this.initialize(backupWorkspacePath);
|
||||
}
|
||||
|
||||
initialize(backupWorkspacePath: string): void {
|
||||
this.backupWorkspacePath = URI.file(backupWorkspacePath);
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
private init(): Promise<IBackupFilesModel> {
|
||||
const model = new BackupFilesModel(this.fileService);
|
||||
|
||||
return model.resolve(this.backupWorkspacePath);
|
||||
}
|
||||
|
||||
async hasBackups(): Promise<boolean> {
|
||||
const model = await this.ready;
|
||||
|
||||
return model.count() > 0;
|
||||
}
|
||||
|
||||
async loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const model = await this.ready;
|
||||
|
||||
// Return directly if we have a known backup with that resource
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource)) {
|
||||
return backupResource;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Without Metadata: URI + END
|
||||
if (!preamble) {
|
||||
preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
}
|
||||
|
||||
// Update content with value
|
||||
await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
|
||||
|
||||
// Update model
|
||||
model.add(backupResource, versionId, meta);
|
||||
});
|
||||
}
|
||||
|
||||
async discardResourceBackup(resource: URI): Promise<void> {
|
||||
const model = await this.ready;
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
await this.fileService.del(backupResource, { recursive: true });
|
||||
|
||||
model.remove(backupResource);
|
||||
});
|
||||
}
|
||||
|
||||
async discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
await this.fileService.del(this.backupWorkspacePath, { recursive: true });
|
||||
|
||||
model.clear();
|
||||
}
|
||||
|
||||
async getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
const model = await this.ready;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
async resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
|
||||
// Metadata extraction
|
||||
let metaRaw = '';
|
||||
let metaEndFound = false;
|
||||
|
||||
// Add a filter method to filter out everything until the meta end marker
|
||||
const metaPreambleFilter = (chunk: VSBuffer) => {
|
||||
const chunkString = chunk.toString();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
metaEndFound = true;
|
||||
metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return joinPath(this.backupWorkspacePath, resource.scheme, hashPath(resource));
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryBackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private backups: Map<string, ITextSnapshot> = new Map();
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return Promise.resolve(this.backups.size > 0);
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (this.backups.has(backupResource.toString())) {
|
||||
return Promise.resolve(backupResource);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): 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>> {
|
||||
const snapshot = this.backups.get(backupResource.toString());
|
||||
if (snapshot) {
|
||||
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
|
||||
}
|
||||
|
||||
return Promise.reject('Unexpected backup resource to resolve');
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.backups.clear();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return URI.file(join(resource.scheme, hashPath(resource)));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -411,7 +21,5 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
registerSingleton(IBackupFileService, BackupFileService);
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
@@ -11,22 +11,26 @@ 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 { BackupFilesModel } from 'vs/workbench/services/backup/common/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 { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/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';
|
||||
import { hashPath, BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
|
||||
import { BACKUPS } from 'vs/platform/environment/common/environment';
|
||||
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
|
||||
|
||||
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
|
||||
const backupHome = path.join(parentDir, 'Backups');
|
||||
const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
|
||||
const appSettingsHome = path.join(userdataDir, 'User');
|
||||
const backupHome = path.join(userdataDir, 'Backups');
|
||||
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
|
||||
|
||||
const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
|
||||
@@ -43,18 +47,10 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(u
|
||||
|
||||
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;
|
||||
constructor(backupPath: string) {
|
||||
super({ ...parseArgs(process.argv), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath);
|
||||
}
|
||||
|
||||
get configuration(): IWindowConfiguration {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
|
||||
class TestBackupFileService extends BackupFileService {
|
||||
@@ -62,9 +58,11 @@ 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);
|
||||
const fileService = new FileService(new NullLogService());
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService));
|
||||
|
||||
super(environmentService, fileService);
|
||||
|
||||
@@ -124,8 +122,8 @@ suite('BackupFileService', () => {
|
||||
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);
|
||||
const expectedPath = URI.file(path.join(appSettingsHome, BACKUPS, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString();
|
||||
assert.equal(service.toBackupResource(backupResource).toString(), expectedPath);
|
||||
});
|
||||
|
||||
test('should get the correct backup path for untitled files', () => {
|
||||
@@ -133,8 +131,8 @@ suite('BackupFileService', () => {
|
||||
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);
|
||||
const expectedPath = URI.file(path.join(appSettingsHome, BACKUPS, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString();
|
||||
assert.equal(service.toBackupResource(backupResource).toString(), expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,6 +155,16 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
});
|
||||
|
||||
test('text file (with version)', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false), 666);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
assert.ok(!service.hasBackupSync(fooFile, 555));
|
||||
assert.ok(service.hasBackupSync(fooFile, 666));
|
||||
});
|
||||
|
||||
test('text file (with meta)', async () => {
|
||||
@@ -164,6 +172,7 @@ suite('BackupFileService', () => {
|
||||
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`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
@@ -171,6 +180,7 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
assert.ok(service.hasBackupSync(untitledFile));
|
||||
});
|
||||
|
||||
test('text file (ITextSnapshot)', async () => {
|
||||
@@ -180,6 +190,8 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
@@ -190,6 +202,7 @@ suite('BackupFileService', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -201,6 +214,8 @@ suite('BackupFileService', () => {
|
||||
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}`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
@@ -212,6 +227,8 @@ suite('BackupFileService', () => {
|
||||
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}`);
|
||||
assert.ok(service.hasBackupSync(untitledFile));
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
@@ -220,9 +237,12 @@ suite('BackupFileService', () => {
|
||||
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.ok(service.hasBackupSync(fooFile));
|
||||
|
||||
await service.discardResourceBackup(fooFile);
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
|
||||
assert.ok(!service.hasBackupSync(fooFile));
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
|
||||
@@ -1,30 +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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export const IBroadcastService = createDecorator<IBroadcastService>('broadcastService');
|
||||
|
||||
export interface IBroadcast {
|
||||
channel: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface IBroadcastService {
|
||||
_serviceBrand: any;
|
||||
|
||||
onBroadcast: Event<IBroadcast>;
|
||||
|
||||
broadcast(b: IBroadcast): void;
|
||||
}
|
||||
|
||||
export class NullBroadcastService implements IBroadcastService {
|
||||
_serviceBrand: any;
|
||||
onBroadcast: Event<IBroadcast> = Event.None;
|
||||
broadcast(_b: IBroadcast): void {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,51 +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 { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IBroadcastService, IBroadcast } from 'vs/workbench/services/broadcast/common/broadcast';
|
||||
|
||||
class BroadcastService extends Disposable implements IBroadcastService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _onBroadcast: Emitter<IBroadcast> = this._register(new Emitter<IBroadcast>());
|
||||
get onBroadcast(): Event<IBroadcast> { return this._onBroadcast.event; }
|
||||
|
||||
private windowId: number;
|
||||
|
||||
constructor(
|
||||
@IWindowService readonly windowService: IWindowService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.windowId = windowService.windowId;
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
ipc.on('vscode:broadcast', (event: unknown, b: IBroadcast) => {
|
||||
this.logService.trace(`Received broadcast from main in window ${this.windowId}: `, b);
|
||||
|
||||
this._onBroadcast.fire(b);
|
||||
});
|
||||
}
|
||||
|
||||
broadcast(b: IBroadcast): void {
|
||||
this.logService.trace(`Sending broadcast to main from window ${this.windowId}: `, b);
|
||||
|
||||
ipc.send('vscode:broadcast', this.windowId, {
|
||||
channel: b.channel,
|
||||
payload: b.payload
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IBroadcastService, BroadcastService, true);
|
||||
@@ -18,7 +18,7 @@ import { localize } from 'vs/nls';
|
||||
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { emptyProgressRunner, IProgress, IProgressRunner } from 'vs/platform/progress/common/progress';
|
||||
import { IProgress, IProgressStep, emptyProgress } from 'vs/platform/progress/common/progress';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
@@ -231,11 +231,11 @@ export class BulkEdit {
|
||||
|
||||
private _edits: Edit[] = [];
|
||||
private _editor: ICodeEditor | undefined;
|
||||
private _progress?: IProgressRunner;
|
||||
private _progress: IProgress<IProgressStep>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor | undefined,
|
||||
progress: IProgressRunner | undefined,
|
||||
progress: IProgress<IProgressStep> | undefined,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@@ -244,7 +244,7 @@ export class BulkEdit {
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._progress = progress || emptyProgressRunner;
|
||||
this._progress = progress || emptyProgress;
|
||||
}
|
||||
|
||||
add(edits: Edit[] | Edit): void {
|
||||
@@ -294,10 +294,9 @@ export class BulkEdit {
|
||||
|
||||
// define total work and progress callback
|
||||
// for child operations
|
||||
if (this._progress) {
|
||||
this._progress.total(total);
|
||||
}
|
||||
let progress: IProgress<void> = { report: _ => this._progress && this._progress.worked(1) };
|
||||
this._progress.report({ total });
|
||||
|
||||
let progress: IProgress<void> = { report: _ => this._progress.report({ increment: 1 }) };
|
||||
|
||||
// do it.
|
||||
for (const group of groups) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { CommandService } from 'vs/workbench/services/commands/common/commandService';
|
||||
import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
@@ -103,7 +103,7 @@ suite('CommandService', function () {
|
||||
test('Stop waiting for * extensions to activate when trigger is satisfied #62457', function () {
|
||||
|
||||
let callCounter = 0;
|
||||
let dispoables: IDisposable[] = [];
|
||||
const dispoables = new DisposableStore();
|
||||
let events: string[] = [];
|
||||
let service = new CommandService(new InstantiationService(), new class extends NullExtensionService {
|
||||
|
||||
@@ -118,7 +118,7 @@ suite('CommandService', function () {
|
||||
let reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => {
|
||||
callCounter += 1;
|
||||
});
|
||||
dispoables.push(reg);
|
||||
dispoables.add(reg);
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
@@ -131,14 +131,15 @@ suite('CommandService', function () {
|
||||
return service.executeCommand('farboo').then(() => {
|
||||
assert.equal(callCounter, 1);
|
||||
assert.deepEqual(events.sort(), ['*', 'onCommand:farboo'].sort());
|
||||
dispose(dispoables);
|
||||
}).finally(() => {
|
||||
dispoables.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #71471: wait for onCommand activation even if a command is registered', () => {
|
||||
let expectedOrder: string[] = ['registering command', 'resolving activation event', 'executing command'];
|
||||
let actualOrder: string[] = [];
|
||||
let disposables: IDisposable[] = [];
|
||||
const disposables = new DisposableStore();
|
||||
let service = new CommandService(new InstantiationService(), new class extends NullExtensionService {
|
||||
|
||||
activateByEvent(event: string): Promise<void> {
|
||||
@@ -153,7 +154,7 @@ suite('CommandService', function () {
|
||||
let reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => {
|
||||
actualOrder.push('executing command');
|
||||
});
|
||||
disposables.push(reg);
|
||||
disposables.add(reg);
|
||||
|
||||
setTimeout(() => {
|
||||
// Resolve the activation event after some more time
|
||||
@@ -170,7 +171,8 @@ suite('CommandService', function () {
|
||||
|
||||
return service.executeCommand('farboo2').then(() => {
|
||||
assert.deepEqual(actualOrder, expectedOrder);
|
||||
dispose(disposables);
|
||||
}).finally(() => {
|
||||
disposables.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,10 +9,10 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
|
||||
import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
|
||||
import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, ConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService';
|
||||
import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
@@ -20,15 +20,54 @@ import { ConfigurationScope } from 'vs/platform/configuration/common/configurati
|
||||
import { extname, join } from 'vs/base/common/path';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IConfigurationModel, compare } from 'vs/platform/configuration/common/configuration';
|
||||
import { createSHA1 } from 'vs/base/browser/hash';
|
||||
import { IConfigurationModel } from 'vs/platform/configuration/common/configuration';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
|
||||
export class UserConfiguration extends Disposable {
|
||||
|
||||
private readonly parser: ConfigurationModelParser;
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
protected readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
|
||||
|
||||
constructor(
|
||||
private readonly userSettingsResource: URI,
|
||||
private readonly scopes: ConfigurationScope[] | undefined,
|
||||
private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes);
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this.reloadConfigurationScheduler.schedule()));
|
||||
}
|
||||
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
return this.reload();
|
||||
}
|
||||
|
||||
async reload(): Promise<ConfigurationModel> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.userSettingsResource);
|
||||
this.parser.parseContent(content.value.toString() || '{}');
|
||||
return this.parser.configurationModel;
|
||||
} catch (e) {
|
||||
return new ConfigurationModel();
|
||||
}
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
this.parser.parse();
|
||||
return this.parser.configurationModel;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteUserConfiguration extends Disposable {
|
||||
|
||||
private readonly _cachedConfiguration: CachedUserConfiguration;
|
||||
private readonly _configurationFileService: IConfigurationFileService;
|
||||
private _userConfiguration: UserConfiguration | CachedUserConfiguration;
|
||||
private readonly _cachedConfiguration: CachedRemoteUserConfiguration;
|
||||
private readonly _configurationFileService: ConfigurationFileService;
|
||||
private _userConfiguration: FileServiceBasedRemoteUserConfiguration | CachedRemoteUserConfiguration;
|
||||
private _userConfigurationInitializationPromise: Promise<ConfigurationModel> | null = null;
|
||||
|
||||
private readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
@@ -37,15 +76,15 @@ export class RemoteUserConfiguration extends Disposable {
|
||||
constructor(
|
||||
remoteAuthority: string,
|
||||
configurationCache: IConfigurationCache,
|
||||
configurationFileService: IConfigurationFileService,
|
||||
configurationFileService: ConfigurationFileService,
|
||||
remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
super();
|
||||
this._configurationFileService = configurationFileService;
|
||||
this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, configurationCache);
|
||||
this._userConfiguration = this._cachedConfiguration = new CachedRemoteUserConfiguration(remoteAuthority, configurationCache);
|
||||
remoteAgentService.getEnvironment().then(async environment => {
|
||||
if (environment) {
|
||||
const userConfiguration = this._register(new UserConfiguration(environment.settingsPath, REMOTE_MACHINE_SCOPES, this._configurationFileService));
|
||||
const userConfiguration = this._register(new FileServiceBasedRemoteUserConfiguration(environment.settingsPath, REMOTE_MACHINE_SCOPES, this._configurationFileService));
|
||||
this._register(userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel)));
|
||||
this._userConfigurationInitializationPromise = userConfiguration.initialize();
|
||||
const configurationModel = await this._userConfigurationInitializationPromise;
|
||||
@@ -57,7 +96,7 @@ export class RemoteUserConfiguration extends Disposable {
|
||||
}
|
||||
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
if (this._userConfiguration instanceof UserConfiguration) {
|
||||
if (this._userConfiguration instanceof FileServiceBasedRemoteUserConfiguration) {
|
||||
return this._userConfiguration.initialize();
|
||||
}
|
||||
|
||||
@@ -90,7 +129,7 @@ export class RemoteUserConfiguration extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
export class UserConfiguration extends Disposable {
|
||||
class FileServiceBasedRemoteUserConfiguration extends Disposable {
|
||||
|
||||
private readonly parser: ConfigurationModelParser;
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
@@ -103,7 +142,7 @@ export class UserConfiguration extends Disposable {
|
||||
constructor(
|
||||
private readonly configurationResource: URI,
|
||||
private readonly scopes: ConfigurationScope[] | undefined,
|
||||
private readonly configurationFileService: IConfigurationFileService
|
||||
private readonly configurationFileService: ConfigurationFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -138,11 +177,7 @@ export class UserConfiguration extends Disposable {
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
const exists = await this.configurationFileService.exists(this.configurationResource);
|
||||
this.onResourceExists(exists);
|
||||
const configuraitonModel = await this.reload();
|
||||
if (!this.configurationFileService.isWatching) {
|
||||
this.configurationFileService.whenWatchingStarted.then(() => this.onWatchStarted(configuraitonModel));
|
||||
}
|
||||
return configuraitonModel;
|
||||
return this.reload();
|
||||
}
|
||||
|
||||
async reload(): Promise<ConfigurationModel> {
|
||||
@@ -160,14 +195,6 @@ export class UserConfiguration extends Disposable {
|
||||
return this.parser.configurationModel;
|
||||
}
|
||||
|
||||
private async onWatchStarted(currentModel: ConfigurationModel): Promise<void> {
|
||||
const configuraitonModel = await this.reload();
|
||||
const { added, removed, updated } = compare(currentModel, configuraitonModel);
|
||||
if (added.length || removed.length || updated.length) {
|
||||
this._onDidChangeConfiguration.fire(configuraitonModel);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFileEvents(event: FileChangesEvent): Promise<void> {
|
||||
const events = event.changes;
|
||||
|
||||
@@ -202,7 +229,7 @@ export class UserConfiguration extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
class CachedUserConfiguration extends Disposable {
|
||||
class CachedRemoteUserConfiguration extends Disposable {
|
||||
|
||||
private readonly _onDidChange: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChange: Event<ConfigurationModel> = this._onDidChange.event;
|
||||
@@ -252,7 +279,7 @@ class CachedUserConfiguration extends Disposable {
|
||||
|
||||
export class WorkspaceConfiguration extends Disposable {
|
||||
|
||||
private readonly _configurationFileService: IConfigurationFileService;
|
||||
private readonly _configurationFileService: ConfigurationFileService;
|
||||
private readonly _cachedConfiguration: CachedWorkspaceConfiguration;
|
||||
private _workspaceConfiguration: IWorkspaceConfiguration;
|
||||
private _workspaceConfigurationChangeDisposable: IDisposable = Disposable.None;
|
||||
@@ -266,7 +293,7 @@ export class WorkspaceConfiguration extends Disposable {
|
||||
|
||||
constructor(
|
||||
configurationCache: IConfigurationCache,
|
||||
configurationFileService: IConfigurationFileService
|
||||
configurationFileService: ConfigurationFileService
|
||||
) {
|
||||
super();
|
||||
this._configurationFileService = configurationFileService;
|
||||
@@ -369,7 +396,7 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable implements IWork
|
||||
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(private configurationFileService: IConfigurationFileService) {
|
||||
constructor(private configurationFileService: ConfigurationFileService) {
|
||||
super();
|
||||
|
||||
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser('');
|
||||
@@ -378,10 +405,6 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable implements IWork
|
||||
this._register(configurationFileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
|
||||
this.workspaceConfigWatcher = this._register(this.watchWorkspaceConfigurationFile());
|
||||
|
||||
if (!this.configurationFileService.isWatching) {
|
||||
this.configurationFileService.whenWatchingStarted.then(() => this.onWatchStarted());
|
||||
}
|
||||
}
|
||||
|
||||
get workspaceIdentifier(): IWorkspaceIdentifier | null {
|
||||
@@ -426,18 +449,6 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable implements IWork
|
||||
return this.getWorkspaceSettings();
|
||||
}
|
||||
|
||||
private async onWatchStarted(): Promise<void> {
|
||||
if (this.workspaceIdentifier) {
|
||||
const currentModel = this.getConfigurationModel();
|
||||
await this.load(this.workspaceIdentifier);
|
||||
const newModel = this.getConfigurationModel();
|
||||
const { added, removed, updated } = compare(currentModel, newModel);
|
||||
if (added.length || removed.length || updated.length) {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private consolidate(): void {
|
||||
this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel);
|
||||
}
|
||||
@@ -546,7 +557,7 @@ class FileServiceBasedFolderConfiguration extends Disposable implements IFolderC
|
||||
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(protected readonly configurationFolder: URI, workbenchState: WorkbenchState, private configurationFileService: IConfigurationFileService) {
|
||||
constructor(protected readonly configurationFolder: URI, workbenchState: WorkbenchState, private configurationFileService: ConfigurationFileService) {
|
||||
super();
|
||||
|
||||
this.configurationNames = [FOLDER_SETTINGS_NAME /*First one should be settings */, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY];
|
||||
@@ -557,9 +568,6 @@ class FileServiceBasedFolderConfiguration extends Disposable implements IFolderC
|
||||
|
||||
this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
|
||||
this._register(configurationFileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
|
||||
if (!this.configurationFileService.isWatching) {
|
||||
this.configurationFileService.whenWatchingStarted.then(() => this.onWatchStarted());
|
||||
}
|
||||
}
|
||||
|
||||
async loadConfiguration(): Promise<ConfigurationModel> {
|
||||
@@ -611,15 +619,6 @@ class FileServiceBasedFolderConfiguration extends Disposable implements IFolderC
|
||||
this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations);
|
||||
}
|
||||
|
||||
private async onWatchStarted(): Promise<void> {
|
||||
const currentModel = this._cache;
|
||||
const newModel = await this.loadConfiguration();
|
||||
const { added, removed, updated } = compare(currentModel, newModel);
|
||||
if (added.length || removed.length || updated.length) {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
|
||||
const events = event.changes;
|
||||
let affectedByChanges = false;
|
||||
@@ -672,7 +671,7 @@ class CachedFolderConfiguration extends Disposable implements IFolderConfigurati
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
private configurationModel: ConfigurationModel;
|
||||
private readonly key: Thenable<ConfigurationKey>;
|
||||
private readonly key: ConfigurationKey;
|
||||
|
||||
constructor(
|
||||
folder: URI,
|
||||
@@ -680,14 +679,13 @@ 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 = { type: 'folder', key: hash(join(folder.path, configFolderRelativePath)).toString(16) };
|
||||
this.configurationModel = new ConfigurationModel();
|
||||
}
|
||||
|
||||
async loadConfiguration(): Promise<ConfigurationModel> {
|
||||
try {
|
||||
const key = await this.key;
|
||||
const contents = await this.configurationCache.read(key);
|
||||
const contents = await this.configurationCache.read(this.key);
|
||||
const parsed: IConfigurationModel = JSON.parse(contents.toString());
|
||||
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
|
||||
} catch (e) {
|
||||
@@ -696,11 +694,10 @@ class CachedFolderConfiguration extends Disposable implements IFolderConfigurati
|
||||
}
|
||||
|
||||
async updateConfiguration(configurationModel: ConfigurationModel): Promise<void> {
|
||||
const key = await this.key;
|
||||
if (configurationModel.keys.length) {
|
||||
await this.configurationCache.write(key, JSON.stringify(configurationModel.toJSON()));
|
||||
await this.configurationCache.write(this.key, JSON.stringify(configurationModel.toJSON()));
|
||||
} else {
|
||||
await this.configurationCache.remove(key);
|
||||
await this.configurationCache.remove(this.key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +724,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
|
||||
readonly workspaceFolder: IWorkspaceFolder,
|
||||
configFolderRelativePath: string,
|
||||
private readonly workbenchState: WorkbenchState,
|
||||
configurationFileService: IConfigurationFileService,
|
||||
configurationFileService: ConfigurationFileService,
|
||||
configurationCache: IConfigurationCache
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration';
|
||||
|
||||
export class ConfigurationCache implements IConfigurationCache {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async read(key: ConfigurationKey): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async write(key: ConfigurationKey, content: string): Promise<void> {
|
||||
}
|
||||
|
||||
async remove(key: ConfigurationKey): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,10 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Queue, Barrier } from 'vs/base/common/async';
|
||||
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, ConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
|
||||
@@ -28,6 +27,8 @@ import { localize } from 'vs/nls';
|
||||
import { isEqual, dirname } from 'vs/base/common/resources';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService {
|
||||
|
||||
@@ -37,14 +38,16 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
private completeWorkspaceBarrier: Barrier;
|
||||
private readonly configurationCache: IConfigurationCache;
|
||||
private _configuration: Configuration;
|
||||
private initialized: boolean = false;
|
||||
private defaultConfiguration: DefaultConfigurationModel;
|
||||
private localUserConfiguration: UserConfiguration | null = null;
|
||||
private localUserConfiguration: UserConfiguration;
|
||||
private remoteUserConfiguration: RemoteUserConfiguration | null = null;
|
||||
private workspaceConfiguration: WorkspaceConfiguration;
|
||||
private cachedFolderConfigs: ResourceMap<FolderConfiguration>;
|
||||
|
||||
private workspaceEditingQueue: Queue<void>;
|
||||
|
||||
private readonly configurationFileService: ConfigurationFileService;
|
||||
|
||||
protected readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
|
||||
public readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
|
||||
|
||||
@@ -64,21 +67,23 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
private cyclicDependency = new Promise<void>(resolve => this.cyclicDependencyReady = resolve);
|
||||
|
||||
constructor(
|
||||
{ userSettingsResource, remoteAuthority, configurationCache }: { userSettingsResource?: URI, remoteAuthority?: string, configurationCache: IConfigurationCache },
|
||||
private readonly configurationFileService: IConfigurationFileService,
|
||||
remoteAgentService: IRemoteAgentService,
|
||||
{ remoteAuthority, configurationCache }: { remoteAuthority?: string, configurationCache: IConfigurationCache },
|
||||
environmentService: IWorkbenchEnvironmentService,
|
||||
fileService: IFileService,
|
||||
remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.completeWorkspaceBarrier = new Barrier();
|
||||
this.defaultConfiguration = new DefaultConfigurationModel();
|
||||
this.configurationCache = configurationCache;
|
||||
if (userSettingsResource) {
|
||||
this.localUserConfiguration = this._register(new UserConfiguration(userSettingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, configurationFileService));
|
||||
this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration)));
|
||||
}
|
||||
this.configurationFileService = new ConfigurationFileService(fileService);
|
||||
this._configuration = new Configuration(this.defaultConfiguration, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace);
|
||||
this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>();
|
||||
this.localUserConfiguration = this._register(new UserConfiguration(environmentService.settingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, fileService));
|
||||
this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration)));
|
||||
if (remoteAuthority) {
|
||||
this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, configurationFileService, remoteAgentService));
|
||||
this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, this.configurationFileService, remoteAgentService));
|
||||
this._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration)));
|
||||
}
|
||||
this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, this.configurationFileService));
|
||||
@@ -225,13 +230,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
}
|
||||
|
||||
private contains(resources: URI[], toCheck: URI): boolean {
|
||||
return resources.some(resource => {
|
||||
if (isLinux) {
|
||||
return resource.toString() === toCheck.toString();
|
||||
}
|
||||
|
||||
return resource.toString().toLowerCase() === toCheck.toString().toLowerCase();
|
||||
});
|
||||
return resources.some(resource => isEqual(resource, toCheck));
|
||||
}
|
||||
|
||||
// Workspace Configuration Service Impl
|
||||
@@ -420,7 +419,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
}
|
||||
|
||||
private initializeUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> {
|
||||
return Promise.all([this.localUserConfiguration ? this.localUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel()), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())])
|
||||
return Promise.all([this.localUserConfiguration.initialize(), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())])
|
||||
.then(([local, remote]) => ({ local, remote }));
|
||||
}
|
||||
|
||||
@@ -429,7 +428,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
}
|
||||
|
||||
private reloadLocalUserConfiguration(key?: string): Promise<ConfigurationModel> {
|
||||
return this.localUserConfiguration ? this.localUserConfiguration.reload() : Promise.resolve(new ConfigurationModel());
|
||||
return this.localUserConfiguration.reload();
|
||||
}
|
||||
|
||||
private reloadRemoeUserConfiguration(key?: string): Promise<ConfigurationModel> {
|
||||
@@ -466,11 +465,12 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
const currentConfiguration = this._configuration;
|
||||
this._configuration = new Configuration(this.defaultConfiguration, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace);
|
||||
|
||||
if (currentConfiguration) {
|
||||
if (this.initialized) {
|
||||
const changedKeys = this._configuration.compare(currentConfiguration);
|
||||
this.triggerConfigurationChange(new ConfigurationChangeEvent().change(changedKeys), ConfigurationTarget.WORKSPACE);
|
||||
} else {
|
||||
this._onDidChangeConfiguration.fire(new AllKeysConfigurationChangeEvent(this._configuration, ConfigurationTarget.WORKSPACE, this.getTargetConfiguration(ConfigurationTarget.WORKSPACE)));
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -489,7 +489,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
private onDefaultConfigurationChanged(keys: string[]): void {
|
||||
this.defaultConfiguration = new DefaultConfigurationModel();
|
||||
this.registerConfigurationSchemas();
|
||||
if (this.workspace && this._configuration) {
|
||||
if (this.workspace) {
|
||||
this._configuration.updateDefaultConfiguration(this.defaultConfiguration);
|
||||
if (this.remoteUserConfiguration) {
|
||||
this._configuration.updateRemoteUserConfiguration(this.remoteUserConfiguration.reprocess());
|
||||
@@ -545,14 +545,12 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
}
|
||||
|
||||
private onRemoteUserConfigurationChanged(userConfiguration: ConfigurationModel): void {
|
||||
if (this._configuration) {
|
||||
const keys = this._configuration.compareAndUpdateRemoteUserConfiguration(userConfiguration);
|
||||
this.triggerConfigurationChange(keys, ConfigurationTarget.USER);
|
||||
}
|
||||
const keys = this._configuration.compareAndUpdateRemoteUserConfiguration(userConfiguration);
|
||||
this.triggerConfigurationChange(keys, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
private onWorkspaceConfigurationChanged(): Promise<void> {
|
||||
if (this.workspace && this.workspace.configuration && this._configuration) {
|
||||
if (this.workspace && this.workspace.configuration) {
|
||||
const workspaceConfigurationChangeEvent = this._configuration.compareAndUpdateWorkspaceConfiguration(this.workspaceConfiguration.getConfiguration());
|
||||
let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), this.workspace.configuration);
|
||||
const changes = this.compareFolders(this.workspace.folders, configuredFolders);
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
|
||||
export const FOLDER_CONFIG_FOLDER_NAME = '.azuredatastudio';
|
||||
@@ -42,24 +41,11 @@ export interface IConfigurationCache {
|
||||
|
||||
}
|
||||
|
||||
export interface IConfigurationFileService {
|
||||
fileService: IFileService | null;
|
||||
readonly onFileChanges: Event<FileChangesEvent>;
|
||||
readonly isWatching: boolean;
|
||||
readonly whenWatchingStarted: Promise<void>;
|
||||
whenProviderRegistered(scheme: string): Promise<void>;
|
||||
watch(resource: URI): IDisposable;
|
||||
exists(resource: URI): Promise<boolean>;
|
||||
readFile(resource: URI): Promise<string>;
|
||||
}
|
||||
export class ConfigurationFileService {
|
||||
|
||||
export class ConfigurationFileService implements IConfigurationFileService {
|
||||
|
||||
constructor(public fileService: IFileService) { }
|
||||
constructor(private readonly fileService: IFileService) { }
|
||||
|
||||
get onFileChanges() { return this.fileService.onFileChanges; }
|
||||
readonly whenWatchingStarted: Promise<void> = Promise.resolve();
|
||||
readonly isWatching: boolean = true;
|
||||
|
||||
whenProviderRegistered(scheme: string): Promise<void> {
|
||||
if (this.fileService.canHandleResource(URI.from({ scheme }))) {
|
||||
|
||||
@@ -528,7 +528,7 @@ export class ConfigurationEditingService {
|
||||
|
||||
private getConfigurationFileResource(target: EditableConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null {
|
||||
if (target === EditableConfigurationTarget.USER_LOCAL) {
|
||||
return URI.file(this.environmentService.appSettingsPath);
|
||||
return this.environmentService.settingsResource;
|
||||
}
|
||||
if (target === EditableConfigurationTarget.USER_REMOTE) {
|
||||
return this.remoteSettingsResource;
|
||||
|
||||
@@ -39,10 +39,11 @@ export class JSONEditingService implements IJSONEditingService {
|
||||
return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(resource, value, save))); // queue up writes to prevent race conditions
|
||||
}
|
||||
|
||||
private doWriteConfiguration(resource: URI, value: IJSONValue, save: boolean): Promise<void> {
|
||||
return this.resolveAndValidate(resource, save)
|
||||
.then(reference => this.writeToBuffer(reference.object.textEditorModel, value)
|
||||
.then(() => reference.dispose()));
|
||||
private async doWriteConfiguration(resource: URI, value: IJSONValue, save: boolean): Promise<void> {
|
||||
const reference = await this.resolveAndValidate(resource, save);
|
||||
await this.writeToBuffer(reference.object.textEditorModel, value);
|
||||
|
||||
reference.dispose();
|
||||
}
|
||||
|
||||
private async writeToBuffer(model: ITextModel, value: IJSONValue): Promise<any> {
|
||||
@@ -97,21 +98,21 @@ export class JSONEditingService implements IJSONEditingService {
|
||||
return parseErrors.length > 0;
|
||||
}
|
||||
|
||||
private resolveAndValidate(resource: URI, checkDirty: boolean): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
return this.resolveModelReference(resource)
|
||||
.then(reference => {
|
||||
const model = reference.object.textEditorModel;
|
||||
private async resolveAndValidate(resource: URI, checkDirty: boolean): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
const reference = await this.resolveModelReference(resource);
|
||||
|
||||
if (this.hasParseErrors(model)) {
|
||||
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
|
||||
}
|
||||
const model = reference.object.textEditorModel;
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (checkDirty && this.textFileService.isDirty(resource)) {
|
||||
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
|
||||
}
|
||||
return reference;
|
||||
});
|
||||
if (this.hasParseErrors(model)) {
|
||||
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
|
||||
}
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (checkDirty && this.textFileService.isDirty(resource)) {
|
||||
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
private reject<T>(code: JSONEditingErrorCode): Promise<T> {
|
||||
|
||||
@@ -7,12 +7,13 @@ import * as pfs from 'vs/base/node/pfs';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService';
|
||||
|
||||
export class ConfigurationCache implements IConfigurationCache {
|
||||
|
||||
private readonly cachedConfigurations: Map<string, CachedConfiguration> = new Map<string, CachedConfiguration>();
|
||||
|
||||
constructor(private readonly environmentService: IEnvironmentService) {
|
||||
constructor(private readonly environmentService: IWorkbenchEnvironmentService) {
|
||||
}
|
||||
|
||||
read(key: ConfigurationKey): Promise<string> {
|
||||
|
||||
@@ -1,79 +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 pfs from 'vs/base/node/pfs';
|
||||
import { IConfigurationFileService, ConfigurationFileService as FileServiceBasedConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export class ConfigurationFileService extends Disposable implements IConfigurationFileService {
|
||||
|
||||
private _fileServiceBasedConfigurationFileService: FileServiceBasedConfigurationFileService | null = null;
|
||||
private _fileServiceBasedConfigurationFileServiceCallback: (fileServiceBasedConfigurationFileService: FileServiceBasedConfigurationFileService) => void;
|
||||
private _whenFileServiceBasedConfigurationFileServiceAvailable: Promise<FileServiceBasedConfigurationFileService> = new Promise((c) => this._fileServiceBasedConfigurationFileServiceCallback = c);
|
||||
private _watchResources: { resource: URI, disposable: { disposable: IDisposable | null } }[] = [];
|
||||
readonly whenWatchingStarted: Promise<void> = this._whenFileServiceBasedConfigurationFileServiceAvailable.then(() => undefined);
|
||||
|
||||
private readonly _onFileChanges: Emitter<FileChangesEvent> = this._register(new Emitter<FileChangesEvent>());
|
||||
readonly onFileChanges: Event<FileChangesEvent> = this._onFileChanges.event;
|
||||
|
||||
get isWatching(): boolean {
|
||||
return this._fileServiceBasedConfigurationFileService ? this._fileServiceBasedConfigurationFileService.isWatching : false;
|
||||
}
|
||||
|
||||
watch(resource: URI): IDisposable {
|
||||
if (this._fileServiceBasedConfigurationFileService) {
|
||||
return this._fileServiceBasedConfigurationFileService.watch(resource);
|
||||
}
|
||||
const disposable: { disposable: IDisposable | null } = { disposable: null };
|
||||
this._watchResources.push({ resource, disposable });
|
||||
return toDisposable(() => {
|
||||
if (disposable.disposable) {
|
||||
disposable.disposable.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
whenProviderRegistered(scheme: string): Promise<void> {
|
||||
if (scheme === Schemas.file) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._whenFileServiceBasedConfigurationFileServiceAvailable
|
||||
.then(fileServiceBasedConfigurationFileService => fileServiceBasedConfigurationFileService.whenProviderRegistered(scheme));
|
||||
}
|
||||
|
||||
exists(resource: URI): Promise<boolean> {
|
||||
return this._fileServiceBasedConfigurationFileService ? this._fileServiceBasedConfigurationFileService.exists(resource) : pfs.exists(resource.fsPath);
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<string> {
|
||||
if (this._fileServiceBasedConfigurationFileService) {
|
||||
return this._fileServiceBasedConfigurationFileService.readFile(resource);
|
||||
} else {
|
||||
const contents = await pfs.readFile(resource.fsPath);
|
||||
return contents.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private _fileService: IFileService | null;
|
||||
get fileService(): IFileService | null {
|
||||
return this._fileService;
|
||||
}
|
||||
|
||||
set fileService(fileService: IFileService | null) {
|
||||
if (fileService && !this._fileServiceBasedConfigurationFileService) {
|
||||
this._fileServiceBasedConfigurationFileService = new FileServiceBasedConfigurationFileService(fileService);
|
||||
this._fileService = fileService;
|
||||
this._register(this._fileServiceBasedConfigurationFileService.onFileChanges(e => this._onFileChanges.fire(e)));
|
||||
for (const { resource, disposable } of this._watchResources) {
|
||||
disposable.disposable = this._fileServiceBasedConfigurationFileService.watch(resource);
|
||||
}
|
||||
this._fileServiceBasedConfigurationFileServiceCallback(this._fileServiceBasedConfigurationFileService);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,9 @@ import * as path from 'vs/base/common/path';
|
||||
import * as fs from 'fs';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
@@ -33,21 +32,25 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { createHash } from 'crypto';
|
||||
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache';
|
||||
import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService';
|
||||
import { KeybindingsEditingService, IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
|
||||
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
|
||||
|
||||
class SettingsTestEnvironmentService extends EnvironmentService {
|
||||
class TestEnvironmentService extends WorkbenchEnvironmentService {
|
||||
|
||||
constructor(args: ParsedArgs, _execPath: string, private customAppSettingsHome: string) {
|
||||
super(args, _execPath);
|
||||
constructor(private _appSettingsHome: URI) {
|
||||
super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
|
||||
}
|
||||
|
||||
get appSettingsPath(): string { return this.customAppSettingsHome; }
|
||||
get appSettingsHome() { return this._appSettingsHome; }
|
||||
|
||||
}
|
||||
|
||||
suite('ConfigurationEditingService', () => {
|
||||
@@ -90,9 +93,8 @@ suite('ConfigurationEditingService', () => {
|
||||
const id = uuid.generateUuid();
|
||||
parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
workspaceDir = path.join(parentDir, 'workspaceconfig', id);
|
||||
globalSettingsFile = path.join(workspaceDir, 'config.json');
|
||||
// {{SQL CARBON EDIT}}
|
||||
workspaceSettingsDir = path.join(workspaceDir, '.azuredatastudio');
|
||||
globalSettingsFile = path.join(workspaceDir, 'settings.json');
|
||||
workspaceSettingsDir = path.join(workspaceDir, '.azuredatastudio'); // {{SQL CARBON EDIT}} .vscode to .azuredatastudio
|
||||
|
||||
return await mkdirp(workspaceSettingsDir, 493);
|
||||
}
|
||||
@@ -102,17 +104,20 @@ suite('ConfigurationEditingService', () => {
|
||||
clearServices();
|
||||
|
||||
instantiationService = <TestInstantiationService>workbenchInstantiationService();
|
||||
const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile);
|
||||
const environmentService = new TestEnvironmentService(URI.file(workspaceDir));
|
||||
instantiationService.stub(IEnvironmentService, environmentService);
|
||||
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, {});
|
||||
const fileService = new FileService(new NullLogService());
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService));
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
instantiationService.stub(IRemoteAgentService, remoteAgentService);
|
||||
const workspaceService = new WorkspaceService({ userSettingsResource: URI.file(environmentService.appSettingsPath), configurationCache: new ConfigurationCache(environmentService) }, new ConfigurationFileService(), remoteAgentService);
|
||||
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService);
|
||||
instantiationService.stub(IWorkspaceContextService, workspaceService);
|
||||
return workspaceService.initialize(noWorkspace ? { id: '' } : { folder: URI.file(workspaceDir), id: createHash('md5').update(URI.file(workspaceDir).toString()).digest('hex') }).then(() => {
|
||||
instantiationService.stub(IConfigurationService, workspaceService);
|
||||
const fileService = new FileService(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
instantiationService.stub(IKeybindingEditingService, instantiationService.createInstance(KeybindingsEditingService));
|
||||
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
|
||||
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
|
||||
instantiationService.stub(ICommandService, CommandService);
|
||||
@@ -122,7 +127,10 @@ suite('ConfigurationEditingService', () => {
|
||||
|
||||
teardown(() => {
|
||||
clearServices();
|
||||
return clearWorkspace();
|
||||
if (workspaceDir) {
|
||||
return rimraf(workspaceDir, RimRafMode.MOVE);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
function clearServices(): void {
|
||||
@@ -135,16 +143,6 @@ suite('ConfigurationEditingService', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function clearWorkspace(): Promise<void> {
|
||||
return new Promise<void>((c, e) => {
|
||||
if (parentDir) {
|
||||
rimraf(parentDir, RimRafMode.MOVE).then(c, c);
|
||||
} else {
|
||||
c(undefined);
|
||||
}
|
||||
}).then(() => parentDir = null!);
|
||||
}
|
||||
|
||||
test('errors cases - invalid key', () => {
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' })
|
||||
.then(() => assert.fail('Should fail with ERROR_UNKNOWN_KEY'),
|
||||
@@ -187,7 +185,7 @@ suite('ConfigurationEditingService', () => {
|
||||
test('do not notify error', () => {
|
||||
instantiationService.stub(ITextFileService, 'isDirty', true);
|
||||
const target = sinon.stub();
|
||||
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: null, notify: null!, error: null!, info: null!, warn: null! });
|
||||
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: null!, notify: null!, error: null!, info: null!, warn: null!, status: null! });
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true })
|
||||
.then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'),
|
||||
(error: ConfigurationEditingError) => {
|
||||
|
||||
@@ -10,8 +10,7 @@ import * as path from 'vs/base/common/path';
|
||||
import * as os from 'os';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
@@ -37,21 +36,27 @@ import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
|
||||
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-browser/remoteAuthorityResolverService';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache';
|
||||
import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { IConfigurationCache } from 'vs/workbench/services/configuration/common/configuration';
|
||||
// import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { SignService } from 'vs/platform/sign/browser/signService';
|
||||
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
|
||||
import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
|
||||
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
class SettingsTestEnvironmentService extends EnvironmentService {
|
||||
class TestEnvironmentService extends WorkbenchEnvironmentService {
|
||||
|
||||
constructor(args: ParsedArgs, _execPath: string, private customAppSettingsHome: string) {
|
||||
super(args, _execPath);
|
||||
constructor(private _appSettingsHome: URI) {
|
||||
super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
|
||||
}
|
||||
|
||||
get appSettingsPath(): string { return this.customAppSettingsHome; }
|
||||
get appSettingsHome() { return this._appSettingsHome; }
|
||||
|
||||
}
|
||||
|
||||
function setUpFolderWorkspace(folderName: string): Promise<{ parentDir: string, folderDir: string }> {
|
||||
@@ -93,6 +98,14 @@ function setUpWorkspace(folders: string[]): Promise<{ parentDir: string, configP
|
||||
|
||||
|
||||
suite('WorkspaceContextService - Folder', () => {
|
||||
setup(() => {
|
||||
// {{SQL CARBON EDIT}} - Remove tests
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
// {{SQL CARBON EDIT}} - Remove tests
|
||||
});
|
||||
|
||||
test('getWorkspace()', () => {
|
||||
// {{SQL CARBON EDIT}} - Remove tests
|
||||
assert.equal(0, 0);
|
||||
@@ -145,15 +158,14 @@ suite('WorkspaceConfigurationService - Remote Folder', () => {
|
||||
remoteSettingsFile = path.join(parentDir, 'remote-settings.json');
|
||||
|
||||
instantiationService = <TestInstantiationService>workbenchInstantiationService();
|
||||
const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile);
|
||||
const environmentService = new TestEnvironmentService(URI.file(parentDir));
|
||||
const remoteEnvironmentPromise = new Promise<Partial<IRemoteAgentEnvironment>>(c => resolveRemoteEnvironment = () => c({ settingsPath: URI.file(remoteSettingsFile).with({ scheme: Schemas.vscodeRemote, authority: remoteAuthority }) }));
|
||||
const remoteAgentService = instantiationService.stub(IRemoteAgentService, <Partial<IRemoteAgentService>>{ getEnvironment: () => remoteEnvironmentPromise });
|
||||
const fileService = new FileService(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
const configurationFileService = new ConfigurationFileService();
|
||||
configurationFileService.fileService = fileService;
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService));
|
||||
const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve() };
|
||||
testObject = new WorkspaceService({ userSettingsResource: URI.file(environmentService.appSettingsPath), configurationCache, remoteAuthority }, configurationFileService, remoteAgentService);
|
||||
testObject = new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService);
|
||||
instantiationService.stub(IWorkspaceContextService, testObject);
|
||||
instantiationService.stub(IConfigurationService, testObject);
|
||||
instantiationService.stub(IEnvironmentService, environmentService);
|
||||
@@ -264,7 +276,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => {
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// fs.writeFileSync(remoteSettingsFile, '{ "configurationService.remote.machineSetting": "remoteValue" }');
|
||||
// await instantiationService.get(IFileService).writeFile(URI.file(remoteSettingsFile), VSBuffer.fromString('{ "configurationService.remote.machineSetting": "remoteValue" }'));
|
||||
// return promise;
|
||||
// });
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import { ConfiguredInput, IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
|
||||
export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService {
|
||||
|
||||
@@ -304,4 +305,6 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi
|
||||
) {
|
||||
super(environmentService.configuration.userEnv, editorService, environmentService, configurationService, commandService, workspaceContextService, quickInputService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true);
|
||||
@@ -35,7 +35,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
|
||||
private _context: IVariableResolveContext,
|
||||
private _envVariables: IProcessEnvironment
|
||||
) {
|
||||
if (isWindows) {
|
||||
if (isWindows && _envVariables) {
|
||||
this._envVariables = Object.create(null);
|
||||
Object.keys(_envVariables).forEach(key => {
|
||||
this._envVariables[key.toLowerCase()] = _envVariables[key];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions';
|
||||
import { IAction, IActionRunner, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
|
||||
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
@@ -66,7 +66,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
|
||||
_serviceBrand: any;
|
||||
|
||||
private _onDidContextMenu = this._register(new Emitter<void>());
|
||||
get onDidContextMenu(): Event<void> { return this._onDidContextMenu.event; }
|
||||
readonly onDidContextMenu: Event<void> = this._onDidContextMenu.event;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@@ -115,7 +115,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
|
||||
}
|
||||
}
|
||||
|
||||
private createMenu(delegate: IContextMenuDelegate, entries: Array<IAction | ContextSubMenu>, onHide: () => void): IContextMenuItem[] {
|
||||
private createMenu(delegate: IContextMenuDelegate, entries: ReadonlyArray<IAction | ContextSubMenu>, onHide: () => void): IContextMenuItem[] {
|
||||
const actionRunner = delegate.actionRunner || new ActionRunner();
|
||||
|
||||
return entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide));
|
||||
@@ -173,13 +173,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
|
||||
}
|
||||
|
||||
private async runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): Promise<void> {
|
||||
/* __GDPR__
|
||||
"workbenchActionExecuted" : {
|
||||
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('workbenchActionExecuted', { id: actionToRun.id, from: 'contextMenu' });
|
||||
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: actionToRun.id, from: 'contextMenu' });
|
||||
|
||||
const context = delegate.getActionsContext ? delegate.getActionsContext(event) : event;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isThenable } from 'vs/base/common/async';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
|
||||
@@ -95,20 +95,20 @@ class DecorationRule {
|
||||
}
|
||||
}
|
||||
|
||||
class DecorationStyles {
|
||||
class DecorationStyles extends Disposable {
|
||||
|
||||
private readonly _disposables: IDisposable[];
|
||||
private readonly _styleElement = createStyleSheet();
|
||||
private readonly _decorationRules = new Map<string, DecorationRule>();
|
||||
|
||||
constructor(
|
||||
private _themeService: IThemeService,
|
||||
) {
|
||||
this._disposables = [this._themeService.onThemeChange(this._onThemeChange, this)];
|
||||
super();
|
||||
this._register(this._themeService.onThemeChange(this._onThemeChange, this));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._disposables);
|
||||
super.dispose();
|
||||
|
||||
const parent = this._styleElement.parentElement;
|
||||
if (parent) {
|
||||
@@ -334,15 +334,14 @@ class DecorationProviderWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
export class FileDecorationsService implements IDecorationsService {
|
||||
export class FileDecorationsService extends Disposable implements IDecorationsService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _data = new LinkedList<DecorationProviderWrapper>();
|
||||
private readonly _onDidChangeDecorationsDelayed = new Emitter<URI | URI[]>();
|
||||
private readonly _onDidChangeDecorations = new Emitter<IResourceDecorationChangeEvent>();
|
||||
private readonly _onDidChangeDecorationsDelayed = this._register(new Emitter<URI | URI[]>());
|
||||
private readonly _onDidChangeDecorations = this._register(new Emitter<IResourceDecorationChangeEvent>());
|
||||
private readonly _decorationStyles: DecorationStyles;
|
||||
private readonly _disposables: IDisposable[];
|
||||
|
||||
readonly onDidChangeDecorations: Event<IResourceDecorationChangeEvent> = Event.any(
|
||||
this._onDidChangeDecorations.event,
|
||||
@@ -356,27 +355,17 @@ export class FileDecorationsService implements IDecorationsService {
|
||||
constructor(
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
this._decorationStyles = new DecorationStyles(themeService);
|
||||
super();
|
||||
this._decorationStyles = this._register(new DecorationStyles(themeService));
|
||||
|
||||
// every so many events we check if there are
|
||||
// css styles that we don't need anymore
|
||||
let count = 0;
|
||||
let reg = this.onDidChangeDecorations(() => {
|
||||
this._register(this.onDidChangeDecorations(() => {
|
||||
if (++count % 17 === 0) {
|
||||
this._decorationStyles.cleanUp(this._data.iterator());
|
||||
}
|
||||
});
|
||||
|
||||
this._disposables = [
|
||||
reg,
|
||||
this._decorationStyles
|
||||
];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._disposables);
|
||||
dispose(this._onDidChangeDecorations);
|
||||
dispose(this._onDidChangeDecorationsDelayed);
|
||||
}));
|
||||
}
|
||||
|
||||
registerDecorationsProvider(provider: IDecorationsProvider): IDisposable {
|
||||
|
||||
@@ -86,14 +86,16 @@ export class FileDialogService implements IFileDialogService {
|
||||
|
||||
private shouldUseSimplified(schema: string): boolean {
|
||||
const setting = this.configurationService.getValue('files.simpleDialog.enable');
|
||||
|
||||
return (schema !== Schemas.file) || (setting === true);
|
||||
}
|
||||
|
||||
private ensureFileSchema(schema: string): string[] {
|
||||
return schema !== Schemas.file ? [schema, Schemas.file] : [schema];
|
||||
// Don't allow untitled schema through.
|
||||
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
|
||||
}
|
||||
|
||||
pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
async pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
|
||||
if (!options.defaultUri) {
|
||||
@@ -103,21 +105,23 @@ export class FileDialogService implements IFileDialogService {
|
||||
if (this.shouldUseSimplified(schema)) {
|
||||
const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder');
|
||||
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
|
||||
return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => {
|
||||
if (uri) {
|
||||
return (this.fileService.resolve(uri)).then(stat => {
|
||||
const toOpen: IURIToOpen = stat.isDirectory ? { folderUri: uri } : { fileUri: uri };
|
||||
return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow });
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const uri = await this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
|
||||
|
||||
if (uri) {
|
||||
const stat = await this.fileService.resolve(uri);
|
||||
|
||||
const toOpen: IURIToOpen = stat.isDirectory ? { folderUri: uri } : { fileUri: uri };
|
||||
return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
pickFileAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
async pickFileAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
|
||||
if (!options.defaultUri) {
|
||||
@@ -127,18 +131,19 @@ export class FileDialogService implements IFileDialogService {
|
||||
if (this.shouldUseSimplified(schema)) {
|
||||
const title = nls.localize('openFile.title', 'Open File');
|
||||
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
|
||||
return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => {
|
||||
if (uri) {
|
||||
return this.windowService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const uri = await this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
|
||||
if (uri) {
|
||||
return this.windowService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
pickFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
async pickFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
|
||||
if (!options.defaultUri) {
|
||||
@@ -148,18 +153,19 @@ export class FileDialogService implements IFileDialogService {
|
||||
if (this.shouldUseSimplified(schema)) {
|
||||
const title = nls.localize('openFolder.title', 'Open Folder');
|
||||
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
|
||||
return this.pickRemoteResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => {
|
||||
if (uri) {
|
||||
return this.windowService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const uri = await this.pickRemoteResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
|
||||
if (uri) {
|
||||
return this.windowService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
|
||||
async pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
|
||||
if (!options.defaultUri) {
|
||||
@@ -170,18 +176,39 @@ export class FileDialogService implements IFileDialogService {
|
||||
const title = nls.localize('openWorkspace.title', 'Open Workspace');
|
||||
const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }];
|
||||
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
|
||||
return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }).then(uri => {
|
||||
if (uri) {
|
||||
return this.windowService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const uri = await this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems });
|
||||
if (uri) {
|
||||
return this.windowService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
async pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
if (this.shouldUseSimplified(schema)) {
|
||||
if (!options.availableFileSystems) {
|
||||
options.availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
|
||||
}
|
||||
|
||||
options.title = nls.localize('saveFileAs.title', 'Save As');
|
||||
return this.saveRemoteResource(options);
|
||||
}
|
||||
|
||||
const result = await this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options));
|
||||
if (result) {
|
||||
return URI.file(result);
|
||||
}
|
||||
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-check
|
||||
}
|
||||
|
||||
private toNativeSaveDialogOptions(options: ISaveDialogOptions): Electron.SaveDialogOptions {
|
||||
options.defaultUri = options.defaultUri ? URI.file(options.defaultUri.path) : undefined;
|
||||
return {
|
||||
defaultPath: options.defaultUri && options.defaultUri.fsPath,
|
||||
buttonLabel: options.saveLabel,
|
||||
@@ -190,33 +217,34 @@ export class FileDialogService implements IFileDialogService {
|
||||
};
|
||||
}
|
||||
|
||||
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
if (this.shouldUseSimplified(schema)) {
|
||||
if (!options.availableFileSystems) {
|
||||
options.availableFileSystems = [schema]; // by default only allow saving in the own file system
|
||||
}
|
||||
|
||||
return this.saveRemoteResource(options);
|
||||
}
|
||||
|
||||
return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => {
|
||||
if (result) {
|
||||
return URI.file(result);
|
||||
}
|
||||
const result = await this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options));
|
||||
if (result) {
|
||||
return URI.file(result);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
}
|
||||
|
||||
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
|
||||
async showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
|
||||
const schema = this.getFileSystemSchema(options);
|
||||
if (this.shouldUseSimplified(schema)) {
|
||||
if (!options.availableFileSystems) {
|
||||
options.availableFileSystems = [schema]; // by default only allow loading in the own file system
|
||||
}
|
||||
return this.pickRemoteResource(options).then(uri => {
|
||||
return uri ? [uri] : undefined;
|
||||
});
|
||||
|
||||
const uri = await this.pickRemoteResource(options);
|
||||
|
||||
return uri ? [uri] : undefined;
|
||||
}
|
||||
|
||||
const defaultUri = options.defaultUri;
|
||||
@@ -243,27 +271,30 @@ export class FileDialogService implements IFileDialogService {
|
||||
newOptions.properties!.push('multiSelections');
|
||||
}
|
||||
|
||||
return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined);
|
||||
const result = await this.windowService.showOpenDialog(newOptions);
|
||||
|
||||
return result ? result.map(URI.file) : undefined;
|
||||
}
|
||||
|
||||
private pickRemoteResource(options: IOpenDialogOptions): Promise<URI | undefined> {
|
||||
const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog);
|
||||
|
||||
return remoteFileDialog.showOpenDialog(options);
|
||||
}
|
||||
|
||||
private saveRemoteResource(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog);
|
||||
|
||||
return remoteFileDialog.showSaveDialog(options);
|
||||
}
|
||||
|
||||
private getSchemeFilterForWindow() {
|
||||
private getSchemeFilterForWindow(): string {
|
||||
return !this.environmentService.configuration.remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME;
|
||||
}
|
||||
|
||||
private getFileSystemSchema(options: { availableFileSystems?: string[], defaultUri?: URI }): string {
|
||||
return options.availableFileSystems && options.availableFileSystems[0] || options.defaultUri && options.defaultUri.scheme || this.getSchemeFilterForWindow();
|
||||
return options.availableFileSystems && options.availableFileSystems[0] || this.getSchemeFilterForWindow();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isUntitledWorkspace(path: URI, environmentService: IWorkbenchEnvironmentService): boolean {
|
||||
|
||||
@@ -23,11 +23,15 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
|
||||
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 { OpenLocalFileAction, OpenLocalFileFolderAction, OpenLocalFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
|
||||
import { OpenLocalFileCommand, OpenLocalFileFolderCommand, OpenLocalFolderCommand, SaveLocalFileCommand } 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';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface FileQuickPickItem extends IQuickPickItem {
|
||||
uri: URI;
|
||||
@@ -60,6 +64,12 @@ export class RemoteFileDialog {
|
||||
private badPath: string | undefined;
|
||||
private remoteAgentEnvironment: IRemoteAgentEnvironment | null;
|
||||
private separator: string;
|
||||
private onBusyChangeEmitter = new Emitter<boolean>();
|
||||
private updatingPromise: CancelablePromise<void> | undefined;
|
||||
|
||||
protected disposables: IDisposable[] = [
|
||||
this.onBusyChangeEmitter
|
||||
];
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@@ -79,8 +89,19 @@ export class RemoteFileDialog {
|
||||
this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
set busy(busy: boolean) {
|
||||
if (this.filePickBox.busy !== busy) {
|
||||
this.filePickBox.busy = busy;
|
||||
this.onBusyChangeEmitter.fire(busy);
|
||||
}
|
||||
}
|
||||
|
||||
get busy(): boolean {
|
||||
return this.filePickBox.busy;
|
||||
}
|
||||
|
||||
public async showOpenDialog(options: IOpenDialogOptions = {}): Promise<URI | undefined> {
|
||||
this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems);
|
||||
this.scheme = this.getScheme(options.availableFileSystems);
|
||||
this.userHome = await this.getUserHome();
|
||||
const newOptions = await this.getOptions(options);
|
||||
if (!newOptions) {
|
||||
@@ -91,7 +112,7 @@ export class RemoteFileDialog {
|
||||
}
|
||||
|
||||
public async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems);
|
||||
this.scheme = this.getScheme(options.availableFileSystems);
|
||||
this.userHome = await this.getUserHome();
|
||||
this.requiresTrailing = true;
|
||||
const newOptions = await this.getOptions(options, true);
|
||||
@@ -110,9 +131,13 @@ export class RemoteFileDialog {
|
||||
}
|
||||
|
||||
private getOptions(options: ISaveDialogOptions | IOpenDialogOptions, isSave: boolean = false): IOpenDialogOptions | undefined {
|
||||
let defaultUri = options.defaultUri;
|
||||
const filename = (defaultUri && isSave && (resources.dirname(defaultUri).path === '/')) ? resources.basename(defaultUri) : undefined;
|
||||
if (!defaultUri || filename) {
|
||||
let defaultUri: URI | undefined = undefined;
|
||||
let filename: string | undefined = undefined;
|
||||
if (options.defaultUri) {
|
||||
defaultUri = (this.scheme === options.defaultUri.scheme) ? options.defaultUri : undefined;
|
||||
filename = isSave ? resources.basename(options.defaultUri) : undefined;
|
||||
}
|
||||
if (!defaultUri) {
|
||||
defaultUri = this.userHome;
|
||||
if (filename) {
|
||||
defaultUri = resources.joinPath(defaultUri, filename);
|
||||
@@ -132,8 +157,8 @@ export class RemoteFileDialog {
|
||||
return resources.toLocalResource(URI.from({ scheme: this.scheme, path }), this.scheme === Schemas.file ? undefined : this.remoteAuthority);
|
||||
}
|
||||
|
||||
private getScheme(defaultUri: URI | undefined, available: string[] | undefined): string {
|
||||
return defaultUri ? defaultUri.scheme : (available ? available[0] : Schemas.file);
|
||||
private getScheme(available: string[] | undefined): string {
|
||||
return available ? available[0] : Schemas.file;
|
||||
}
|
||||
|
||||
private async getRemoteAgentEnvironment(): Promise<IRemoteAgentEnvironment | null> {
|
||||
@@ -185,15 +210,20 @@ export class RemoteFileDialog {
|
||||
|
||||
return new Promise<URI | undefined>(async (resolve) => {
|
||||
this.filePickBox = this.quickInputService.createQuickPick<FileQuickPickItem>();
|
||||
this.filePickBox.busy = true;
|
||||
this.busy = true;
|
||||
this.filePickBox.matchOnLabel = false;
|
||||
this.filePickBox.autoFocusOnList = false;
|
||||
this.filePickBox.ignoreFocusOut = true;
|
||||
this.filePickBox.ok = true;
|
||||
if (this.options && this.options.availableFileSystems && (this.options.availableFileSystems.length > 1)) {
|
||||
if (this.options && this.options.availableFileSystems && (this.options.availableFileSystems.length > 1) && (this.options.availableFileSystems.indexOf(Schemas.file) > -1)) {
|
||||
this.filePickBox.customButton = true;
|
||||
this.filePickBox.customLabel = nls.localize('remoteFileDialog.local', 'Show Local');
|
||||
const action = this.allowFileSelection ? (this.allowFolderSelection ? OpenLocalFileFolderAction : OpenLocalFileAction) : OpenLocalFolderAction;
|
||||
let action;
|
||||
if (isSave) {
|
||||
action = SaveLocalFileCommand;
|
||||
} else {
|
||||
action = this.allowFileSelection ? (this.allowFolderSelection ? OpenLocalFileFolderCommand : OpenLocalFileCommand) : OpenLocalFolderCommand;
|
||||
}
|
||||
const keybinding = this.keybindingService.lookupKeybinding(action.ID);
|
||||
if (keybinding) {
|
||||
const label = keybinding.getLabel();
|
||||
@@ -203,9 +233,9 @@ export class RemoteFileDialog {
|
||||
}
|
||||
}
|
||||
|
||||
let isResolving = false;
|
||||
let isResolving: number = 0;
|
||||
let isAcceptHandled = false;
|
||||
this.currentFolder = homedir;
|
||||
this.currentFolder = resources.dirname(homedir);
|
||||
this.userEnteredPathSegment = '';
|
||||
this.autoCompletePathSegment = '';
|
||||
|
||||
@@ -215,24 +245,27 @@ export class RemoteFileDialog {
|
||||
this.filePickBox.items = [];
|
||||
|
||||
function doResolve(dialog: RemoteFileDialog, uri: URI | undefined) {
|
||||
if (uri) {
|
||||
uri = resources.removeTrailingPathSeparator(uri);
|
||||
}
|
||||
resolve(uri);
|
||||
dialog.contextKey.set(false);
|
||||
dialog.filePickBox.dispose();
|
||||
dispose(dialog.disposables);
|
||||
}
|
||||
|
||||
this.filePickBox.onDidCustom(() => {
|
||||
if (isAcceptHandled || this.filePickBox.busy) {
|
||||
if (isAcceptHandled || this.busy) {
|
||||
return undefined; // {{SQL CARBON EDIT}} @todo anthonydresser return to return; when we do strict null checks
|
||||
}
|
||||
|
||||
isAcceptHandled = true;
|
||||
isResolving = true;
|
||||
isResolving++;
|
||||
if (this.options.availableFileSystems && (this.options.availableFileSystems.length > 1)) {
|
||||
this.options.availableFileSystems.shift();
|
||||
}
|
||||
this.options.defaultUri = undefined;
|
||||
this.filePickBox.hide();
|
||||
if (this.requiresTrailing) {
|
||||
if (isSave) {
|
||||
return this.fileDialogService.showSaveDialog(this.options).then(result => {
|
||||
doResolve(this, result);
|
||||
});
|
||||
@@ -243,38 +276,56 @@ export class RemoteFileDialog {
|
||||
}
|
||||
});
|
||||
|
||||
this.filePickBox.onDidAccept(_ => {
|
||||
if (isAcceptHandled || this.filePickBox.busy) {
|
||||
function handleAccept(dialog: RemoteFileDialog) {
|
||||
if (dialog.busy) {
|
||||
// Save the accept until the file picker is not busy.
|
||||
dialog.onBusyChangeEmitter.event((busy: boolean) => {
|
||||
if (!busy) {
|
||||
handleAccept(dialog);
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if (isAcceptHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAcceptHandled = true;
|
||||
isResolving = true;
|
||||
this.onDidAccept().then(resolveValue => {
|
||||
isResolving++;
|
||||
dialog.onDidAccept().then(resolveValue => {
|
||||
if (resolveValue) {
|
||||
this.filePickBox.hide();
|
||||
doResolve(this, resolveValue);
|
||||
} else if (this.hidden) {
|
||||
doResolve(this, undefined);
|
||||
dialog.filePickBox.hide();
|
||||
doResolve(dialog, resolveValue);
|
||||
} else if (dialog.hidden) {
|
||||
doResolve(dialog, undefined);
|
||||
} else {
|
||||
isResolving = false;
|
||||
isResolving--;
|
||||
isAcceptHandled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.filePickBox.onDidAccept(_ => {
|
||||
handleAccept(this);
|
||||
});
|
||||
|
||||
this.filePickBox.onDidChangeActive(i => {
|
||||
isAcceptHandled = false;
|
||||
// update input box to match the first selected item
|
||||
if ((i.length === 1) && this.isChangeFromUser()) {
|
||||
if ((i.length === 1) && this.isSelectionChangeFromUser()) {
|
||||
this.filePickBox.validationMessage = undefined;
|
||||
this.setAutoComplete(this.constructFullUserPath(), this.userEnteredPathSegment, i[0], true);
|
||||
const userPath = this.constructFullUserPath();
|
||||
if (!equalsIgnoreCase(this.filePickBox.value.substring(0, userPath.length), userPath)) {
|
||||
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
|
||||
this.insertText(userPath, userPath);
|
||||
}
|
||||
this.setAutoComplete(userPath, this.userEnteredPathSegment, i[0], true);
|
||||
}
|
||||
});
|
||||
|
||||
this.filePickBox.onDidChangeValue(async value => {
|
||||
try {
|
||||
// onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything
|
||||
if (this.isChangeFromUser()) {
|
||||
if (this.isValueChangeFromUser()) {
|
||||
// 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;
|
||||
@@ -288,6 +339,7 @@ export class RemoteFileDialog {
|
||||
}
|
||||
} else {
|
||||
this.filePickBox.activeItems = [];
|
||||
this.userEnteredPathSegment = '';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -296,7 +348,7 @@ export class RemoteFileDialog {
|
||||
});
|
||||
this.filePickBox.onDidHide(() => {
|
||||
this.hidden = true;
|
||||
if (!isResolving) {
|
||||
if (isResolving === 0) {
|
||||
doResolve(this, undefined);
|
||||
}
|
||||
});
|
||||
@@ -309,7 +361,7 @@ export class RemoteFileDialog {
|
||||
} else {
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
|
||||
}
|
||||
this.filePickBox.busy = false;
|
||||
this.busy = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,16 +369,27 @@ export class RemoteFileDialog {
|
||||
return this.badPath && (value.length > this.badPath.length) && equalsIgnoreCase(value.substring(0, this.badPath.length), this.badPath);
|
||||
}
|
||||
|
||||
private isChangeFromUser(): boolean {
|
||||
if (equalsIgnoreCase(this.filePickBox.value, this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment))
|
||||
&& (this.activeItem === (this.filePickBox.activeItems ? this.filePickBox.activeItems[0] : undefined))) {
|
||||
private isValueChangeFromUser(): boolean {
|
||||
if (equalsIgnoreCase(this.filePickBox.value, this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private isSelectionChangeFromUser(): boolean {
|
||||
if (this.activeItem === (this.filePickBox.activeItems ? this.filePickBox.activeItems[0] : undefined)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private constructFullUserPath(): string {
|
||||
return this.pathAppend(this.currentFolder, this.userEnteredPathSegment);
|
||||
const currentFolderPath = this.pathFromUri(this.currentFolder);
|
||||
if (equalsIgnoreCase(this.filePickBox.value.substr(0, this.userEnteredPathSegment.length), this.userEnteredPathSegment) && equalsIgnoreCase(this.filePickBox.value.substr(0, currentFolderPath.length), currentFolderPath)) {
|
||||
return currentFolderPath;
|
||||
} else {
|
||||
return this.pathAppend(this.currentFolder, this.userEnteredPathSegment);
|
||||
}
|
||||
}
|
||||
|
||||
private filePickBoxValue(): URI {
|
||||
@@ -340,14 +403,19 @@ export class RemoteFileDialog {
|
||||
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);
|
||||
let path = resources.joinPath(this.currentFolder, relativePath);
|
||||
const directBasename = resources.basename(directUri);
|
||||
if ((directBasename === '.') || (directBasename === '..')) {
|
||||
path = this.remoteUriFrom(this.pathAppend(path, directBasename));
|
||||
}
|
||||
return resources.hasTrailingPathSeparator(directUri) ? resources.addTrailingPathSeparator(path) : path;
|
||||
} else {
|
||||
return directUri;
|
||||
}
|
||||
}
|
||||
|
||||
private async onDidAccept(): Promise<URI | undefined> {
|
||||
this.filePickBox.busy = true;
|
||||
this.busy = true;
|
||||
if (this.filePickBox.activeItems.length === 1) {
|
||||
const item = this.filePickBox.selectedItems[0];
|
||||
if (item.isFolder) {
|
||||
@@ -357,10 +425,9 @@ export class RemoteFileDialog {
|
||||
// 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);
|
||||
if (startsWithIgnoreCase(newPath, this.filePickBox.value) && (equalsIgnoreCase(item.label, resources.basename(item.uri)))) {
|
||||
this.filePickBox.valueSelection = [this.pathFromUri(this.currentFolder).length, this.filePickBox.value.length];
|
||||
this.insertText(newPath, item.label);
|
||||
} else if ((item.label === '..') && startsWithIgnoreCase(this.filePickBox.value, newPath)) {
|
||||
this.filePickBox.valueSelection = [newPath.length, this.filePickBox.value.length];
|
||||
this.insertText(newPath, '');
|
||||
@@ -368,11 +435,13 @@ export class RemoteFileDialog {
|
||||
await this.updateItems(item.uri, true);
|
||||
}
|
||||
}
|
||||
this.filePickBox.busy = false;
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
}
|
||||
} else {
|
||||
// If the items have updated, don't try to resolve
|
||||
if ((await this.tryUpdateItems(this.filePickBox.value, this.filePickBoxValue())) !== UpdateResult.NotUpdated) {
|
||||
this.filePickBox.busy = false;
|
||||
return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check
|
||||
}
|
||||
}
|
||||
@@ -388,10 +457,10 @@ export class RemoteFileDialog {
|
||||
resolveValue = this.addPostfix(resolveValue);
|
||||
}
|
||||
if (await this.validate(resolveValue)) {
|
||||
this.filePickBox.busy = false;
|
||||
this.busy = false;
|
||||
return resolveValue;
|
||||
}
|
||||
this.filePickBox.busy = false;
|
||||
this.busy = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -429,7 +498,7 @@ export class RemoteFileDialog {
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
if (statWithoutTrailing && statWithoutTrailing.isDirectory && (resources.basename(valueUri) !== '.')) {
|
||||
if (statWithoutTrailing && statWithoutTrailing.isDirectory) {
|
||||
await this.updateItems(inputUriDirname, false, resources.basename(valueUri));
|
||||
this.badPath = undefined;
|
||||
return UpdateResult.Updated;
|
||||
@@ -447,11 +516,13 @@ export class RemoteFileDialog {
|
||||
const userPath = this.constructFullUserPath();
|
||||
if (equalsIgnoreCase(userPath, value.substring(0, userPath.length))) {
|
||||
let hasMatch = false;
|
||||
for (let i = 0; i < this.filePickBox.items.length; i++) {
|
||||
const item = <FileQuickPickItem>this.filePickBox.items[i];
|
||||
if (this.setAutoComplete(value, inputBasename, item)) {
|
||||
hasMatch = true;
|
||||
break;
|
||||
if (inputBasename.length > this.userEnteredPathSegment.length) {
|
||||
for (let i = 0; i < this.filePickBox.items.length; i++) {
|
||||
const item = <FileQuickPickItem>this.filePickBox.items[i];
|
||||
if (this.setAutoComplete(value, inputBasename, item)) {
|
||||
hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasMatch) {
|
||||
@@ -460,17 +531,13 @@ export class RemoteFileDialog {
|
||||
this.filePickBox.activeItems = [];
|
||||
}
|
||||
} else {
|
||||
if (!equalsIgnoreCase(inputBasename, resources.basename(this.currentFolder))) {
|
||||
this.userEnteredPathSegment = inputBasename;
|
||||
} else {
|
||||
this.userEnteredPathSegment = '';
|
||||
}
|
||||
this.userEnteredPathSegment = inputBasename;
|
||||
this.autoCompletePathSegment = '';
|
||||
}
|
||||
}
|
||||
|
||||
private setAutoComplete(startingValue: string, startingBasename: string, quickPickItem: FileQuickPickItem, force: boolean = false): boolean {
|
||||
if (this.filePickBox.busy) {
|
||||
if (this.busy) {
|
||||
// We're in the middle of something else. Doing an auto complete now can result jumbled or incorrect autocompletes.
|
||||
this.userEnteredPathSegment = startingBasename;
|
||||
this.autoCompletePathSegment = '';
|
||||
@@ -480,7 +547,7 @@ export class RemoteFileDialog {
|
||||
// Either force the autocomplete, or the old value should be one smaller than the new value and match the new value.
|
||||
if (itemBasename === '..') {
|
||||
// Don't match on the up directory item ever.
|
||||
this.userEnteredPathSegment = startingValue;
|
||||
this.userEnteredPathSegment = '';
|
||||
this.autoCompletePathSegment = '';
|
||||
this.activeItem = quickPickItem;
|
||||
if (force) {
|
||||
@@ -494,9 +561,6 @@ export class RemoteFileDialog {
|
||||
// Changing the active items will trigger the onDidActiveItemsChanged. Clear the autocomplete first, then set it after.
|
||||
this.autoCompletePathSegment = '';
|
||||
this.filePickBox.activeItems = [quickPickItem];
|
||||
this.autoCompletePathSegment = this.trimTrailingSlash(itemBasename.substr(startingBasename.length));
|
||||
this.insertText(startingValue + this.autoCompletePathSegment, this.autoCompletePathSegment);
|
||||
this.filePickBox.valueSelection = [startingValue.length, this.filePickBox.value.length];
|
||||
return true;
|
||||
} else if (force && (!equalsIgnoreCase(quickPickItem.label, (this.userEnteredPathSegment + this.autoCompletePathSegment)))) {
|
||||
this.userEnteredPathSegment = '';
|
||||
@@ -641,30 +705,46 @@ export class RemoteFileDialog {
|
||||
}
|
||||
|
||||
private async updateItems(newFolder: URI, force: boolean = false, trailing?: string) {
|
||||
this.filePickBox.busy = true;
|
||||
this.busy = true;
|
||||
this.userEnteredPathSegment = trailing ? trailing : '';
|
||||
this.autoCompletePathSegment = '';
|
||||
const newValue = trailing ? this.pathFromUri(resources.joinPath(newFolder, trailing)) : this.pathFromUri(newFolder, true);
|
||||
const newValue = trailing ? this.pathAppend(newFolder, trailing) : this.pathFromUri(newFolder, true);
|
||||
this.currentFolder = resources.addTrailingPathSeparator(newFolder, this.separator);
|
||||
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 (force && trailing) {
|
||||
// Keep the cursor position in front of the save as name.
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - trailing.length];
|
||||
} else if (!trailing) {
|
||||
// If there is trailing, we don't move the cursor. If there is no trailing, cursor goes at the end.
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
|
||||
}
|
||||
this.filePickBox.busy = false;
|
||||
|
||||
const updatingPromise = createCancelablePromise(async token => {
|
||||
return this.createItems(this.currentFolder, token).then(items => {
|
||||
if (token.isCancellationRequested) {
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
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 matche directory.
|
||||
if (!equalsIgnoreCase(this.filePickBox.value, newValue) && force) {
|
||||
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
|
||||
this.insertText(newValue, newValue);
|
||||
}
|
||||
if (force && trailing) {
|
||||
// Keep the cursor position in front of the save as name.
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - trailing.length];
|
||||
} else if (!trailing) {
|
||||
// If there is trailing, we don't move the cursor. If there is no trailing, cursor goes at the end.
|
||||
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
|
||||
}
|
||||
this.busy = false;
|
||||
this.updatingPromise = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
if (this.updatingPromise !== undefined) {
|
||||
this.updatingPromise.cancel();
|
||||
}
|
||||
this.updatingPromise = updatingPromise;
|
||||
|
||||
return updatingPromise;
|
||||
}
|
||||
|
||||
private pathFromUri(uri: URI, endWithSeparator: boolean = false): string {
|
||||
@@ -683,7 +763,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.separator + additional;
|
||||
} else {
|
||||
return this.pathFromUri(resources.joinPath(uri, additional));
|
||||
}
|
||||
@@ -716,14 +796,14 @@ export class RemoteFileDialog {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async createItems(currentFolder: URI): Promise<FileQuickPickItem[]> {
|
||||
private async createItems(currentFolder: URI, token: CancellationToken): Promise<FileQuickPickItem[]> {
|
||||
const result: FileQuickPickItem[] = [];
|
||||
|
||||
const backDir = this.createBackItem(currentFolder);
|
||||
try {
|
||||
const folder = await this.fileService.resolve(currentFolder);
|
||||
const fileNames = folder.children ? folder.children.map(child => child.name) : [];
|
||||
const items = await Promise.all(fileNames.map(fileName => this.createItem(fileName, currentFolder)));
|
||||
const items = await Promise.all(fileNames.map(fileName => this.createItem(fileName, currentFolder, token)));
|
||||
for (let item of items) {
|
||||
if (item) {
|
||||
result.push(item);
|
||||
@@ -733,6 +813,9 @@ export class RemoteFileDialog {
|
||||
// ignore
|
||||
console.log(e);
|
||||
}
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
const sorted = result.sort((i1, i2) => {
|
||||
if (i1.isFolder !== i2.isFolder) {
|
||||
return i1.isFolder ? -1 : 1;
|
||||
@@ -763,7 +846,10 @@ export class RemoteFileDialog {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async createItem(filename: string, parent: URI): Promise<FileQuickPickItem | undefined> {
|
||||
private async createItem(filename: string, parent: URI, token: CancellationToken): Promise<FileQuickPickItem | undefined> {
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
let fullPath = resources.joinPath(parent, filename);
|
||||
try {
|
||||
const stat = await this.fileService.resolve(fullPath);
|
||||
|
||||
@@ -21,13 +21,13 @@ import { localize } from 'vs/nls';
|
||||
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IResourceEditor, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
//{{SQL CARBON EDIT}}
|
||||
import { convertEditorInput, getFileMode } from 'sql/workbench/common/customInputConverter';
|
||||
@@ -44,16 +44,16 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
//#region events
|
||||
|
||||
private readonly _onDidActiveEditorChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
get onDidActiveEditorChange(): Event<void> { return this._onDidActiveEditorChange.event; }
|
||||
readonly onDidActiveEditorChange: Event<void> = this._onDidActiveEditorChange.event;
|
||||
|
||||
private readonly _onDidVisibleEditorsChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
get onDidVisibleEditorsChange(): Event<void> { return this._onDidVisibleEditorsChange.event; }
|
||||
readonly onDidVisibleEditorsChange: Event<void> = this._onDidVisibleEditorsChange.event;
|
||||
|
||||
private readonly _onDidCloseEditor: Emitter<IEditorCloseEvent> = this._register(new Emitter<IEditorCloseEvent>());
|
||||
get onDidCloseEditor(): Event<IEditorCloseEvent> { return this._onDidCloseEditor.event; }
|
||||
readonly onDidCloseEditor: Event<IEditorCloseEvent> = this._onDidCloseEditor.event;
|
||||
|
||||
private readonly _onDidOpenEditorFail: Emitter<IEditorIdentifier> = this._register(new Emitter<IEditorIdentifier>());
|
||||
get onDidOpenEditorFail(): Event<IEditorIdentifier> { return this._onDidOpenEditorFail.event; }
|
||||
readonly onDidOpenEditorFail: Event<IEditorIdentifier> = this._onDidOpenEditorFail.event;
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -122,29 +122,29 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
private registerGroupListeners(group: IEditorGroupView): void {
|
||||
const groupDisposeables: IDisposable[] = [];
|
||||
const groupDisposables = new DisposableStore();
|
||||
|
||||
groupDisposeables.push(group.onDidGroupChange(e => {
|
||||
groupDisposables.add(group.onDidGroupChange(e => {
|
||||
if (e.kind === GroupChangeKind.EDITOR_ACTIVE) {
|
||||
this.handleActiveEditorChange(group);
|
||||
this._onDidVisibleEditorsChange.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
groupDisposeables.push(group.onDidCloseEditor(event => {
|
||||
groupDisposables.add(group.onDidCloseEditor(event => {
|
||||
this._onDidCloseEditor.fire(event);
|
||||
}));
|
||||
|
||||
groupDisposeables.push(group.onWillOpenEditor(event => {
|
||||
groupDisposables.add(group.onWillOpenEditor(event => {
|
||||
this.onGroupWillOpenEditor(group, event);
|
||||
}));
|
||||
|
||||
groupDisposeables.push(group.onDidOpenEditorFail(editor => {
|
||||
groupDisposables.add(group.onDidOpenEditorFail(editor => {
|
||||
this._onDidOpenEditorFail.fire({ editor, groupId: group.id });
|
||||
}));
|
||||
|
||||
Event.once(group.onWillDispose)(() => {
|
||||
dispose(groupDisposeables);
|
||||
dispose(groupDisposables);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
// Side by Side Support
|
||||
const resourceSideBySideInput = <IResourceSideBySideInput>input;
|
||||
const resourceSideBySideInput = input as IResourceSideBySideInput;
|
||||
if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) {
|
||||
const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource, forceFile: resourceSideBySideInput.forceFile });
|
||||
const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource, forceFile: resourceSideBySideInput.forceFile });
|
||||
@@ -521,17 +521,17 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
// Diff Editor Support
|
||||
const resourceDiffInput = <IResourceDiffInput>input;
|
||||
const resourceDiffInput = input as IResourceDiffInput;
|
||||
if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) {
|
||||
const leftInput = this.createInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile });
|
||||
const rightInput = this.createInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile });
|
||||
const label = resourceDiffInput.label || localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput), this.toDiffLabel(rightInput));
|
||||
|
||||
return new DiffEditorInput(label, withUndefinedAsNull(resourceDiffInput.description), leftInput, rightInput);
|
||||
return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput);
|
||||
}
|
||||
|
||||
// Untitled file support
|
||||
const untitledInput = <IUntitledResourceInput>input;
|
||||
const untitledInput = input as IUntitledResourceInput;
|
||||
if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Need to get mode for QueryEditor and Notebook
|
||||
@@ -540,7 +540,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
// Resource Editor Support
|
||||
const resourceInput = <IResourceInput>input;
|
||||
const resourceInput = input as IResourceInput;
|
||||
if (resourceInput.resource instanceof URI) {
|
||||
let label = resourceInput.label;
|
||||
if (!label && resourceInput.resource.scheme !== Schemas.data) {
|
||||
@@ -658,18 +658,17 @@ export class DelegatingEditorService extends EditorService {
|
||||
this.editorOpenHandler = handler;
|
||||
}
|
||||
|
||||
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor | null> {
|
||||
protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor | null> {
|
||||
if (!this.editorOpenHandler) {
|
||||
return super.doOpenEditor(group, editor, options);
|
||||
}
|
||||
|
||||
return this.editorOpenHandler(group, editor, options).then(control => {
|
||||
if (control) {
|
||||
return control; // the opening was handled, so return early
|
||||
}
|
||||
const control = await this.editorOpenHandler(group, editor, options);
|
||||
if (control) {
|
||||
return control; // the opening was handled, so return early
|
||||
}
|
||||
|
||||
return super.doOpenEditor(group, editor, options);
|
||||
});
|
||||
return super.doOpenEditor(group, editor, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,12 +232,12 @@ export interface IEditorGroupsService {
|
||||
/**
|
||||
* Returns the size of a group.
|
||||
*/
|
||||
getSize(group: IEditorGroup | GroupIdentifier): number;
|
||||
getSize(group: IEditorGroup | GroupIdentifier): { width: number, height: number };
|
||||
|
||||
/**
|
||||
* Sets the size of a group.
|
||||
*/
|
||||
setSize(group: IEditorGroup | GroupIdentifier, size: number): void;
|
||||
setSize(group: IEditorGroup | GroupIdentifier, size: { width: number, height: number }): void;
|
||||
|
||||
/**
|
||||
* Arrange all groups according to the provided arrangement.
|
||||
@@ -411,7 +411,7 @@ export interface IEditorGroup {
|
||||
/**
|
||||
* Returns the editor at a specific index of the group.
|
||||
*/
|
||||
getEditor(index: number): IEditorInput | null;
|
||||
getEditor(index: number): IEditorInput | undefined;
|
||||
|
||||
/**
|
||||
* Get all editors that are currently opened in the group optionally
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { workbenchInstantiationService, TestStorageService, NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService';
|
||||
@@ -31,6 +31,7 @@ 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';
|
||||
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
|
||||
|
||||
// {{SQL CARBON EDIT}} - Disable editor tests
|
||||
/*
|
||||
@@ -84,7 +85,7 @@ class FileServiceProvider extends Disposable {
|
||||
*/suite('EditorService', () => {/*
|
||||
|
||||
function registerTestEditorInput(): void {
|
||||
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), new SyncDescriptor(TestEditorInput));
|
||||
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), [new SyncDescriptor(TestEditorInput)]);
|
||||
}
|
||||
|
||||
registerTestEditorInput();
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IWindowConfiguration, IPath, IPathsToWaitFor } from 'vs/platform/windows/common/windows';
|
||||
import { IEnvironmentService, IExtensionHostDebugParams, IDebugParams, BACKUPS } from 'vs/platform/environment/common/environment';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { ExportData } from 'vs/base/common/performance';
|
||||
import { LogLevel } from 'vs/platform/log/common/log';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export class BrowserWindowConfiguration implements IWindowConfiguration {
|
||||
|
||||
_: any[];
|
||||
|
||||
machineId: string;
|
||||
windowId: number;
|
||||
logLevel: LogLevel;
|
||||
|
||||
mainPid: number;
|
||||
|
||||
appRoot: string;
|
||||
execPath: string;
|
||||
isInitialStartup?: boolean;
|
||||
|
||||
userEnv: IProcessEnvironment;
|
||||
nodeCachedDataDir?: string;
|
||||
|
||||
backupPath?: string;
|
||||
|
||||
workspace?: IWorkspaceIdentifier;
|
||||
folderUri?: ISingleFolderWorkspaceIdentifier;
|
||||
|
||||
remoteAuthority: string;
|
||||
|
||||
zoomLevel?: number;
|
||||
fullscreen?: boolean;
|
||||
maximized?: boolean;
|
||||
highContrast?: boolean;
|
||||
frameless?: boolean;
|
||||
accessibilitySupport?: boolean;
|
||||
partsSplashPath?: string;
|
||||
|
||||
perfStartTime?: number;
|
||||
perfAppReady?: number;
|
||||
perfWindowLoadTime?: number;
|
||||
perfEntries: ExportData;
|
||||
|
||||
filesToOpenOrCreate?: IPath[];
|
||||
filesToDiff?: IPath[];
|
||||
filesToWait?: IPathsToWaitFor;
|
||||
termProgram?: string;
|
||||
}
|
||||
|
||||
export interface IBrowserWindowConfiguration {
|
||||
workspaceId: string;
|
||||
remoteAuthority?: string;
|
||||
webviewEndpoint?: string;
|
||||
}
|
||||
|
||||
export class BrowserWorkbenchEnvironmentService implements IEnvironmentService {
|
||||
_serviceBrand: ServiceIdentifier<IEnvironmentService>;
|
||||
|
||||
readonly configuration: IWindowConfiguration = new BrowserWindowConfiguration();
|
||||
|
||||
constructor(configuration: IBrowserWindowConfiguration) {
|
||||
this.args = { _: [] };
|
||||
this.appRoot = '/web/';
|
||||
this.appNameLong = 'Visual Studio Code - Web';
|
||||
|
||||
this.configuration.remoteAuthority = configuration.remoteAuthority;
|
||||
this.userRoamingDataHome = URI.file('/User').with({ scheme: Schemas.userData });
|
||||
this.settingsResource = joinPath(this.userRoamingDataHome, 'settings.json');
|
||||
this.keybindingsResource = joinPath(this.userRoamingDataHome, 'keybindings.json');
|
||||
this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json');
|
||||
this.localeResource = joinPath(this.userRoamingDataHome, 'locale.json');
|
||||
this.backupHome = joinPath(this.userRoamingDataHome, BACKUPS);
|
||||
this.configuration.backupWorkspaceResource = joinPath(this.backupHome, configuration.workspaceId);
|
||||
|
||||
this.logsPath = '/web/logs';
|
||||
|
||||
this.debugExtensionHost = {
|
||||
port: null,
|
||||
break: false
|
||||
};
|
||||
|
||||
this.webviewEndpoint = configuration.webviewEndpoint;
|
||||
this.untitledWorkspacesHome = URI.from({ scheme: Schemas.untitled, path: 'Workspaces' });
|
||||
}
|
||||
|
||||
untitledWorkspacesHome: URI;
|
||||
extensionTestsLocationURI?: URI;
|
||||
args: any;
|
||||
execPath: string;
|
||||
cliPath: string;
|
||||
appRoot: string;
|
||||
userHome: string;
|
||||
userDataPath: string;
|
||||
appNameLong: string;
|
||||
appQuality?: string;
|
||||
appSettingsHome: URI;
|
||||
userRoamingDataHome: URI;
|
||||
settingsResource: URI;
|
||||
keybindingsResource: URI;
|
||||
keyboardLayoutResource: URI;
|
||||
localeResource: URI;
|
||||
machineSettingsHome: URI;
|
||||
machineSettingsResource: URI;
|
||||
globalStorageHome: string;
|
||||
workspaceStorageHome: string;
|
||||
backupHome: URI;
|
||||
backupWorkspacesPath: string;
|
||||
workspacesHome: string;
|
||||
isExtensionDevelopment: boolean;
|
||||
disableExtensions: boolean | string[];
|
||||
builtinExtensionsPath: string;
|
||||
extensionsPath: string;
|
||||
extensionDevelopmentLocationURI?: URI[];
|
||||
extensionTestsPath?: string;
|
||||
debugExtensionHost: IExtensionHostDebugParams;
|
||||
debugSearch: IDebugParams;
|
||||
logExtensionHostCommunication: boolean;
|
||||
isBuilt: boolean;
|
||||
wait: boolean;
|
||||
status: boolean;
|
||||
log?: string;
|
||||
logsPath: string;
|
||||
verbose: boolean;
|
||||
skipGettingStarted: boolean;
|
||||
skipReleaseNotes: boolean;
|
||||
skipAddToRecentlyOpened: boolean;
|
||||
mainIPCHandle: string;
|
||||
sharedIPCHandle: string;
|
||||
nodeCachedDataDir?: string;
|
||||
installSourcePath: string;
|
||||
disableUpdates: boolean;
|
||||
disableCrashReporter: boolean;
|
||||
driverHandle?: string;
|
||||
driverVerbose: boolean;
|
||||
webviewEndpoint?: string;
|
||||
|
||||
get webviewResourceRoot(): string {
|
||||
return this.webviewEndpoint ? this.webviewEndpoint + '/vscode-resource{{resource}}' : 'vscode-resource:{{resource}}';
|
||||
}
|
||||
|
||||
get webviewCspSource(): string {
|
||||
return this.webviewEndpoint ? this.webviewEndpoint : 'vscode-resource:';
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export const IWorkbenchEnvironmentService = createDecorator<IWorkbenchEnvironmentService>('environmentService');
|
||||
|
||||
export interface IWorkbenchEnvironmentService extends IEnvironmentService {
|
||||
_serviceBrand: any;
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IEnvironmentService>;
|
||||
|
||||
readonly configuration: IWindowConfiguration;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup';
|
||||
|
||||
export class WorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService {
|
||||
|
||||
@@ -16,9 +20,13 @@ export class WorkbenchEnvironmentService extends EnvironmentService implements I
|
||||
execPath: string
|
||||
) {
|
||||
super(_configuration, execPath);
|
||||
this._configuration.backupWorkspaceResource = this._configuration.backupPath ? toBackupWorkspaceResource(this._configuration.backupPath, this) : undefined;
|
||||
}
|
||||
|
||||
get configuration(): IWindowConfiguration {
|
||||
return this._configuration;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
|
||||
const DISABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/disabled';
|
||||
const ENABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/enabled';
|
||||
@@ -36,6 +37,7 @@ export class ExtensionEnablementService extends Disposable implements IExtension
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
this.storageManger = this._register(new StorageManager(storageService));
|
||||
@@ -137,7 +139,7 @@ export class ExtensionEnablementService extends Disposable implements IExtension
|
||||
return disabledExtensions.some(id => areSameExtensions({ id }, extension.identifier));
|
||||
}
|
||||
if (this.environmentService.configuration.remoteAuthority) {
|
||||
const server = isUIExtension(extension.manifest, this.configurationService) ? this.extensionManagementServerService.localExtensionManagementServer : this.extensionManagementServerService.remoteExtensionManagementServer;
|
||||
const server = isUIExtension(extension.manifest, this.productService, this.configurationService) ? this.extensionManagementServerService.localExtensionManagementServer : this.extensionManagementServerService.remoteExtensionManagementServer;
|
||||
return this.extensionManagementServerService.getExtensionManagementServer(extension.location) !== server;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { IExtensionContributions, ExtensionType, IExtension } from 'vs/platform/
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ProductService } from 'vs/platform/product/node/productService';
|
||||
|
||||
function storageService(instantiationService: TestInstantiationService): IStorageService {
|
||||
let service = instantiationService.get(IStorageService);
|
||||
@@ -38,7 +39,9 @@ export class TestExtensionEnablementService extends ExtensionEnablementService {
|
||||
instantiationService.get(IWorkbenchEnvironmentService) || instantiationService.stub(IWorkbenchEnvironmentService, { configuration: Object.create(null) } as IWorkbenchEnvironmentService),
|
||||
instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService,
|
||||
{ onDidInstallExtension: new Emitter<DidInstallExtensionEvent>().event, onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event } as IExtensionManagementService),
|
||||
instantiationService.get(IConfigurationService), instantiationService.get(IExtensionManagementServerService));
|
||||
instantiationService.get(IConfigurationService), instantiationService.get(IExtensionManagementServerService),
|
||||
new ProductService()
|
||||
);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
|
||||
101
src/vs/workbench/services/extensions/browser/extensionService.ts
Normal file
101
src/vs/workbench/services/extensions/browser/extensionService.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { AbstractExtensionService } from 'vs/workbench/services/extensions/common/abstractExtensionService';
|
||||
import { browserWebSocketFactory } from 'vs/platform/remote/browser/browserWebSocketFactory';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
|
||||
import { RemoteExtensionHostClient, IInitDataProvider } from 'vs/workbench/services/extensions/common/remoteExtensionHostClient';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
|
||||
|
||||
private _remoteExtensionsEnvironmentData: IRemoteAgentEnvironment | null;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService extensionEnablementService: IExtensionEnablementService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IProductService productService: IProductService,
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
) {
|
||||
super(
|
||||
instantiationService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
telemetryService,
|
||||
extensionEnablementService,
|
||||
fileService,
|
||||
productService,
|
||||
);
|
||||
|
||||
this._remoteExtensionsEnvironmentData = null;
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private _createProvider(remoteAuthority: string): IInitDataProvider {
|
||||
return {
|
||||
remoteAuthority: remoteAuthority,
|
||||
getInitData: () => {
|
||||
return this.whenInstalledExtensionsRegistered().then(() => {
|
||||
return this._remoteExtensionsEnvironmentData!;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected _createExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] {
|
||||
const result: ExtensionHostProcessManager[] = [];
|
||||
|
||||
const remoteAgentConnection = this._remoteAgentService.getConnection()!;
|
||||
const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority), browserWebSocketFactory);
|
||||
const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents);
|
||||
result.push(remoteExtHostProcessManager);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async _scanAndHandleExtensions(): Promise<void> {
|
||||
// fetch the remote environment
|
||||
const remoteEnv = (await this._remoteAgentService.getEnvironment())!;
|
||||
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(remoteEnv.extensions);
|
||||
|
||||
// remove disabled extensions
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension));
|
||||
|
||||
// save for remote extension's init data
|
||||
this._remoteExtensionsEnvironmentData = remoteEnv;
|
||||
|
||||
// this._handleExtensionPoints((<IExtensionDescription[]>[]).concat(remoteEnv.extensions).concat(localExtensions));
|
||||
const result = this._registry.deltaExtensions(remoteEnv.extensions, []);
|
||||
if (result.removedDueToLooping.length > 0) {
|
||||
this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', ')));
|
||||
}
|
||||
|
||||
this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions());
|
||||
}
|
||||
|
||||
public _onExtensionHostExit(code: number): void {
|
||||
console.log(`vscode:exit`, code);
|
||||
// ipc.send('vscode:exit', code);
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IExtensionService, ExtensionService);
|
||||
@@ -0,0 +1,498 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as perf from 'vs/base/common/performance';
|
||||
import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
|
||||
import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export abstract class AbstractExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
protected readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event;
|
||||
|
||||
protected readonly _onDidChangeExtensionsStatus: Emitter<ExtensionIdentifier[]> = this._register(new Emitter<ExtensionIdentifier[]>());
|
||||
public readonly onDidChangeExtensionsStatus: Event<ExtensionIdentifier[]> = this._onDidChangeExtensionsStatus.event;
|
||||
|
||||
protected readonly _onDidChangeExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeExtensions: Event<void> = this._onDidChangeExtensions.event;
|
||||
|
||||
protected readonly _onWillActivateByEvent = this._register(new Emitter<IWillActivateEvent>());
|
||||
public readonly onWillActivateByEvent: Event<IWillActivateEvent> = this._onWillActivateByEvent.event;
|
||||
|
||||
protected readonly _onDidChangeResponsiveChange = this._register(new Emitter<IResponsiveStateChangeEvent>());
|
||||
public readonly onDidChangeResponsiveChange: Event<IResponsiveStateChangeEvent> = this._onDidChangeResponsiveChange.event;
|
||||
|
||||
protected readonly _registry: ExtensionDescriptionRegistry;
|
||||
private readonly _installedExtensionsReady: Barrier;
|
||||
protected readonly _isDev: boolean;
|
||||
private readonly _extensionsMessages: Map<string, IMessage[]>;
|
||||
protected readonly _allRequestedActivateEvents = new Set<string>();
|
||||
private readonly _proposedApiController: ProposedApiController;
|
||||
private readonly _isExtensionDevHost: boolean;
|
||||
protected readonly _isExtensionDevTestFromCli: boolean;
|
||||
|
||||
// --- Members used per extension host process
|
||||
protected _extensionHostProcessManagers: ExtensionHostProcessManager[];
|
||||
protected _extensionHostActiveExtensions: Map<string, ExtensionIdentifier>;
|
||||
private _extensionHostProcessActivationTimes: Map<string, ActivationTimes>;
|
||||
private _extensionHostExtensionRuntimeErrors: Map<string, Error[]>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
|
||||
@INotificationService protected readonly _notificationService: INotificationService,
|
||||
@IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@ITelemetryService protected readonly _telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService protected readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IFileService protected readonly _fileService: IFileService,
|
||||
@IProductService protected readonly _productService: IProductService
|
||||
) {
|
||||
super();
|
||||
|
||||
// help the file service to activate providers by activating extensions by file system event
|
||||
this._register(this._fileService.onWillActivateFileSystemProvider(e => {
|
||||
e.join(this.activateByEvent(`onFileSystem:${e.scheme}`));
|
||||
}));
|
||||
|
||||
this._registry = new ExtensionDescriptionRegistry([]);
|
||||
this._installedExtensionsReady = new Barrier();
|
||||
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
|
||||
this._extensionsMessages = new Map<string, IMessage[]>();
|
||||
this._proposedApiController = new ProposedApiController(this._environmentService, this._productService);
|
||||
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
const devOpts = parseExtensionDevOptions(this._environmentService);
|
||||
this._isExtensionDevHost = devOpts.isExtensionDevHost;
|
||||
this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;
|
||||
}
|
||||
|
||||
protected async _initialize(): Promise<void> {
|
||||
perf.mark('willLoadExtensions');
|
||||
this._startExtensionHostProcess(true, []);
|
||||
this.whenInstalledExtensionsRegistered().then(() => perf.mark('didLoadExtensions'));
|
||||
await this._scanAndHandleExtensions();
|
||||
this._releaseBarrier();
|
||||
}
|
||||
|
||||
private _releaseBarrier(): void {
|
||||
perf.mark('extensionHostReady');
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(undefined);
|
||||
this._onDidChangeExtensionsStatus.fire(this._registry.getAllExtensionDescriptions().map(e => e.identifier));
|
||||
}
|
||||
|
||||
private _stopExtensionHostProcess(): void {
|
||||
let previouslyActivatedExtensionIds: ExtensionIdentifier[] = [];
|
||||
this._extensionHostActiveExtensions.forEach((value) => {
|
||||
previouslyActivatedExtensionIds.push(value);
|
||||
});
|
||||
|
||||
for (const manager of this._extensionHostProcessManagers) {
|
||||
manager.dispose();
|
||||
}
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
if (previouslyActivatedExtensionIds.length > 0) {
|
||||
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
|
||||
}
|
||||
}
|
||||
|
||||
private _startExtensionHostProcess(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
const processManagers = this._createExtensionHosts(isInitialStart, initialActivationEvents);
|
||||
processManagers.forEach((processManager) => {
|
||||
processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal));
|
||||
processManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); });
|
||||
this._extensionHostProcessManagers.push(processManager);
|
||||
});
|
||||
}
|
||||
|
||||
private _onExtensionHostCrashOrExit(extensionHost: ExtensionHostProcessManager, code: number, signal: string | null): void {
|
||||
|
||||
// Unexpected termination
|
||||
if (!this._isExtensionDevHost) {
|
||||
this._onExtensionHostCrashed(extensionHost, code, signal);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onExtensionHostExit(code);
|
||||
}
|
||||
|
||||
protected _onExtensionHostCrashed(extensionHost: ExtensionHostProcessManager, code: number, signal: string | null): void {
|
||||
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
|
||||
this._stopExtensionHostProcess();
|
||||
}
|
||||
|
||||
//#region IExtensionService
|
||||
|
||||
public canAddExtension(extension: IExtensionDescription): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public canRemoveExtension(extension: IExtensionDescription): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public restartExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
this._startExtensionHostProcess(false, Array.from(this._allRequestedActivateEvents.keys()));
|
||||
}
|
||||
|
||||
public startExtensionHost(): void {
|
||||
this._startExtensionHostProcess(false, Array.from(this._allRequestedActivateEvents.keys()));
|
||||
}
|
||||
|
||||
public stopExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._installedExtensionsReady.isOpen()) {
|
||||
// Extensions have been scanned and interpreted
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents.add(activationEvent);
|
||||
|
||||
if (!this._registry.containsActivationEvent(activationEvent)) {
|
||||
// There is no extension that is interested in this activation event
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
|
||||
return this._activateByEvent(activationEvent);
|
||||
} else {
|
||||
// Extensions have not been scanned yet.
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents.add(activationEvent);
|
||||
|
||||
return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent));
|
||||
}
|
||||
}
|
||||
|
||||
private _activateByEvent(activationEvent: string): Promise<void> {
|
||||
const result = Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent))
|
||||
).then(() => { });
|
||||
this._onWillActivateByEvent.fire({
|
||||
event: activationEvent,
|
||||
activation: result
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public whenInstalledExtensionsRegistered(): Promise<boolean> {
|
||||
return this._installedExtensionsReady.wait();
|
||||
}
|
||||
|
||||
public getExtensions(): Promise<IExtensionDescription[]> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this._registry.getAllExtensionDescriptions();
|
||||
});
|
||||
}
|
||||
|
||||
public getExtension(id: string): Promise<IExtensionDescription | undefined> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this._registry.getExtensionDescription(id);
|
||||
});
|
||||
}
|
||||
|
||||
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): Promise<ExtensionPointContribution<T>[]> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
const availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
|
||||
const result: ExtensionPointContribution<T>[] = [];
|
||||
for (const desc of availableExtensions) {
|
||||
if (desc.contributes && hasOwnProperty.call(desc.contributes, extPoint.name)) {
|
||||
result.push(new ExtensionPointContribution<T>(desc, desc.contributes[extPoint.name])); // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
|
||||
let result: { [id: string]: IExtensionsStatus; } = Object.create(null);
|
||||
if (this._registry) {
|
||||
const extensions = this._registry.getAllExtensionDescriptions();
|
||||
for (const extension of extensions) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extension.identifier);
|
||||
result[extension.identifier.value] = {
|
||||
messages: this._extensionsMessages.get(extensionKey) || [],
|
||||
activationTimes: this._extensionHostProcessActivationTimes.get(extensionKey),
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors.get(extensionKey) || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getInspectPort(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// --- impl
|
||||
|
||||
protected _checkEnableProposedApi(extensions: IExtensionDescription[]): void {
|
||||
for (let extension of extensions) {
|
||||
this._proposedApiController.updateEnableProposedApi(extension);
|
||||
}
|
||||
}
|
||||
|
||||
private _isExtensionUnderDevelopment(extension: IExtensionDescription): boolean {
|
||||
if (this._environmentService.isExtensionDevelopment) {
|
||||
const extDevLocs = this._environmentService.extensionDevelopmentLocationURI;
|
||||
if (extDevLocs) {
|
||||
const extLocation = extension.extensionLocation;
|
||||
for (let p of extDevLocs) {
|
||||
if (isEqualOrParent(extLocation, p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected _isEnabled(extension: IExtensionDescription): boolean {
|
||||
if (this._isExtensionUnderDevelopment(extension)) {
|
||||
// Never disable extensions under development
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ExtensionIdentifier.equals(extension.identifier, BetterMergeId)) {
|
||||
// Check if this is the better merge extension which was migrated to a built-in extension
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._extensionEnablementService.isEnabled(toExtension(extension));
|
||||
}
|
||||
|
||||
protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void {
|
||||
const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null);
|
||||
for (let extensionDescription of affectedExtensions) {
|
||||
if (extensionDescription.contributes) {
|
||||
for (let extPointName in extensionDescription.contributes) {
|
||||
if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) {
|
||||
affectedExtensionPoints[extPointName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
const availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
const extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
for (const extensionPoint of extensionPoints) {
|
||||
if (affectedExtensionPoints[extensionPoint.name]) {
|
||||
AbstractExtensionService._handleExtensionPoint(extensionPoint, availableExtensions, messageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleExtensionPointMessage(msg: IMessage) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(msg.extensionId);
|
||||
|
||||
if (!this._extensionsMessages.has(extensionKey)) {
|
||||
this._extensionsMessages.set(extensionKey, []);
|
||||
}
|
||||
this._extensionsMessages.get(extensionKey)!.push(msg);
|
||||
|
||||
const extension = this._registry.getExtensionDescription(msg.extensionId);
|
||||
const strMsg = `[${msg.extensionId.value}]: ${msg.message}`;
|
||||
if (extension && extension.isUnderDevelopment) {
|
||||
// This message is about the extension currently being developed
|
||||
this._showMessageToUser(msg.type, strMsg);
|
||||
} else {
|
||||
this._logMessageInConsole(msg.type, strMsg);
|
||||
}
|
||||
|
||||
if (!this._isDev && msg.extensionId) {
|
||||
const { type, extensionId, extensionPointId, message } = msg;
|
||||
type ExtensionsMessageClassification = {
|
||||
type: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
extensionId: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
|
||||
extensionPointId: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
|
||||
message: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
|
||||
};
|
||||
type ExtensionsMessageEvent = {
|
||||
type: Severity;
|
||||
extensionId: string;
|
||||
extensionPointId: string;
|
||||
message: string;
|
||||
};
|
||||
this._telemetryService.publicLog2<ExtensionsMessageEvent, ExtensionsMessageClassification>('extensionsMessage', {
|
||||
type, extensionId: extensionId.value, extensionPointId, message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static _handleExtensionPoint<T>(extensionPoint: ExtensionPoint<T>, availableExtensions: IExtensionDescription[], messageHandler: (msg: IMessage) => void): void {
|
||||
const users: IExtensionPointUser<T>[] = [];
|
||||
for (const desc of availableExtensions) {
|
||||
if (desc.contributes && hasOwnProperty.call(desc.contributes, extensionPoint.name)) {
|
||||
users.push({
|
||||
description: desc,
|
||||
value: desc.contributes[extensionPoint.name], // {{SQL CARBON EDIT}} strict-null-checks
|
||||
collector: new ExtensionMessageCollector(messageHandler, desc, extensionPoint.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
perf.mark(`willHandleExtensionPoint/${extensionPoint.name}`);
|
||||
extensionPoint.acceptUsers(users);
|
||||
perf.mark(`didHandleExtensionPoint/${extensionPoint.name}`);
|
||||
}
|
||||
|
||||
private _showMessageToUser(severity: Severity, msg: string): void {
|
||||
if (severity === Severity.Error || severity === Severity.Warning) {
|
||||
this._notificationService.notify({ severity, message: msg });
|
||||
} else {
|
||||
this._logMessageInConsole(severity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private _logMessageInConsole(severity: Severity, msg: string): void {
|
||||
if (severity === Severity.Error) {
|
||||
console.error(msg);
|
||||
} else if (severity === Severity.Warning) {
|
||||
console.warn(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
//#region Called by extension host
|
||||
|
||||
public _logOrShowMessage(severity: Severity, msg: string): void {
|
||||
if (this._isDev) {
|
||||
this._showMessageToUser(severity, msg);
|
||||
} else {
|
||||
this._logMessageInConsole(severity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public async _activateById(extensionId: ExtensionIdentifier, activationEvent: string): Promise<void> {
|
||||
const results = await Promise.all(
|
||||
this._extensionHostProcessManagers.map(manager => manager.activate(extensionId, activationEvent))
|
||||
);
|
||||
const activated = results.some(e => e);
|
||||
if (!activated) {
|
||||
throw new Error(`Unknown extension ${extensionId.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
public _onWillActivateExtension(extensionId: ExtensionIdentifier): void {
|
||||
this._extensionHostActiveExtensions.set(ExtensionIdentifier.toKey(extensionId), extensionId);
|
||||
}
|
||||
|
||||
public _onDidActivateExtension(extensionId: ExtensionIdentifier, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
|
||||
this._extensionHostProcessActivationTimes.set(ExtensionIdentifier.toKey(extensionId), new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent));
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
public _onExtensionRuntimeError(extensionId: ExtensionIdentifier, err: Error): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extensionId);
|
||||
if (!this._extensionHostExtensionRuntimeErrors.has(extensionKey)) {
|
||||
this._extensionHostExtensionRuntimeErrors.set(extensionKey, []);
|
||||
}
|
||||
this._extensionHostExtensionRuntimeErrors.get(extensionKey)!.push(err);
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
protected abstract _createExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[];
|
||||
protected abstract _scanAndHandleExtensions(): Promise<void>;
|
||||
public abstract _onExtensionHostExit(code: number): void;
|
||||
}
|
||||
|
||||
class ProposedApiController {
|
||||
|
||||
private readonly enableProposedApiFor: string | string[];
|
||||
private readonly enableProposedApiForAll: boolean;
|
||||
private readonly productAllowProposedApi: Set<string>;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IProductService productService: IProductService
|
||||
) {
|
||||
this.enableProposedApiFor = environmentService.args['enable-proposed-api'] || [];
|
||||
if (this.enableProposedApiFor.length) {
|
||||
// Make enabled proposed API be lowercase for case insensitive comparison
|
||||
if (Array.isArray(this.enableProposedApiFor)) {
|
||||
this.enableProposedApiFor = this.enableProposedApiFor.map(id => id.toLowerCase());
|
||||
} else {
|
||||
this.enableProposedApiFor = this.enableProposedApiFor.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
this.enableProposedApiForAll = !environmentService.isBuilt ||
|
||||
(!!environmentService.extensionDevelopmentLocationURI && productService.nameLong !== 'Visual Studio Code') ||
|
||||
(this.enableProposedApiFor.length === 0 && 'enable-proposed-api' in environmentService.args);
|
||||
|
||||
this.productAllowProposedApi = new Set<string>();
|
||||
if (isNonEmptyArray(productService.extensionAllowedProposedApi)) {
|
||||
productService.extensionAllowedProposedApi.forEach((id) => this.productAllowProposedApi.add(ExtensionIdentifier.toKey(id)));
|
||||
}
|
||||
}
|
||||
|
||||
public updateEnableProposedApi(extension: IExtensionDescription): void {
|
||||
if (this._allowProposedApiFromProduct(extension.identifier)) {
|
||||
// fast lane -> proposed api is available to all extensions
|
||||
// that are listed in product.json-files
|
||||
extension.enableProposedApi = true;
|
||||
|
||||
} else if (extension.enableProposedApi && !extension.isBuiltin) {
|
||||
if (
|
||||
!this.enableProposedApiForAll &&
|
||||
this.enableProposedApiFor.indexOf(extension.identifier.value.toLowerCase()) < 0
|
||||
) {
|
||||
extension.enableProposedApi = false;
|
||||
console.error(`Extension '${extension.identifier.value} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
|
||||
|
||||
} else {
|
||||
// proposed api is available when developing or when an extension was explicitly
|
||||
// spelled out via a command line argument
|
||||
console.warn(`Extension '${extension.identifier.value}' uses PROPOSED API which is subject to change and removal without notice.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _allowProposedApiFromProduct(id: ExtensionIdentifier): boolean {
|
||||
return this.productAllowProposedApi.has(ExtensionIdentifier.toKey(id));
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
public readonly onDidCrash: Event<[number, string | null]>;
|
||||
public readonly onDidExit: Event<[number, string | null]>;
|
||||
|
||||
private readonly _onDidChangeResponsiveState: Emitter<ResponsiveState> = this._register(new Emitter<ResponsiveState>());
|
||||
public readonly onDidChangeResponsiveState: Event<ResponsiveState> = this._onDidChangeResponsiveState.event;
|
||||
@@ -54,6 +54,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
private _resolveAuthorityAttempt: number;
|
||||
|
||||
constructor(
|
||||
public readonly isLocal: boolean,
|
||||
extensionHostProcessWorker: IExtensionHostStarter,
|
||||
private readonly _remoteAuthority: string,
|
||||
initialActivationEvents: string[],
|
||||
@@ -66,7 +67,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
this._extensionHostProcessCustomers = [];
|
||||
|
||||
this._extensionHostProcessWorker = extensionHostProcessWorker;
|
||||
this.onDidCrash = this._extensionHostProcessWorker.onCrashed;
|
||||
this.onDidExit = this._extensionHostProcessWorker.onExit;
|
||||
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start()!.then(
|
||||
(protocol) => {
|
||||
return { value: this._createExtensionHostCustomers(protocol) };
|
||||
@@ -203,7 +204,8 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
// Check that no named customers are missing
|
||||
// {{SQL CARBON EDIT}} filter out services we don't expose
|
||||
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => (<any>MainContext)[key]).filter(v => v !== MainContext.MainThreadDebugService);
|
||||
const filtered: ProxyIdentifier<any>[] = [MainContext.MainThreadDebugService, MainContext.MainThreadTask];
|
||||
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => (<any>MainContext)[key]).filter(v => !filtered.includes(v));
|
||||
this._extensionHostProcessRPCProtocol.assertRegistered(expected);
|
||||
|
||||
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface IExtHostReadyMessage {
|
||||
export interface IExtHostSocketMessage {
|
||||
type: 'VSCODE_EXTHOST_IPC_SOCKET';
|
||||
initialDataChunk: string;
|
||||
skipWebSocketFrames: boolean;
|
||||
}
|
||||
|
||||
export const enum MessageType {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export interface Translations {
|
||||
[id: string]: string;
|
||||
}
|
||||
|
||||
export namespace Translations {
|
||||
export function equals(a: Translations, b: Translations): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
let aKeys = Object.keys(a);
|
||||
let bKeys: Set<string> = new Set<string>();
|
||||
for (let key of Object.keys(b)) {
|
||||
bKeys.add(key);
|
||||
}
|
||||
if (aKeys.length !== bKeys.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let key of aKeys) {
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
bKeys.delete(key);
|
||||
}
|
||||
return bKeys.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILog {
|
||||
error(source: string, message: string): void;
|
||||
warn(source: string, message: string): void;
|
||||
info(source: string, message: string): void;
|
||||
}
|
||||
|
||||
export class Logger implements ILog {
|
||||
|
||||
private readonly _messageHandler: (severity: Severity, source: string, message: string) => void;
|
||||
|
||||
constructor(
|
||||
messageHandler: (severity: Severity, source: string, message: string) => void
|
||||
) {
|
||||
this._messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
public error(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Error, source, message);
|
||||
}
|
||||
|
||||
public warn(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Warning, source, message);
|
||||
}
|
||||
|
||||
public info(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Info, source, message);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,8 @@ export interface IExtensionHostProfile {
|
||||
}
|
||||
|
||||
export interface IExtensionHostStarter {
|
||||
readonly onCrashed: Event<[number, string | null]>;
|
||||
readonly onExit: Event<[number, string | null]>;
|
||||
|
||||
start(): Promise<IMessagePassingProtocol> | null;
|
||||
getInspectPort(): number | undefined;
|
||||
dispose(): void;
|
||||
|
||||
@@ -206,7 +206,7 @@ export const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
// extensions will fill in
|
||||
},
|
||||
} as { [key: string]: any },
|
||||
default: {}
|
||||
},
|
||||
preview: {
|
||||
@@ -341,6 +341,19 @@ export const schema = {
|
||||
pattern: EXTENSION_IDENTIFIER_PATTERN
|
||||
}
|
||||
},
|
||||
extensionKind: {
|
||||
description: nls.localize('extensionKind', "Define the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote."),
|
||||
type: 'string',
|
||||
enum: [
|
||||
'ui',
|
||||
'workspace'
|
||||
],
|
||||
enumDescriptions: [
|
||||
nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."),
|
||||
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.")
|
||||
],
|
||||
default: 'workspace'
|
||||
},
|
||||
scripts: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -8,9 +8,9 @@ import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
|
||||
export function isUIExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean {
|
||||
export function isUIExtension(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
|
||||
const uiContributions = ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').map(e => e.name);
|
||||
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
|
||||
const extensionKind = getExtensionKind(manifest, configurationService);
|
||||
@@ -19,7 +19,7 @@ export function isUIExtension(manifest: IExtensionManifest, configurationService
|
||||
case 'workspace': return false;
|
||||
default: {
|
||||
// Tagged as UI extension in product
|
||||
if (isNonEmptyArray(product.uiExtensions) && product.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) {
|
||||
if (isNonEmptyArray(productService.uiExtensions) && productService.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) {
|
||||
return true;
|
||||
}
|
||||
// Not an UI extension if it has main
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -70,10 +70,10 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
this.handleURL(URI.revive(JSON.parse(urlToHandleValue)), true);
|
||||
}
|
||||
|
||||
this.disposable = combinedDisposable([
|
||||
this.disposable = combinedDisposable(
|
||||
urlService.registerHandler(this),
|
||||
toDisposable(() => clearInterval(interval))
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
async handleURL(uri: URI, confirmed?: boolean): Promise<boolean> {
|
||||
|
||||
@@ -3,17 +3,13 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import pkg from 'vs/platform/product/node/package';
|
||||
import { connectRemoteAgentExtensionHost, IRemoteExtensionHostStartParams, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { connectRemoteAgentExtensionHost, IRemoteExtensionHostStartParams, IConnectionOptions, IWebSocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IInitData } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
||||
@@ -28,8 +24,9 @@ import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { nodeWebSocketFactory } from 'vs/platform/remote/node/nodeWebSocketFactory';
|
||||
import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
|
||||
export interface IInitDataProvider {
|
||||
readonly remoteAuthority: string;
|
||||
@@ -38,28 +35,29 @@ export interface IInitDataProvider {
|
||||
|
||||
export class RemoteExtensionHostClient extends Disposable implements IExtensionHostStarter {
|
||||
|
||||
private _onCrashed: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>());
|
||||
public readonly onCrashed: Event<[number, string | null]> = this._onCrashed.event;
|
||||
private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>());
|
||||
public readonly onExit: Event<[number, string | null]> = this._onExit.event;
|
||||
|
||||
private _protocol: PersistentProtocol | null;
|
||||
|
||||
private readonly _isExtensionDevHost: boolean;
|
||||
private readonly _isExtensionDevTestFromCli: boolean;
|
||||
|
||||
private _terminating: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly _allExtensions: Promise<IExtensionDescription[]>,
|
||||
private readonly _initDataProvider: IInitDataProvider,
|
||||
private readonly _webSocketFactory: IWebSocketFactory,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||
@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService
|
||||
@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@ISignService private readonly _signService: ISignService
|
||||
) {
|
||||
super();
|
||||
this._protocol = null;
|
||||
@@ -69,20 +67,20 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
|
||||
|
||||
const devOpts = parseExtensionDevOptions(this._environmentService);
|
||||
this._isExtensionDevHost = devOpts.isExtensionDevHost;
|
||||
this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;
|
||||
}
|
||||
|
||||
public start(): Promise<IMessagePassingProtocol> {
|
||||
const options: IConnectionOptions = {
|
||||
isBuilt: this._environmentService.isBuilt,
|
||||
commit: product.commit,
|
||||
webSocketFactory: nodeWebSocketFactory,
|
||||
commit: this._productService.commit,
|
||||
webSocketFactory: this._webSocketFactory,
|
||||
addressProvider: {
|
||||
getAddress: async () => {
|
||||
const { host, port } = await this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority);
|
||||
return { host, port };
|
||||
}
|
||||
}
|
||||
},
|
||||
signService: this._signService
|
||||
};
|
||||
return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolvedAuthority) => {
|
||||
|
||||
@@ -168,20 +166,7 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected termination
|
||||
if (!this._isExtensionDevHost) {
|
||||
this._onCrashed.fire([0, null]);
|
||||
}
|
||||
|
||||
// Expected development extension termination: When the extension host goes down we also shutdown the window
|
||||
else if (!this._isExtensionDevTestFromCli) {
|
||||
this._windowService.closeWindow();
|
||||
}
|
||||
|
||||
// When CLI testing make sure to exit with proper exit code
|
||||
else {
|
||||
ipc.send('vscode:exit', 0);
|
||||
}
|
||||
this._onExit.fire([0, null]);
|
||||
}
|
||||
|
||||
private _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IInitData> {
|
||||
@@ -191,26 +176,32 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
|
||||
const hostExtensions = allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
const r: IInitData = {
|
||||
commit: product.commit,
|
||||
version: pkg.version,
|
||||
commit: this._productService.commit,
|
||||
version: this._productService.version,
|
||||
parentPid: remoteExtensionHostData.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug,
|
||||
appRoot: remoteExtensionHostData.appRoot,
|
||||
appSettingsHome: remoteExtensionHostData.appSettingsHome,
|
||||
appName: product.nameLong,
|
||||
appUriScheme: product.urlProtocol,
|
||||
appName: this._productService.nameLong,
|
||||
appUriScheme: this._productService.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: remoteExtensionHostData.globalStorageHome,
|
||||
userHome: remoteExtensionHostData.userHome
|
||||
userHome: remoteExtensionHostData.userHome,
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
webviewCspSource: this._environmentService.webviewCspSource,
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : {
|
||||
configuration: workspace.configuration,
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
},
|
||||
remote: {
|
||||
isRemote: true,
|
||||
authority: this._initDataProvider.remoteAuthority
|
||||
},
|
||||
resolvedExtensions: resolvedExtensions,
|
||||
hostExtensions: hostExtensions,
|
||||
extensions: remoteExtensionHostData.extensions,
|
||||
@@ -218,7 +209,6 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: remoteExtensionHostData.extensionHostLogsPath,
|
||||
autoStart: true,
|
||||
remoteAuthority: this._initDataProvider.remoteAuthority,
|
||||
};
|
||||
return r;
|
||||
});
|
||||
@@ -21,7 +21,8 @@ import pkg from 'vs/platform/product/node/package';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver, ILog, IRelaxedExtensionDescription, Translations } from 'vs/workbench/services/extensions/node/extensionPoints';
|
||||
import { ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver, IRelaxedExtensionDescription } from 'vs/workbench/services/extensions/node/extensionPoints';
|
||||
import { Translations, ILog } from 'vs/workbench/services/extensions/common/extensionPoints';
|
||||
|
||||
interface IExtensionCacheData {
|
||||
input: ExtensionScannerInput;
|
||||
@@ -387,26 +388,3 @@ class NullLogger implements ILog {
|
||||
public info(source: string, message: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export class Logger implements ILog {
|
||||
|
||||
private readonly _messageHandler: (severity: Severity, source: string, message: string) => void;
|
||||
|
||||
constructor(
|
||||
messageHandler: (severity: Severity, source: string, message: string) => void
|
||||
) {
|
||||
this._messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
public error(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Error, source, message);
|
||||
}
|
||||
|
||||
public warn(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Warning, source, message);
|
||||
}
|
||||
|
||||
public info(source: string, message: string): void {
|
||||
this._messageHandler(Severity.Info, source, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { Server, Socket, createServer } from 'net';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import pkg from 'vs/platform/product/node/package';
|
||||
@@ -42,10 +41,10 @@ import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
|
||||
export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
|
||||
private readonly _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>();
|
||||
public readonly onCrashed: Event<[number, string]> = this._onCrashed.event;
|
||||
private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>();
|
||||
public readonly onExit: Event<[number, string]> = this._onExit.event;
|
||||
|
||||
private readonly _toDispose: IDisposable[];
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
|
||||
private readonly _isExtensionDevHost: boolean;
|
||||
private readonly _isExtensionDevDebug: boolean;
|
||||
@@ -58,7 +57,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
|
||||
// Resources, in order they get acquired/created when .start() is called:
|
||||
private _namedPipeServer: Server | null;
|
||||
private _inspectPort: number;
|
||||
private _inspectPort: number | null;
|
||||
private _extensionHostProcess: ChildProcess | null;
|
||||
private _extensionHostConnection: Socket | null;
|
||||
private _messageProtocol: Promise<PersistentProtocol> | null;
|
||||
@@ -92,16 +91,15 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
this._extensionHostConnection = null;
|
||||
this._messageProtocol = null;
|
||||
|
||||
this._toDispose = [];
|
||||
this._toDispose.push(this._onCrashed);
|
||||
this._toDispose.push(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));
|
||||
this._toDispose.push(this._lifecycleService.onShutdown(reason => this.terminate()));
|
||||
this._toDispose.push(this._extensionHostDebugService.onClose(event => {
|
||||
this._toDispose.add(this._onExit);
|
||||
this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));
|
||||
this._toDispose.add(this._lifecycleService.onShutdown(reason => this.terminate()));
|
||||
this._toDispose.add(this._extensionHostDebugService.onClose(event => {
|
||||
if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) {
|
||||
this._windowService.closeWindow();
|
||||
}
|
||||
}));
|
||||
this._toDispose.push(this._extensionHostDebugService.onReload(event => {
|
||||
this._toDispose.add(this._extensionHostDebugService.onReload(event => {
|
||||
if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) {
|
||||
this._windowService.reloadWindow();
|
||||
}
|
||||
@@ -109,7 +107,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
|
||||
const globalExitListener = () => this.terminate();
|
||||
process.once('exit', globalExitListener);
|
||||
this._toDispose.push(toDisposable(() => {
|
||||
this._toDispose.add(toDisposable(() => {
|
||||
process.removeListener('exit', globalExitListener);
|
||||
}));
|
||||
}
|
||||
@@ -125,7 +123,10 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
}
|
||||
|
||||
if (!this._messageProtocol) {
|
||||
this._messageProtocol = Promise.all([this._tryListenOnPipe(), this._tryFindDebugPort()]).then(data => {
|
||||
this._messageProtocol = Promise.all([
|
||||
this._tryListenOnPipe(),
|
||||
!this._environmentService.args['disable-inspect'] ? this._tryFindDebugPort() : Promise.resolve(null)
|
||||
]).then(data => {
|
||||
const pipeName = data[0];
|
||||
const portData = data[1];
|
||||
|
||||
@@ -148,7 +149,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
silent: true
|
||||
};
|
||||
|
||||
if (portData.actual) {
|
||||
if (portData && portData.actual) {
|
||||
opts.execArgv = [
|
||||
'--nolazy',
|
||||
(this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portData.actual
|
||||
@@ -215,10 +216,12 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal));
|
||||
|
||||
// Notify debugger that we are ready to attach to the process if we run a development extension
|
||||
if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {
|
||||
this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portData.actual);
|
||||
if (portData) {
|
||||
if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {
|
||||
this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portData.actual);
|
||||
}
|
||||
this._inspectPort = portData.actual;
|
||||
}
|
||||
this._inspectPort = portData.actual;
|
||||
|
||||
// Help in case we fail to start it
|
||||
let startupTimeoutHandle: any;
|
||||
@@ -389,14 +392,16 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? URI.file(this._environmentService.appSettingsHome) : undefined,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined,
|
||||
appName: product.nameLong,
|
||||
appUriScheme: product.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: URI.file(this._environmentService.globalStorageHome),
|
||||
userHome: URI.file(this._environmentService.userHome)
|
||||
userHome: URI.file(this._environmentService.userHome),
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
webviewCspSource: this._environmentService.webviewCspSource,
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {
|
||||
configuration: withNullAsUndefined(workspace.configuration),
|
||||
@@ -404,6 +409,10 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
name: this._labelService.getWorkspaceLabel(workspace),
|
||||
isUntitled: workspace.configuration ? isEqualOrParent(workspace.configuration, this._environmentService.untitledWorkspacesHome) : false
|
||||
},
|
||||
remote: {
|
||||
authority: this._environmentService.configuration.remoteAuthority,
|
||||
isRemote: false
|
||||
},
|
||||
resolvedExtensions: [],
|
||||
hostExtensions: [],
|
||||
extensions: extensionDescriptions,
|
||||
@@ -451,36 +460,11 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected termination
|
||||
if (!this._isExtensionDevHost) {
|
||||
this._onCrashed.fire([code, signal]);
|
||||
}
|
||||
|
||||
// Expected development extension termination: When the extension host goes down we also shutdown the window
|
||||
else if (!this._isExtensionDevTestFromCli) {
|
||||
this._windowService.closeWindow();
|
||||
}
|
||||
|
||||
// When CLI testing make sure to exit with proper exit code
|
||||
else {
|
||||
ipc.send('vscode:exit', code);
|
||||
}
|
||||
this._onExit.fire([code, signal]);
|
||||
}
|
||||
|
||||
public enableInspector(): Promise<void> {
|
||||
if (this._inspectPort) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// send SIGUSR1 and wait a little the actual port is read from the process stdout which we
|
||||
// scan here: https://github.com/Microsoft/vscode/blob/67ffab8dcd1a6752d8b62bcd13d7020101eef568/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts#L225-L240
|
||||
if (this._extensionHostProcess) {
|
||||
this._extensionHostProcess.kill('SIGUSR1');
|
||||
}
|
||||
return timeout(1000);
|
||||
}
|
||||
|
||||
public getInspectPort(): number {
|
||||
return this._inspectPort;
|
||||
public getInspectPort(): number | undefined {
|
||||
return withNullAsUndefined(this._inspectPort);
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
@@ -489,7 +473,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
}
|
||||
this._terminating = true;
|
||||
|
||||
dispose(this._toDispose);
|
||||
this._toDispose.dispose();
|
||||
|
||||
if (!this._messageProtocol) {
|
||||
// .start() was not called
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
import { localize } from 'vs/nls';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/node/extensionManagementIpc';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { RemoteExtensionManagementChannelClient } from 'vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
|
||||
const localExtensionManagementServerAuthority: string = 'vscode-local';
|
||||
|
||||
@@ -25,14 +29,18 @@ export class ExtensionManagementServerService implements IExtensionManagementSer
|
||||
|
||||
constructor(
|
||||
@ISharedProcessService sharedProcessService: ISharedProcessService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IExtensionGalleryService galleryService: IExtensionGalleryService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IProductService productService: IProductService,
|
||||
@ILogService logService: ILogService
|
||||
) {
|
||||
const localExtensionManagementService = new ExtensionManagementChannelClient(sharedProcessService.getChannel('extensions'));
|
||||
|
||||
this.localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, authority: localExtensionManagementServerAuthority, label: localize('local', "Local") };
|
||||
const remoteAgentConnection = remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection.getChannel<IChannel>('extensions'));
|
||||
const extensionManagementService = new RemoteExtensionManagementChannelClient(remoteAgentConnection.getChannel<IChannel>('extensions'), this.localExtensionManagementServer.extensionManagementService, galleryService, logService, configurationService, productService);
|
||||
this.remoteExtensionManagementServer = { authority: remoteAgentConnection.remoteAuthority, extensionManagementService, label: localize('remote', "Remote") };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,62 +3,37 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { nodeWebSocketFactory } from 'vs/platform/remote/node/nodeWebSocketFactory';
|
||||
import { AbstractExtensionService } from 'vs/workbench/services/extensions/common/abstractExtensionService';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { Barrier, runWhenIdle } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as perf from 'vs/base/common/performance';
|
||||
import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { runWhenIdle } from 'vs/base/common/async';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IExtensionEnablementService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInitDataProvider, RemoteExtensionHostClient } from 'vs/workbench/services/extensions/electron-browser/remoteExtensionHostClient';
|
||||
import { IInitDataProvider, RemoteExtensionHostClient } from 'vs/workbench/services/extensions/common/remoteExtensionHostClient';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAuthorityResolverService, ResolvedAuthority, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import pkg from 'vs/platform/product/node/package';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser, schema } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
|
||||
import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol';
|
||||
import { CachedExtensionScanner, Logger } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner';
|
||||
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
|
||||
import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
||||
import { PersistenConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
schema.properties.engines.properties.vscode.default = `^${pkg.version}`;
|
||||
|
||||
let productAllowProposedApi: Set<string> | null = null;
|
||||
function allowProposedApiFromProduct(id: ExtensionIdentifier): boolean {
|
||||
// create set if needed
|
||||
if (!productAllowProposedApi) {
|
||||
productAllowProposedApi = new Set<string>();
|
||||
if (isNonEmptyArray(product.extensionAllowedProposedApi)) {
|
||||
product.extensionAllowedProposedApi.forEach((id) => productAllowProposedApi!.add(ExtensionIdentifier.toKey(id)));
|
||||
}
|
||||
}
|
||||
return productAllowProposedApi.has(ExtensionIdentifier.toKey(id));
|
||||
}
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints';
|
||||
|
||||
class DeltaExtensionsQueueItem {
|
||||
constructor(
|
||||
@@ -67,80 +42,38 @@ class DeltaExtensionsQueueItem {
|
||||
) { }
|
||||
}
|
||||
|
||||
export class ExtensionService extends Disposable implements IExtensionService {
|
||||
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
private _remoteExtensionsEnvironmentData: Map<string, IRemoteAgentEnvironment>;
|
||||
private readonly _remoteExtensionsEnvironmentData: Map<string, IRemoteAgentEnvironment>;
|
||||
|
||||
private readonly _extensionHostLogsLocation: URI;
|
||||
private readonly _registry: ExtensionDescriptionRegistry;
|
||||
private readonly _installedExtensionsReady: Barrier;
|
||||
private readonly _isDev: boolean;
|
||||
private readonly _extensionsMessages: Map<string, IMessage[]>;
|
||||
private _allRequestedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private readonly _extensionScanner: CachedExtensionScanner;
|
||||
private _deltaExtensionsQueue: DeltaExtensionsQueueItem[];
|
||||
|
||||
private readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event;
|
||||
|
||||
private readonly _onDidChangeExtensionsStatus: Emitter<ExtensionIdentifier[]> = this._register(new Emitter<ExtensionIdentifier[]>());
|
||||
public readonly onDidChangeExtensionsStatus: Event<ExtensionIdentifier[]> = this._onDidChangeExtensionsStatus.event;
|
||||
|
||||
private readonly _onDidChangeExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeExtensions: Event<void> = this._onDidChangeExtensions.event;
|
||||
|
||||
private readonly _onWillActivateByEvent = this._register(new Emitter<IWillActivateEvent>());
|
||||
public readonly onWillActivateByEvent: Event<IWillActivateEvent> = this._onWillActivateByEvent.event;
|
||||
|
||||
private readonly _onDidChangeResponsiveChange = this._register(new Emitter<IResponsiveStateChangeEvent>());
|
||||
public readonly onDidChangeResponsiveChange: Event<IResponsiveStateChangeEvent> = this._onDidChangeResponsiveChange.event;
|
||||
|
||||
// --- Members used per extension host process
|
||||
private _extensionHostProcessManagers: ExtensionHostProcessManager[];
|
||||
private _extensionHostActiveExtensions: Map<string, ExtensionIdentifier>;
|
||||
private _extensionHostProcessActivationTimes: Map<string, ActivationTimes>;
|
||||
private _extensionHostExtensionRuntimeErrors: Map<string, Error[]>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService extensionEnablementService: IExtensionEnablementService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IProductService productService: IProductService,
|
||||
@IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IFileService fileService: IFileService
|
||||
@IWindowService protected readonly _windowService: IWindowService,
|
||||
) {
|
||||
super();
|
||||
|
||||
// help the file service to activate providers by activating extensions by file system event
|
||||
this._register(fileService.onWillActivateFileSystemProvider(e => {
|
||||
e.join(this.activateByEvent(`onFileSystem:${e.scheme}`));
|
||||
}));
|
||||
|
||||
this._remoteExtensionsEnvironmentData = new Map<string, IRemoteAgentEnvironment>();
|
||||
|
||||
this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${_windowService.windowId}`));
|
||||
this._registry = new ExtensionDescriptionRegistry([]);
|
||||
this._installedExtensionsReady = new Barrier();
|
||||
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
|
||||
this._extensionsMessages = new Map<string, IMessage[]>();
|
||||
this._allRequestedActivateEvents = Object.create(null);
|
||||
this._extensionScanner = this._instantiationService.createInstance(CachedExtensionScanner);
|
||||
this._deltaExtensionsQueue = [];
|
||||
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
this._startDelayed(this._lifecycleService);
|
||||
super(
|
||||
instantiationService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
telemetryService,
|
||||
extensionEnablementService,
|
||||
fileService,
|
||||
productService,
|
||||
);
|
||||
|
||||
if (this._extensionEnablementService.allUserExtensionsDisabled) {
|
||||
this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{
|
||||
@@ -151,6 +84,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}]);
|
||||
}
|
||||
|
||||
this._remoteExtensionsEnvironmentData = new Map<string, IRemoteAgentEnvironment>();
|
||||
|
||||
this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${this._windowService.windowId}`));
|
||||
this._extensionScanner = instantiationService.createInstance(CachedExtensionScanner);
|
||||
this._deltaExtensionsQueue = [];
|
||||
|
||||
this._register(this._extensionEnablementService.onEnablementChanged((extensions) => {
|
||||
let toAdd: IExtension[] = [];
|
||||
let toRemove: string[] = [];
|
||||
@@ -181,8 +120,24 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem([], [event.identifier.id]));
|
||||
}
|
||||
}));
|
||||
|
||||
// delay extension host creation and extension scanning
|
||||
// until the workbench is running. we cannot defer the
|
||||
// extension host more (LifecyclePhase.Restored) because
|
||||
// some editors require the extension host to restore
|
||||
// and this would result in a deadlock
|
||||
// see https://github.com/Microsoft/vscode/issues/41322
|
||||
this._lifecycleService.when(LifecyclePhase.Ready).then(() => {
|
||||
// reschedule to ensure this runs after restoring viewlets, panels, and editors
|
||||
runWhenIdle(() => {
|
||||
this._initialize();
|
||||
}, 50 /*max delay*/);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//#region deltaExtensions
|
||||
|
||||
private _inHandleDeltaExtensions = false;
|
||||
private async _handleDeltaExtensions(item: DeltaExtensionsQueueItem): Promise<void> {
|
||||
this._deltaExtensionsQueue.push(item);
|
||||
@@ -211,13 +166,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
for (let i = 0, len = _toAdd.length; i < len; i++) {
|
||||
const extension = _toAdd[i];
|
||||
|
||||
if (extension.location.scheme !== Schemas.file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingExtensionDescription = this._registry.getExtensionDescription(extension.identifier.id);
|
||||
if (existingExtensionDescription) {
|
||||
// this extension is already running (most likely at a different version)
|
||||
if (!this._canAddExtension(extension)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -258,6 +207,9 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', ')));
|
||||
}
|
||||
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(toAdd);
|
||||
|
||||
// Update extension points
|
||||
this._rehandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove));
|
||||
|
||||
@@ -274,48 +226,36 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private _rehandleExtensionPoints(extensionDescriptions: IExtensionDescription[]): void {
|
||||
const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null);
|
||||
for (let extensionDescription of extensionDescriptions) {
|
||||
if (extensionDescription.contributes) {
|
||||
for (let extPointName in extensionDescription.contributes) {
|
||||
if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) {
|
||||
affectedExtensionPoints[extPointName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
|
||||
const availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
const extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
if (affectedExtensionPoints[extensionPoints[i].name]) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
}
|
||||
this._doHandleExtensionPoints(extensionDescriptions);
|
||||
}
|
||||
|
||||
public canAddExtension(extension: IExtensionDescription): boolean {
|
||||
public canAddExtension(extensionDescription: IExtensionDescription): boolean {
|
||||
return this._canAddExtension(toExtension(extensionDescription));
|
||||
}
|
||||
|
||||
public _canAddExtension(extension: IExtension): boolean {
|
||||
if (this._environmentService.configuration.remoteAuthority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extension.extensionLocation.scheme !== Schemas.file) {
|
||||
if (extension.location.scheme !== Schemas.file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// {{ SQL CARBON EDIT }}
|
||||
if (extension.forceReload) {
|
||||
if (extension.manifest.forceReload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionDescription = this._registry.getExtensionDescription(extension.identifier);
|
||||
const extensionDescription = this._registry.getExtensionDescription(extension.identifier.id);
|
||||
if (extensionDescription) {
|
||||
// ignore adding an extension which is already running and cannot be removed
|
||||
if (!this._canRemoveExtension(extensionDescription)) {
|
||||
return false;
|
||||
}
|
||||
// this extension is already running (most likely at a different version)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if extension is renamed
|
||||
if (extension.identifier.uuid && this._registry.getAllExtensionDescriptions().some(e => e.uuid === extension.identifier.uuid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -359,7 +299,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
activationEvent = `onUri:${ExtensionIdentifier.toKey(extensionDescription.identifier)}`;
|
||||
}
|
||||
|
||||
if (this._allRequestedActivateEvents[activationEvent]) {
|
||||
if (this._allRequestedActivateEvents.has(activationEvent)) {
|
||||
// This activation event was fired before the extension was added
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
@@ -388,108 +328,50 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
private _startDelayed(lifecycleService: ILifecycleService): void {
|
||||
// delay extension host creation and extension scanning
|
||||
// until the workbench is running. we cannot defer the
|
||||
// extension host more (LifecyclePhase.Restored) because
|
||||
// some editors require the extension host to restore
|
||||
// and this would result in a deadlock
|
||||
// see https://github.com/Microsoft/vscode/issues/41322
|
||||
lifecycleService.when(LifecyclePhase.Ready).then(() => {
|
||||
// reschedule to ensure this runs after restoring viewlets, panels, and editors
|
||||
runWhenIdle(() => {
|
||||
perf.mark('willLoadExtensions');
|
||||
this._startExtensionHostProcess(true, []);
|
||||
this._scanAndHandleExtensions();
|
||||
this.whenInstalledExtensionsRegistered().then(() => perf.mark('didLoadExtensions'));
|
||||
}, 50 /*max delay*/);
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._onWillActivateByEvent.dispose();
|
||||
this._onDidChangeResponsiveChange.dispose();
|
||||
}
|
||||
|
||||
public restartExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
this._startExtensionHostProcess(false, Object.keys(this._allRequestedActivateEvents));
|
||||
}
|
||||
|
||||
public startExtensionHost(): void {
|
||||
this._startExtensionHostProcess(false, Object.keys(this._allRequestedActivateEvents));
|
||||
}
|
||||
|
||||
public stopExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
}
|
||||
|
||||
private _stopExtensionHostProcess(): void {
|
||||
let previouslyActivatedExtensionIds: ExtensionIdentifier[] = [];
|
||||
this._extensionHostActiveExtensions.forEach((value) => {
|
||||
previouslyActivatedExtensionIds.push(value);
|
||||
});
|
||||
|
||||
for (const manager of this._extensionHostProcessManagers) {
|
||||
manager.dispose();
|
||||
}
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
if (previouslyActivatedExtensionIds.length > 0) {
|
||||
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
private _createProvider(remoteAuthority: string): IInitDataProvider {
|
||||
return {
|
||||
remoteAuthority: remoteAuthority,
|
||||
getInitData: () => {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this.whenInstalledExtensionsRegistered().then(() => {
|
||||
return this._remoteExtensionsEnvironmentData.get(remoteAuthority)!;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _startExtensionHostProcess(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
protected _createExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] {
|
||||
let autoStart: boolean;
|
||||
let extensions: Promise<IExtensionDescription[]>;
|
||||
if (isInitialStart) {
|
||||
autoStart = false;
|
||||
extensions = this._extensionScanner.scannedExtensions;
|
||||
extensions = this._extensionScanner.scannedExtensions.then(extensions => extensions.filter(extension => this._isEnabled(extension))); // remove disabled extensions
|
||||
} else {
|
||||
// restart case
|
||||
autoStart = true;
|
||||
extensions = this.getExtensions().then((extensions) => extensions.filter(ext => ext.extensionLocation.scheme === Schemas.file));
|
||||
}
|
||||
|
||||
const result: ExtensionHostProcessManager[] = [];
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._extensionHostLogsLocation);
|
||||
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, null, initialActivationEvents);
|
||||
extHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal, true));
|
||||
extHostProcessManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); });
|
||||
this._extensionHostProcessManagers.push(extHostProcessManager);
|
||||
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, true, extHostProcessWorker, null, initialActivationEvents);
|
||||
result.push(extHostProcessManager);
|
||||
|
||||
const remoteAgentConnection = this._remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority));
|
||||
const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents);
|
||||
remoteExtHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal, false));
|
||||
remoteExtHostProcessManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); });
|
||||
this._extensionHostProcessManagers.push(remoteExtHostProcessManager);
|
||||
const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority), nodeWebSocketFactory);
|
||||
const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents);
|
||||
result.push(remoteExtHostProcessManager);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _onExtensionHostCrashed(code: number, signal: string | null, showNotification: boolean): void {
|
||||
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
|
||||
this._stopExtensionHostProcess();
|
||||
protected _onExtensionHostCrashed(extensionHost: ExtensionHostProcessManager, code: number, signal: string | null): void {
|
||||
super._onExtensionHostCrashed(extensionHost, code, signal);
|
||||
|
||||
if (showNotification) {
|
||||
if (extensionHost.isLocal) {
|
||||
if (code === 55) {
|
||||
this._notificationService.prompt(
|
||||
Severity.Error,
|
||||
@@ -507,118 +389,19 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = nls.localize('extensionService.crash', "Extension host terminated unexpectedly.");
|
||||
if (code === 87) {
|
||||
message = nls.localize('extensionService.unresponsiveCrash', "Extension host terminated because it was not responsive.");
|
||||
}
|
||||
|
||||
this._notificationService.prompt(Severity.Error, message,
|
||||
this._notificationService.prompt(Severity.Error, nls.localize('extensionService.crash', "Extension host terminated unexpectedly."),
|
||||
[{
|
||||
label: nls.localize('devTools', "Open Developer Tools"),
|
||||
run: () => this._windowService.openDevTools()
|
||||
},
|
||||
{
|
||||
label: nls.localize('restart', "Restart Extension Host"),
|
||||
run: () => this._startExtensionHostProcess(false, Object.keys(this._allRequestedActivateEvents))
|
||||
run: () => this.startExtensionHost()
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- begin IExtensionService
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._installedExtensionsReady.isOpen()) {
|
||||
// Extensions have been scanned and interpreted
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
if (!this._registry.containsActivationEvent(activationEvent)) {
|
||||
// There is no extension that is interested in this activation event
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
|
||||
return this._activateByEvent(activationEvent);
|
||||
} else {
|
||||
// Extensions have not been scanned yet.
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent));
|
||||
}
|
||||
}
|
||||
|
||||
private _activateByEvent(activationEvent: string): Promise<void> {
|
||||
const result = Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent))
|
||||
).then(() => { });
|
||||
this._onWillActivateByEvent.fire({
|
||||
event: activationEvent,
|
||||
activation: result
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public whenInstalledExtensionsRegistered(): Promise<boolean> {
|
||||
return this._installedExtensionsReady.wait();
|
||||
}
|
||||
|
||||
public getExtensions(): Promise<IExtensionDescription[]> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this._registry.getAllExtensionDescriptions();
|
||||
});
|
||||
}
|
||||
|
||||
public getExtension(id: string): Promise<IExtensionDescription | undefined> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
return this._registry.getExtensionDescription(id);
|
||||
});
|
||||
}
|
||||
|
||||
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): Promise<ExtensionPointContribution<T>[]> {
|
||||
return this._installedExtensionsReady.wait().then(() => {
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
|
||||
let result: ExtensionPointContribution<T>[] = [], resultLen = 0;
|
||||
for (let i = 0, len = availableExtensions.length; i < len; i++) {
|
||||
let desc = availableExtensions[i];
|
||||
|
||||
if (desc.contributes && hasOwnProperty.call(desc.contributes, extPoint.name)) {
|
||||
result[resultLen++] = new ExtensionPointContribution<T>(desc, desc.contributes[extPoint.name]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
|
||||
let result: { [id: string]: IExtensionsStatus; } = Object.create(null);
|
||||
if (this._registry) {
|
||||
const extensions = this._registry.getAllExtensionDescriptions();
|
||||
for (const extension of extensions) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extension.identifier);
|
||||
result[extension.identifier.value] = {
|
||||
messages: this._extensionsMessages.get(extensionKey) || [],
|
||||
activationTimes: this._extensionHostProcessActivationTimes.get(extensionKey),
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors.get(extensionKey) || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getInspectPort(): number {
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
return this._extensionHostProcessManagers[0].getInspectPort();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- end IExtensionService
|
||||
|
||||
// --- impl
|
||||
|
||||
private createLogger(): Logger {
|
||||
@@ -647,7 +430,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async _scanAndHandleExtensions(): Promise<void> {
|
||||
protected async _scanAndHandleExtensions(): Promise<void> {
|
||||
this._extensionScanner.startScanningExtensions(this.createLogger());
|
||||
|
||||
const remoteAuthority = this._environmentService.configuration.remoteAuthority;
|
||||
@@ -655,6 +438,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
let localExtensions = await this._extensionScanner.scannedExtensions;
|
||||
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(localExtensions);
|
||||
|
||||
// remove disabled extensions
|
||||
localExtensions = localExtensions.filter(extension => this._isEnabled(extension));
|
||||
|
||||
if (remoteAuthority) {
|
||||
let resolvedAuthority: ResolvedAuthority;
|
||||
|
||||
@@ -698,37 +487,28 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
// fetch the remote environment
|
||||
const remoteEnv = (await this._remoteAgentService.getEnvironment())!;
|
||||
|
||||
// revive URIs
|
||||
remoteEnv.extensions.forEach((extension) => {
|
||||
(<any>extension).extensionLocation = URI.revive(extension.extensionLocation);
|
||||
});
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(remoteEnv.extensions);
|
||||
|
||||
// remove disabled extensions
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension));
|
||||
|
||||
// remove UI extensions from the remote extensions
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => !isUIExtension(extension, this._configurationService));
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => !isUIExtension(extension, this._productService, this._configurationService));
|
||||
|
||||
// remove non-UI extensions from the local extensions
|
||||
localExtensions = localExtensions.filter(extension => extension.isBuiltin || isUIExtension(extension, this._configurationService));
|
||||
localExtensions = localExtensions.filter(extension => extension.isBuiltin || isUIExtension(extension, this._productService, this._configurationService));
|
||||
|
||||
// in case of overlap, the remote wins
|
||||
const isRemoteExtension = new Set<string>();
|
||||
remoteEnv.extensions.forEach(extension => isRemoteExtension.add(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
localExtensions = localExtensions.filter(extension => !isRemoteExtension.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
|
||||
// compute enabled extensions
|
||||
const enabledExtensions = await this._getRuntimeExtensions((<IExtensionDescription[]>[]).concat(remoteEnv.extensions).concat(localExtensions));
|
||||
|
||||
// remove disabled extensions
|
||||
const isEnabled = new Set<string>();
|
||||
enabledExtensions.forEach(extension => isEnabled.add(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => isEnabled.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
localExtensions = localExtensions.filter(extension => isEnabled.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
|
||||
// save for remote extension's init data
|
||||
this._remoteExtensionsEnvironmentData.set(remoteAuthority, remoteEnv);
|
||||
|
||||
this._handleExtensionPoints(enabledExtensions);
|
||||
this._handleExtensionPoints((<IExtensionDescription[]>[]).concat(remoteEnv.extensions).concat(localExtensions));
|
||||
extensionHost.start(localExtensions.map(extension => extension.identifier));
|
||||
this._releaseBarrier();
|
||||
|
||||
} else {
|
||||
await this._startLocalExtensionHost(extensionHost, localExtensions);
|
||||
@@ -736,11 +516,8 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private async _startLocalExtensionHost(extensionHost: ExtensionHostProcessManager, localExtensions: IExtensionDescription[]): Promise<void> {
|
||||
const enabledExtensions = await this._getRuntimeExtensions(localExtensions);
|
||||
|
||||
this._handleExtensionPoints(enabledExtensions);
|
||||
extensionHost.start(enabledExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id)));
|
||||
this._releaseBarrier();
|
||||
this._handleExtensionPoints(localExtensions);
|
||||
extensionHost.start(localExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id)));
|
||||
}
|
||||
|
||||
private _handleExtensionPoints(allExtensions: IExtensionDescription[]): void {
|
||||
@@ -749,234 +526,19 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', ')));
|
||||
}
|
||||
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
let extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
|
||||
let messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions());
|
||||
}
|
||||
|
||||
private _releaseBarrier(): void {
|
||||
perf.mark('extensionHostReady');
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(undefined);
|
||||
this._onDidChangeExtensionsStatus.fire(this._registry.getAllExtensionDescriptions().map(e => e.identifier));
|
||||
}
|
||||
|
||||
private isExtensionUnderDevelopment(extension: IExtensionDescription): boolean {
|
||||
if (this._environmentService.isExtensionDevelopment) {
|
||||
const extDevLocs = this._environmentService.extensionDevelopmentLocationURI;
|
||||
if (extDevLocs) {
|
||||
const extLocation = extension.extensionLocation;
|
||||
for (let p of extDevLocs) {
|
||||
if (isEqualOrParent(extLocation, p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
public getInspectPort(): number {
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
return this._extensionHostProcessManagers[0].getInspectPort();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise<IExtensionDescription[]> {
|
||||
|
||||
const runtimeExtensions: IExtensionDescription[] = [];
|
||||
const extensionsToDisable: IExtensionDescription[] = [];
|
||||
const userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }];
|
||||
|
||||
let enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || [];
|
||||
|
||||
const notFound = (id: string) => nls.localize('notFound', "Extension \`{0}\` cannot use PROPOSED API as it cannot be found", id);
|
||||
|
||||
if (enableProposedApiFor.length) {
|
||||
let allProposed = (enableProposedApiFor instanceof Array ? enableProposedApiFor : [enableProposedApiFor]);
|
||||
allProposed.forEach(id => {
|
||||
if (!allExtensions.some(description => ExtensionIdentifier.equals(description.identifier, id))) {
|
||||
console.error(notFound(id));
|
||||
}
|
||||
});
|
||||
// Make enabled proposed API be lowercase for case insensitive comparison
|
||||
if (Array.isArray(enableProposedApiFor)) {
|
||||
enableProposedApiFor = enableProposedApiFor.map(id => id.toLowerCase());
|
||||
} else {
|
||||
enableProposedApiFor = enableProposedApiFor.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const enableProposedApiForAll = !this._environmentService.isBuilt ||
|
||||
(!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong !== 'Visual Studio Code') ||
|
||||
(enableProposedApiFor.length === 0 && 'enable-proposed-api' in this._environmentService.args);
|
||||
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
|
||||
// Do not disable extensions under development
|
||||
if (!this.isExtensionUnderDevelopment(extension)) {
|
||||
if (!this._extensionEnablementService.isEnabled(toExtension(extension))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension.isBuiltin) {
|
||||
// Check if the extension is changed to system extension
|
||||
const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.identifier.value }))[0];
|
||||
if (userMigratedSystemExtension) {
|
||||
extensionsToDisable.push(extension);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
runtimeExtensions.push(this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor));
|
||||
}
|
||||
|
||||
this._telemetryService.publicLog('extensionsScanned', {
|
||||
totalCount: runtimeExtensions.length,
|
||||
disabledCount: allExtensions.length - runtimeExtensions.length
|
||||
});
|
||||
|
||||
if (extensionsToDisable.length) {
|
||||
return this._extensionEnablementService.setEnablement(extensionsToDisable.map(e => toExtension(e)), EnablementState.Disabled)
|
||||
.then(() => runtimeExtensions);
|
||||
} else {
|
||||
return runtimeExtensions;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateEnableProposedApi(extension: IExtensionDescription, enableProposedApiForAll: boolean, enableProposedApiFor: string | string[]): IExtensionDescription {
|
||||
if (allowProposedApiFromProduct(extension.identifier)) {
|
||||
// fast lane -> proposed api is available to all extensions
|
||||
// that are listed in product.json-files
|
||||
extension.enableProposedApi = true;
|
||||
|
||||
} else if (extension.enableProposedApi && !extension.isBuiltin) {
|
||||
if (
|
||||
!enableProposedApiForAll &&
|
||||
enableProposedApiFor.indexOf(extension.identifier.value.toLowerCase()) < 0
|
||||
) {
|
||||
extension.enableProposedApi = false;
|
||||
console.error(`Extension '${extension.identifier.value} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
|
||||
|
||||
} else {
|
||||
// proposed api is available when developing or when an extension was explicitly
|
||||
// spelled out via a command line argument
|
||||
console.warn(`Extension '${extension.identifier.value}' uses PROPOSED API which is subject to change and removal without notice.`);
|
||||
}
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
private _handleExtensionPointMessage(msg: IMessage) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(msg.extensionId);
|
||||
|
||||
if (!this._extensionsMessages.has(extensionKey)) {
|
||||
this._extensionsMessages.set(extensionKey, []);
|
||||
}
|
||||
this._extensionsMessages.get(extensionKey)!.push(msg);
|
||||
|
||||
const extension = this._registry.getExtensionDescription(msg.extensionId);
|
||||
const strMsg = `[${msg.extensionId.value}]: ${msg.message}`;
|
||||
if (extension && extension.isUnderDevelopment) {
|
||||
// This message is about the extension currently being developed
|
||||
this._showMessageToUser(msg.type, strMsg);
|
||||
} else {
|
||||
this._logMessageInConsole(msg.type, strMsg);
|
||||
}
|
||||
|
||||
if (!this._isDev && msg.extensionId) {
|
||||
const { type, extensionId, extensionPointId, message } = msg;
|
||||
/* __GDPR__
|
||||
"extensionsMessage" : {
|
||||
"type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"extensionId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
|
||||
"extensionPointId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
|
||||
"message": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
|
||||
}
|
||||
*/
|
||||
this._telemetryService.publicLog('extensionsMessage', {
|
||||
type, extensionId: extensionId.value, extensionPointId, message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static _handleExtensionPoint<T>(extensionPoint: ExtensionPoint<T>, availableExtensions: IExtensionDescription[], messageHandler: (msg: IMessage) => void): void {
|
||||
let users: IExtensionPointUser<T>[] = [], usersLen = 0;
|
||||
for (let i = 0, len = availableExtensions.length; i < len; i++) {
|
||||
let desc = availableExtensions[i];
|
||||
|
||||
if (desc.contributes && hasOwnProperty.call(desc.contributes, extensionPoint.name)) {
|
||||
users[usersLen++] = {
|
||||
description: desc,
|
||||
value: desc.contributes[extensionPoint.name],
|
||||
collector: new ExtensionMessageCollector(messageHandler, desc, extensionPoint.name)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extensionPoint.acceptUsers(users);
|
||||
}
|
||||
|
||||
private _showMessageToUser(severity: Severity, msg: string): void {
|
||||
if (severity === Severity.Error || severity === Severity.Warning) {
|
||||
this._notificationService.notify({ severity, message: msg });
|
||||
} else {
|
||||
this._logMessageInConsole(severity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private _logMessageInConsole(severity: Severity, msg: string): void {
|
||||
if (severity === Severity.Error) {
|
||||
console.error(msg);
|
||||
} else if (severity === Severity.Warning) {
|
||||
console.warn(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// -- called by extension host
|
||||
|
||||
public _logOrShowMessage(severity: Severity, msg: string): void {
|
||||
if (this._isDev) {
|
||||
this._showMessageToUser(severity, msg);
|
||||
} else {
|
||||
this._logMessageInConsole(severity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public async _activateById(extensionId: ExtensionIdentifier, activationEvent: string): Promise<void> {
|
||||
const results = await Promise.all(
|
||||
this._extensionHostProcessManagers.map(manager => manager.activate(extensionId, activationEvent))
|
||||
);
|
||||
const activated = results.some(e => e);
|
||||
if (!activated) {
|
||||
throw new Error(`Unknown extension ${extensionId.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
public _onWillActivateExtension(extensionId: ExtensionIdentifier): void {
|
||||
this._extensionHostActiveExtensions.set(ExtensionIdentifier.toKey(extensionId), extensionId);
|
||||
}
|
||||
|
||||
public _onDidActivateExtension(extensionId: ExtensionIdentifier, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
|
||||
this._extensionHostProcessActivationTimes.set(ExtensionIdentifier.toKey(extensionId), new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent));
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
public _onExtensionRuntimeError(extensionId: ExtensionIdentifier, err: Error): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extensionId);
|
||||
if (!this._extensionHostExtensionRuntimeErrors.has(extensionKey)) {
|
||||
this._extensionHostExtensionRuntimeErrors.set(extensionKey, []);
|
||||
}
|
||||
this._extensionHostExtensionRuntimeErrors.get(extensionKey)!.push(err);
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public _onExtensionHostExit(code: number): void {
|
||||
// Expected development extension termination: When the extension host goes down we also shutdown the window
|
||||
const devOpts = parseExtensionDevOptions(this._environmentService);
|
||||
if (!devOpts.isExtensionDevTestFromCli) {
|
||||
if (!this._isExtensionDevTestFromCli) {
|
||||
this._windowService.closeWindow();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { tmpdir } from 'os';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/node/extensionManagementIpc';
|
||||
|
||||
export class RemoteExtensionManagementChannelClient extends ExtensionManagementChannelClient {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
channel: IChannel,
|
||||
private readonly localExtensionManagementService: IExtensionManagementService,
|
||||
private readonly galleryService: IExtensionGalleryService,
|
||||
private readonly logService: ILogService,
|
||||
private readonly configurationService: IConfigurationService,
|
||||
private readonly productService: IProductService
|
||||
) {
|
||||
super(channel);
|
||||
}
|
||||
|
||||
async install(vsix: URI): Promise<ILocalExtension> {
|
||||
const local = await super.install(vsix);
|
||||
await this.installUIDependenciesAndPackedExtensions(local);
|
||||
return local;
|
||||
}
|
||||
|
||||
async installFromGallery(extension: IGalleryExtension): Promise<ILocalExtension> {
|
||||
const local = await this.doInstallFromGallery(extension);
|
||||
await this.installUIDependenciesAndPackedExtensions(local);
|
||||
return local;
|
||||
}
|
||||
|
||||
private async doInstallFromGallery(extension: IGalleryExtension): Promise<ILocalExtension> {
|
||||
try {
|
||||
const local = await super.installFromGallery(extension);
|
||||
return local;
|
||||
} catch (error) {
|
||||
try {
|
||||
this.logService.error(`Error while installing '${extension.identifier.id}' extension in the remote server.`, toErrorMessage(error));
|
||||
this.logService.info(`Trying to download '${extension.identifier.id}' extension locally and install`);
|
||||
const local = await this.downloadCompatibleAndInstall(extension);
|
||||
this.logService.info(`Successfully installed '${extension.identifier.id}' extension`);
|
||||
return local;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadCompatibleAndInstall(extension: IGalleryExtension): Promise<ILocalExtension> {
|
||||
const installed = await this.getInstalled(ExtensionType.User);
|
||||
const compatible = await this.galleryService.getCompatibleExtension(extension);
|
||||
if (!compatible) {
|
||||
return Promise.reject(new Error(localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extension.identifier.id, this.productService.version)));
|
||||
}
|
||||
const manifest = await this.galleryService.getManifest(compatible, CancellationToken.None);
|
||||
if (manifest) {
|
||||
const workspaceExtensions = await this.getAllWorkspaceDependenciesAndPackedExtensions(manifest, CancellationToken.None);
|
||||
await Promise.all(workspaceExtensions.map(e => this.downloadAndInstall(e, installed)));
|
||||
}
|
||||
return this.downloadAndInstall(extension, installed);
|
||||
}
|
||||
|
||||
private async downloadAndInstall(extension: IGalleryExtension, installed: ILocalExtension[]): Promise<ILocalExtension> {
|
||||
const location = await this.galleryService.download(extension, URI.file(tmpdir()), installed.filter(i => areSameExtensions(i.identifier, extension.identifier))[0] ? InstallOperation.Update : InstallOperation.Install);
|
||||
return super.install(location);
|
||||
}
|
||||
|
||||
private async installUIDependenciesAndPackedExtensions(local: ILocalExtension): Promise<void> {
|
||||
const uiExtensions = await this.getAllUIDependenciesAndPackedExtensions(local.manifest, CancellationToken.None);
|
||||
const installed = await this.localExtensionManagementService.getInstalled();
|
||||
const toInstall = uiExtensions.filter(e => installed.every(i => !areSameExtensions(i.identifier, e.identifier)));
|
||||
await Promise.all(toInstall.map(d => this.localExtensionManagementService.installFromGallery(d)));
|
||||
}
|
||||
|
||||
private async getAllUIDependenciesAndPackedExtensions(manifest: IExtensionManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
|
||||
const result = new Map<string, IGalleryExtension>();
|
||||
const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])];
|
||||
await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, true, token);
|
||||
return values(result);
|
||||
}
|
||||
|
||||
private async getAllWorkspaceDependenciesAndPackedExtensions(manifest: IExtensionManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
|
||||
const result = new Map<string, IGalleryExtension>();
|
||||
const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])];
|
||||
await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, false, token);
|
||||
return values(result);
|
||||
}
|
||||
|
||||
private async getDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map<string, IGalleryExtension>, uiExtension: boolean, token: CancellationToken): Promise<void> {
|
||||
if (toGet.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const extensions = (await this.galleryService.query({ names: toGet, pageSize: toGet.length }, token)).firstPage;
|
||||
const manifests = await Promise.all(extensions.map(e => this.galleryService.getManifest(e, token)));
|
||||
const extensionsManifests: IExtensionManifest[] = [];
|
||||
for (let idx = 0; idx < extensions.length; idx++) {
|
||||
const extension = extensions[idx];
|
||||
const manifest = manifests[idx];
|
||||
if (manifest && isUIExtension(manifest, this.productService, this.configurationService) === uiExtension) {
|
||||
result.set(extension.identifier.id.toLowerCase(), extension);
|
||||
extensionsManifests.push(manifest);
|
||||
}
|
||||
}
|
||||
toGet = [];
|
||||
for (const extensionManifest of extensionsManifests) {
|
||||
if (isNonEmptyArray(extensionManifest.extensionDependencies)) {
|
||||
for (const id of extensionManifest.extensionDependencies) {
|
||||
if (!result.has(id.toLowerCase())) {
|
||||
toGet.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isNonEmptyArray(extensionManifest.extensionPack)) {
|
||||
for (const id of extensionManifest.extensionPack) {
|
||||
if (!result.has(id.toLowerCase())) {
|
||||
toGet.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.getDependenciesAndPackedExtensionsRecursively(toGet, result, uiExtension, token);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures';
|
||||
|
||||
// we don't (yet) throw when extensions parse
|
||||
// uris that have no scheme
|
||||
@@ -53,9 +52,7 @@ export class ExtensionHostMain {
|
||||
hostUtils: IHostUtils,
|
||||
consolePatchFn: IConsolePatchFn,
|
||||
logServiceFn: ILogServiceFn,
|
||||
uriTransformer: IURITransformer | null,
|
||||
schemeTransformer: ISchemeTransformer | null,
|
||||
outputChannelName: string,
|
||||
uriTransformer: IURITransformer | null
|
||||
) {
|
||||
this._isTerminating = false;
|
||||
this._hostUtils = hostUtils;
|
||||
@@ -86,8 +83,7 @@ export class ExtensionHostMain {
|
||||
extHostConfiguraiton,
|
||||
initData.environment,
|
||||
this._extHostLogService,
|
||||
schemeTransformer,
|
||||
outputChannelName
|
||||
uriTransformer
|
||||
);
|
||||
|
||||
// error forwarding and stack trace scanning
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { startExtensionHostProcess } from 'vs/workbench/services/extensions/node/extensionHostProcessSetup';
|
||||
|
||||
startExtensionHostProcess(
|
||||
_ => null,
|
||||
_ => null,
|
||||
_ => nls.localize('extension host Log', "Extension Host")
|
||||
).catch((err) => console.log(err));
|
||||
startExtensionHostProcess().catch((err) => console.log(err));
|
||||
|
||||
@@ -5,23 +5,33 @@
|
||||
|
||||
import * as nativeWatchdog from 'native-watchdog';
|
||||
import * as net from 'net';
|
||||
import * as minimist from 'minimist';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { PersistentProtocol, ProtocolConstants, createBufferedEvent } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
import { IInitData, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
||||
import { ExtensionHostMain, IExitFn, ILogServiceFn } from 'vs/workbench/services/extensions/node/extensionHostMain';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { createBufferSpdLogService } from 'vs/platform/log/node/spdlogService';
|
||||
import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ISchemeTransformer } from 'vs/workbench/api/common/extHostLanguageFeatures';
|
||||
import { IURITransformer } from 'vs/base/common/uriIpc';
|
||||
import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
|
||||
import { exists } from 'vs/base/node/pfs';
|
||||
import { realpath } from 'vs/base/node/extpath';
|
||||
import { IHostUtils } from 'vs/workbench/api/node/extHostExtensionService';
|
||||
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
|
||||
|
||||
interface ParsedExtHostArgs {
|
||||
uriTransformerPath?: string;
|
||||
}
|
||||
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
string: [
|
||||
'uriTransformerPath'
|
||||
]
|
||||
}) as ParsedExtHostArgs;
|
||||
|
||||
// With Electron 2.x and node.js 8.x the "natives" module
|
||||
// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and
|
||||
@@ -72,7 +82,7 @@ function patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void {
|
||||
};
|
||||
}
|
||||
|
||||
const createLogService: ILogServiceFn = initData => createBufferSpdLogService(ExtensionHostLogFileName, initData.logLevel, initData.logsLocation.fsPath);
|
||||
const createLogService: ILogServiceFn = initData => new SpdLogService(ExtensionHostLogFileName, initData.logsLocation.fsPath, initData.logLevel);
|
||||
|
||||
interface IRendererConnection {
|
||||
protocol: IMessagePassingProtocol;
|
||||
@@ -101,27 +111,41 @@ function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
||||
process.on('message', (msg: IExtHostSocketMessage, handle: net.Socket) => {
|
||||
if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') {
|
||||
const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64'));
|
||||
let socket: NodeSocket | WebSocketNodeSocket;
|
||||
if (msg.skipWebSocketFrames) {
|
||||
socket = new NodeSocket(handle);
|
||||
} else {
|
||||
socket = new WebSocketNodeSocket(new NodeSocket(handle));
|
||||
}
|
||||
if (protocol) {
|
||||
// reconnection case
|
||||
if (disconnectWaitTimer) {
|
||||
clearTimeout(disconnectWaitTimer);
|
||||
disconnectWaitTimer = null;
|
||||
}
|
||||
protocol.beginAcceptReconnection(new NodeSocket(handle), initialDataChunk);
|
||||
protocol.beginAcceptReconnection(socket, initialDataChunk);
|
||||
protocol.endAcceptReconnection();
|
||||
} else {
|
||||
clearTimeout(timer);
|
||||
protocol = new PersistentProtocol(new NodeSocket(handle), initialDataChunk);
|
||||
protocol = new PersistentProtocol(socket, initialDataChunk);
|
||||
protocol.onClose(() => onTerminate());
|
||||
resolve(protocol);
|
||||
|
||||
protocol.onSocketClose(() => {
|
||||
// The socket has closed, let's give the renderer a certain amount of time to reconnect
|
||||
disconnectWaitTimer = setTimeout(() => {
|
||||
disconnectWaitTimer = null;
|
||||
if (msg.skipWebSocketFrames) {
|
||||
// Wait for rich client to reconnect
|
||||
protocol.onSocketClose(() => {
|
||||
// The socket has closed, let's give the renderer a certain amount of time to reconnect
|
||||
disconnectWaitTimer = setTimeout(() => {
|
||||
disconnectWaitTimer = null;
|
||||
onTerminate();
|
||||
}, ProtocolConstants.ReconnectionGraceTime);
|
||||
});
|
||||
} else {
|
||||
// Do not wait for web companion to reconnect
|
||||
protocol.onSocketClose(() => {
|
||||
onTerminate();
|
||||
}, ProtocolConstants.ReconnectionGraceTime);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -155,16 +179,22 @@ async function createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
||||
|
||||
return new class implements IMessagePassingProtocol {
|
||||
|
||||
private _terminating = false;
|
||||
private readonly _onMessage = new Emitter<VSBuffer>();
|
||||
readonly onMessage: Event<VSBuffer> = createBufferedEvent(this._onMessage.event);
|
||||
|
||||
readonly onMessage: Event<any> = Event.filter(protocol.onMessage, msg => {
|
||||
if (!isMessageOfType(msg, MessageType.Terminate)) {
|
||||
return true;
|
||||
}
|
||||
this._terminating = true;
|
||||
onTerminate();
|
||||
return false;
|
||||
});
|
||||
private _terminating: boolean;
|
||||
|
||||
constructor() {
|
||||
this._terminating = false;
|
||||
protocol.onMessage((msg) => {
|
||||
if (isMessageOfType(msg, MessageType.Terminate)) {
|
||||
this._terminating = true;
|
||||
onTerminate();
|
||||
} else {
|
||||
this._onMessage.fire(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
send(msg: any): void {
|
||||
if (!this._terminating) {
|
||||
@@ -272,11 +302,7 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRenderer
|
||||
}
|
||||
})();
|
||||
|
||||
export async function startExtensionHostProcess(
|
||||
uriTransformerFn: (initData: IInitData) => IURITransformer | null,
|
||||
schemeTransformerFn: (initData: IInitData) => ISchemeTransformer | null,
|
||||
outputChannelNameFn: (initData: IInitData) => string,
|
||||
): Promise<void> {
|
||||
export async function startExtensionHostProcess(): Promise<void> {
|
||||
|
||||
const protocol = await createExtHostProtocol();
|
||||
const renderer = await connectToRenderer(protocol);
|
||||
@@ -291,6 +317,17 @@ export async function startExtensionHostProcess(
|
||||
realpath(path: string) { return realpath(path); }
|
||||
};
|
||||
|
||||
// Attempt to load uri transformer
|
||||
let uriTransformer: IURITransformer | null = null;
|
||||
if (initData.remote.authority && args.uriTransformerPath) {
|
||||
try {
|
||||
const rawURITransformerFactory = <any>require.__$__nodeRequire(args.uriTransformerPath);
|
||||
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(initData.remote.authority);
|
||||
uriTransformer = new URITransformer(rawURITransformer);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const extensionHostMain = new ExtensionHostMain(
|
||||
renderer.protocol,
|
||||
@@ -298,9 +335,7 @@ export async function startExtensionHostProcess(
|
||||
hostUtils,
|
||||
patchPatchedConsole,
|
||||
createLogService,
|
||||
uriTransformerFn(initData),
|
||||
schemeTransformerFn(initData),
|
||||
outputChannelNameFn(initData)
|
||||
uriTransformer
|
||||
);
|
||||
|
||||
// rewrite onTerminate-function to be a proper shutdown
|
||||
|
||||
@@ -12,40 +12,13 @@ import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import { ExtensionIdentifier, ExtensionIdentifierWithVersion, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { getGalleryExtensionId, groupByExtension, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { isValidExtensionVersion } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { Translations, ILog } from 'vs/workbench/services/extensions/common/extensionPoints';
|
||||
|
||||
const MANIFEST_FILE = 'package.json';
|
||||
|
||||
export interface Translations {
|
||||
[id: string]: string;
|
||||
}
|
||||
|
||||
namespace Translations {
|
||||
export function equals(a: Translations, b: Translations): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
let aKeys = Object.keys(a);
|
||||
let bKeys: Set<string> = new Set<string>();
|
||||
for (let key of Object.keys(b)) {
|
||||
bKeys.add(key);
|
||||
}
|
||||
if (aKeys.length !== bKeys.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let key of aKeys) {
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
bKeys.delete(key);
|
||||
}
|
||||
return bKeys.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NlsConfiguration {
|
||||
readonly devMode: boolean;
|
||||
readonly locale: string | undefined;
|
||||
@@ -53,12 +26,6 @@ export interface NlsConfiguration {
|
||||
readonly translations: Translations;
|
||||
}
|
||||
|
||||
export interface ILog {
|
||||
error(source: string, message: string): void;
|
||||
warn(source: string, message: string): void;
|
||||
info(source: string, message: string): void;
|
||||
}
|
||||
|
||||
abstract class ExtensionManifestHandler {
|
||||
|
||||
protected readonly _ourVersion: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata,
|
||||
IExtensionManagementServerService, IExtensionManagementServer, IExtensionGalleryService
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionType, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionType, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -16,10 +16,9 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil';
|
||||
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IProductService } from 'vs/platform/product/common/product';
|
||||
|
||||
export class MultiExtensionManagementService extends Disposable implements IExtensionManagementService {
|
||||
|
||||
@@ -35,7 +34,8 @@ export class MultiExtensionManagementService extends Disposable implements IExte
|
||||
constructor(
|
||||
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
this.servers = this.extensionManagementServerService.remoteExtensionManagementServer ? [this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer] : [this.extensionManagementServerService.localExtensionManagementServer];
|
||||
@@ -85,7 +85,7 @@ export class MultiExtensionManagementService extends Disposable implements IExte
|
||||
private async uninstallInServer(extension: ILocalExtension, server: IExtensionManagementServer, force?: boolean): Promise<void> {
|
||||
if (server === this.extensionManagementServerService.localExtensionManagementServer) {
|
||||
const installedExtensions = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const dependentNonUIExtensions = installedExtensions.filter(i => !isUIExtension(i.manifest, this.configurationService)
|
||||
const dependentNonUIExtensions = installedExtensions.filter(i => !isUIExtension(i.manifest, this.productService, this.configurationService)
|
||||
&& i.manifest.extensionDependencies && i.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier)));
|
||||
if (dependentNonUIExtensions.length) {
|
||||
return Promise.reject(new Error(this.getDependentsErrorMessage(extension, dependentNonUIExtensions)));
|
||||
@@ -132,44 +132,38 @@ export class MultiExtensionManagementService extends Disposable implements IExte
|
||||
return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation, type))).then(([extensionIdentifier]) => extensionIdentifier);
|
||||
}
|
||||
|
||||
async install(vsix: URI): Promise<IExtensionIdentifier> {
|
||||
async install(vsix: URI): Promise<ILocalExtension> {
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
const manifest = await getManifest(vsix.fsPath);
|
||||
if (isLanguagePackExtension(manifest)) {
|
||||
// Install on both servers
|
||||
const [extensionIdentifier] = await Promise.all(this.servers.map(server => server.extensionManagementService.install(vsix)));
|
||||
return extensionIdentifier;
|
||||
const [local] = await Promise.all(this.servers.map(server => server.extensionManagementService.install(vsix)));
|
||||
return local;
|
||||
}
|
||||
if (isUIExtension(manifest, this.configurationService)) {
|
||||
if (isUIExtension(manifest, this.productService, this.configurationService)) {
|
||||
// Install only on local server
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix);
|
||||
}
|
||||
// Install only on remote server
|
||||
const promise = this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.install(vsix);
|
||||
// Install UI Dependencies on local server
|
||||
await this.installUIDependenciesAndPackedExtensions(manifest);
|
||||
return promise;
|
||||
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.install(vsix);
|
||||
}
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.install(vsix);
|
||||
}
|
||||
|
||||
async installFromGallery(gallery: IGalleryExtension): Promise<void> {
|
||||
async installFromGallery(gallery: IGalleryExtension): Promise<ILocalExtension> {
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);
|
||||
if (manifest) {
|
||||
if (isLanguagePackExtension(manifest)) {
|
||||
// Install on both servers
|
||||
return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(() => undefined);
|
||||
return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local);
|
||||
}
|
||||
if (isUIExtension(manifest, this.configurationService)) {
|
||||
if (isUIExtension(manifest, this.productService, this.configurationService)) {
|
||||
// Install only on local server
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
// Install only on remote server
|
||||
const promise = this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
// Install UI dependencies and packed extensions on local server
|
||||
await this.installUIDependenciesAndPackedExtensions(manifest);
|
||||
return promise;
|
||||
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
} else {
|
||||
return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name));
|
||||
}
|
||||
@@ -177,13 +171,6 @@ export class MultiExtensionManagementService extends Disposable implements IExte
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
|
||||
private async installUIDependenciesAndPackedExtensions(manifest: IExtensionManifest): Promise<void> {
|
||||
const uiExtensions = await this.getAllUIDependenciesAndPackedExtensions(manifest, CancellationToken.None);
|
||||
const installed = await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getInstalled();
|
||||
const toInstall = uiExtensions.filter(e => installed.every(i => !areSameExtensions(i.identifier, e.identifier)));
|
||||
await Promise.all(toInstall.map(d => this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(d)));
|
||||
}
|
||||
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> {
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getExtensionsReport();
|
||||
}
|
||||
@@ -191,49 +178,6 @@ export class MultiExtensionManagementService extends Disposable implements IExte
|
||||
private getServer(extension: ILocalExtension): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServerService.getExtensionManagementServer(extension.location);
|
||||
}
|
||||
|
||||
private async getAllUIDependenciesAndPackedExtensions(manifest: IExtensionManifest, token: CancellationToken): Promise<IGalleryExtension[]> {
|
||||
const result = new Map<string, IGalleryExtension>();
|
||||
const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])];
|
||||
await this.getAllUIDependenciesAndPackedExtensionsRecursively(extensions, result, token);
|
||||
return values(result);
|
||||
}
|
||||
|
||||
private async getAllUIDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map<string, IGalleryExtension>, token: CancellationToken): Promise<void> {
|
||||
if (toGet.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const extensions = (await this.extensionGalleryService.query({ names: toGet, pageSize: toGet.length }, token)).firstPage;
|
||||
const manifests = await Promise.all(extensions.map(e => this.extensionGalleryService.getManifest(e, token)));
|
||||
const uiExtensionsManifests: IExtensionManifest[] = [];
|
||||
for (let idx = 0; idx < extensions.length; idx++) {
|
||||
const extension = extensions[idx];
|
||||
const manifest = manifests[idx];
|
||||
if (manifest && isUIExtension(manifest, this.configurationService)) {
|
||||
result.set(extension.identifier.id.toLowerCase(), extension);
|
||||
uiExtensionsManifests.push(manifest);
|
||||
}
|
||||
}
|
||||
toGet = [];
|
||||
for (const uiExtensionManifest of uiExtensionsManifests) {
|
||||
if (isNonEmptyArray(uiExtensionManifest.extensionDependencies)) {
|
||||
for (const id of uiExtensionManifest.extensionDependencies) {
|
||||
if (!result.has(id.toLowerCase())) {
|
||||
toGet.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isNonEmptyArray(uiExtensionManifest.extensionPack)) {
|
||||
for (const id of uiExtensionManifest.extensionPack) {
|
||||
if (!result.has(id.toLowerCase())) {
|
||||
toGet.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.getAllUIDependenciesAndPackedExtensionsRecursively(toGet, result, token);
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IExtensionManagementService, MultiExtensionManagementService);
|
||||
|
||||
@@ -103,22 +103,33 @@ function setupProxyResolution(
|
||||
let results: ConnectionResult[] = [];
|
||||
function logEvent() {
|
||||
timeout = undefined;
|
||||
/* __GDPR__
|
||||
"resolveProxy" : {
|
||||
"count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"errorCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheSize": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheRolls": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"envCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"settingsCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"localhostCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"envNoProxyCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"results": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
|
||||
}
|
||||
*/
|
||||
mainThreadTelemetry.$publicLog('resolveProxy', { count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, envNoProxyCount, results });
|
||||
type ResolveProxyClassification = {
|
||||
count: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
errorCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
cacheCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
cacheSize: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
cacheRolls: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
envCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
settingsCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
localhostCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
envNoProxyCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true };
|
||||
results: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
|
||||
};
|
||||
type ResolveProxyEvent = {
|
||||
count: number;
|
||||
duration: number;
|
||||
errorCount: number;
|
||||
cacheCount: number;
|
||||
cacheSize: number;
|
||||
cacheRolls: number;
|
||||
envCount: number;
|
||||
settingsCount: number;
|
||||
localhostCount: number;
|
||||
envNoProxyCount: number;
|
||||
results: ConnectionResult[];
|
||||
};
|
||||
mainThreadTelemetry.$publicLog2<ResolveProxyEvent, ResolveProxyClassification>('resolveProxy', { count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, envNoProxyCount, results });
|
||||
count = duration = errorCount = cacheCount = envCount = settingsCount = localhostCount = envNoProxyCount = 0;
|
||||
results = [];
|
||||
}
|
||||
@@ -403,7 +414,7 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku
|
||||
const modules = lookup[request];
|
||||
const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath);
|
||||
if (ext && ext.enableProposedApi) {
|
||||
return modules[(<any>ext).proxySupport] || modules.onRequest;
|
||||
return (modules as any)[(<any>ext).proxySupport] || modules.onRequest;
|
||||
}
|
||||
return modules.default;
|
||||
};
|
||||
@@ -457,8 +468,8 @@ async function readCaCertificates() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readWindowsCaCertificates() {
|
||||
const winCA = require.__$__nodeRequire<any>('vscode-windows-ca-certs');
|
||||
async function readWindowsCaCertificates() {
|
||||
const winCA = await import('vscode-windows-ca-certs');
|
||||
|
||||
let ders: any[] = [];
|
||||
const store = winCA();
|
||||
@@ -471,22 +482,26 @@ function readWindowsCaCertificates() {
|
||||
store.done();
|
||||
}
|
||||
|
||||
const seen = {};
|
||||
const certs = ders.map(derToPem)
|
||||
.filter(pem => !seen[pem] && (seen[pem] = true));
|
||||
const certs = new Set(ders.map(derToPem));
|
||||
return {
|
||||
certs,
|
||||
certs: Array.from(certs),
|
||||
append: true
|
||||
};
|
||||
}
|
||||
|
||||
async function readMacCaCertificates() {
|
||||
const stdout = (await promisify(cp.execFile)('/usr/bin/security', ['find-certificate', '-a', '-p'], { encoding: 'utf8', maxBuffer: 1024 * 1024 })).stdout;
|
||||
const seen = {};
|
||||
const certs = stdout.split(/(?=-----BEGIN CERTIFICATE-----)/g)
|
||||
.filter(pem => !!pem.length && !seen[pem] && (seen[pem] = true));
|
||||
const stdout = await new Promise<string>((resolve, reject) => {
|
||||
const child = cp.spawn('/usr/bin/security', ['find-certificate', '-a', '-p']);
|
||||
const stdout: string[] = [];
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', str => stdout.push(str));
|
||||
child.on('error', reject);
|
||||
child.on('exit', code => code ? reject(code) : resolve(stdout.join('')));
|
||||
});
|
||||
const certs = new Set(stdout.split(/(?=-----BEGIN CERTIFICATE-----)/g)
|
||||
.filter(pem => !!pem.length));
|
||||
return {
|
||||
certs,
|
||||
certs: Array.from(certs),
|
||||
append: true
|
||||
};
|
||||
}
|
||||
@@ -500,11 +515,10 @@ async function readLinuxCaCertificates() {
|
||||
for (const certPath of linuxCaCertificatePaths) {
|
||||
try {
|
||||
const content = await promisify(fs.readFile)(certPath, { encoding: 'utf8' });
|
||||
const seen = {};
|
||||
const certs = content.split(/(?=-----BEGIN CERTIFICATE-----)/g)
|
||||
.filter(pem => !!pem.length && !seen[pem] && (seen[pem] = true));
|
||||
const certs = new Set(content.split(/(?=-----BEGIN CERTIFICATE-----)/g)
|
||||
.filter(pem => !!pem.length));
|
||||
return {
|
||||
certs,
|
||||
certs: Array.from(certs),
|
||||
append: false
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { localize } from 'vs/nls';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
|
||||
export class WorkspaceWatcher extends Disposable {
|
||||
|
||||
|
||||
@@ -1,38 +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 { shell } from 'electron';
|
||||
import { DiskFileSystemProvider as NodeDiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { localize } from 'vs/nls';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
|
||||
|
||||
constructor(logService: ILogService) {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
get capabilities(): FileSystemProviderCapabilities {
|
||||
if (!this._capabilities) {
|
||||
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
|
||||
}
|
||||
|
||||
return this._capabilities;
|
||||
}
|
||||
|
||||
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
|
||||
if (!opts.useTrash) {
|
||||
return super.doDelete(filePath, opts);
|
||||
}
|
||||
|
||||
const result = shell.moveItemToTrash(filePath);
|
||||
if (!result) {
|
||||
throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,490 +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 { mkdir, open, close, read, write, fdatasync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { IDisposable, Disposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { isLinux, isWindows } from 'vs/base/common/platform';
|
||||
import { statLink, readdir, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists } from 'vs/base/node/pfs';
|
||||
import { normalize, basename, dirname } from 'vs/base/common/path';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { isEqual } from 'vs/base/common/extpath';
|
||||
import { retry, ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDiskFileChange, toFileChanges } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService';
|
||||
import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService';
|
||||
import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService';
|
||||
import { FileWatcher as NodeJSWatcherService } from 'vs/workbench/services/files/node/watcher/nodejs/watcherService';
|
||||
|
||||
export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider {
|
||||
|
||||
constructor(private logService: ILogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
//#region File Capabilities
|
||||
|
||||
onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
protected _capabilities: FileSystemProviderCapabilities;
|
||||
get capabilities(): FileSystemProviderCapabilities {
|
||||
if (!this._capabilities) {
|
||||
this._capabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite |
|
||||
FileSystemProviderCapabilities.FileOpenReadWriteClose |
|
||||
FileSystemProviderCapabilities.FileFolderCopy;
|
||||
|
||||
if (isLinux) {
|
||||
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
}
|
||||
}
|
||||
|
||||
return this._capabilities;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region File Metadata Resolving
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
|
||||
|
||||
let type: number;
|
||||
if (isSymbolicLink) {
|
||||
type = FileType.SymbolicLink | (stat.isDirectory() ? FileType.Directory : FileType.File);
|
||||
} else {
|
||||
type = stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
ctime: stat.ctime.getTime(),
|
||||
mtime: stat.mtime.getTime(),
|
||||
size: stat.size
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
try {
|
||||
const children = await readdir(this.toFilePath(resource));
|
||||
|
||||
const result: [string, FileType][] = [];
|
||||
await Promise.all(children.map(async child => {
|
||||
try {
|
||||
const stat = await this.stat(joinPath(resource, child));
|
||||
result.push([child, stat.type]);
|
||||
} catch (error) {
|
||||
this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied
|
||||
}
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region File Reading/Writing
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
try {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
return await readFile(filePath);
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
let handle: number | undefined = undefined;
|
||||
try {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
// Validate target
|
||||
const fileExists = await exists(filePath);
|
||||
if (fileExists && !opts.overwrite) {
|
||||
throw createFileSystemProviderError(new Error(localize('fileExists', "File already exists")), FileSystemProviderErrorCode.FileExists);
|
||||
} else if (!fileExists && !opts.create) {
|
||||
throw createFileSystemProviderError(new Error(localize('fileNotExists', "File does not exist")), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
// Open
|
||||
handle = await this.open(resource, { create: true });
|
||||
|
||||
// Write content at once
|
||||
await this.write(handle, 0, content, 0, content.byteLength);
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
} finally {
|
||||
if (typeof handle === 'number') {
|
||||
await this.close(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private writeHandles: Set<number> = new Set();
|
||||
private canFlush: boolean = true;
|
||||
|
||||
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||
try {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
let flags: string | undefined = undefined;
|
||||
if (opts.create) {
|
||||
if (isWindows && await exists(filePath)) {
|
||||
try {
|
||||
// On Windows and if the file exists, we use a different strategy of saving the file
|
||||
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
|
||||
// (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams
|
||||
// (see https://github.com/Microsoft/vscode/issues/6363)
|
||||
await truncate(filePath, 0);
|
||||
|
||||
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
|
||||
flags = 'r+';
|
||||
} catch (error) {
|
||||
this.logService.trace(error);
|
||||
}
|
||||
}
|
||||
|
||||
// we take opts.create as a hint that the file is opened for writing
|
||||
// as such we use 'w' to truncate an existing or create the
|
||||
// file otherwise. we do not allow reading.
|
||||
if (!flags) {
|
||||
flags = 'w';
|
||||
}
|
||||
} else {
|
||||
// otherwise we assume the file is opened for reading
|
||||
// as such we use 'r' to neither truncate, nor create
|
||||
// the file.
|
||||
flags = 'r';
|
||||
}
|
||||
|
||||
const handle = await promisify(open)(filePath, flags);
|
||||
|
||||
// remember that this handle was used for writing
|
||||
if (opts.create) {
|
||||
this.writeHandles.add(handle);
|
||||
}
|
||||
|
||||
return handle;
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async close(fd: number): Promise<void> {
|
||||
try {
|
||||
// if a handle is closed that was used for writing, ensure
|
||||
// to flush the contents to disk if possible.
|
||||
if (this.writeHandles.delete(fd) && this.canFlush) {
|
||||
try {
|
||||
await promisify(fdatasync)(fd);
|
||||
} catch (error) {
|
||||
// In some exotic setups it is well possible that node fails to sync
|
||||
// In that case we disable flushing and log the error to our logger
|
||||
this.canFlush = false;
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return await promisify(close)(fd);
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
try {
|
||||
const result = await promisify(read)(fd, data, offset, length, pos);
|
||||
if (typeof result === 'number') {
|
||||
return result; // node.d.ts fail
|
||||
}
|
||||
|
||||
return result.bytesRead;
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
// we know at this point that the file to write to is truncated and thus empty
|
||||
// if the write now fails, the file remains empty. as such we really try hard
|
||||
// to ensure the write succeeds by retrying up to three times.
|
||||
return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
|
||||
}
|
||||
|
||||
private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
try {
|
||||
const result = await promisify(write)(fd, data, offset, length, pos);
|
||||
if (typeof result === 'number') {
|
||||
return result; // node.d.ts fail
|
||||
}
|
||||
|
||||
return result.bytesWritten;
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Move/Copy/Delete/Create Folder
|
||||
|
||||
async mkdir(resource: URI): Promise<void> {
|
||||
try {
|
||||
await promisify(mkdir)(this.toFilePath(resource));
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
try {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
await this.doDelete(filePath, opts);
|
||||
} catch (error) {
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
|
||||
if (opts.recursive) {
|
||||
await rimraf(filePath, RimRafMode.MOVE);
|
||||
} else {
|
||||
await unlink(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
const fromFilePath = this.toFilePath(from);
|
||||
const toFilePath = this.toFilePath(to);
|
||||
|
||||
try {
|
||||
|
||||
// Ensure target does not exist
|
||||
await this.validateTargetDeleted(from, to, opts && opts.overwrite);
|
||||
|
||||
// Move
|
||||
await move(fromFilePath, toFilePath);
|
||||
} catch (error) {
|
||||
|
||||
// rewrite some typical errors that can happen especially around symlinks
|
||||
// to something the user can better understand
|
||||
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
|
||||
error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
|
||||
}
|
||||
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
const fromFilePath = this.toFilePath(from);
|
||||
const toFilePath = this.toFilePath(to);
|
||||
|
||||
try {
|
||||
|
||||
// Ensure target does not exist
|
||||
await this.validateTargetDeleted(from, to, opts && opts.overwrite);
|
||||
|
||||
// Copy
|
||||
await copy(fromFilePath, toFilePath);
|
||||
} catch (error) {
|
||||
|
||||
// rewrite some typical errors that can happen especially around symlinks
|
||||
// to something the user can better understand
|
||||
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
|
||||
error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
|
||||
}
|
||||
|
||||
throw this.toFileSystemProviderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateTargetDeleted(from: URI, to: URI, overwrite?: boolean): Promise<void> {
|
||||
const fromFilePath = this.toFilePath(from);
|
||||
const toFilePath = this.toFilePath(to);
|
||||
|
||||
const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
|
||||
const isCaseChange = isPathCaseSensitive ? false : isEqual(fromFilePath, toFilePath, true /* ignore case */);
|
||||
|
||||
// handle existing target (unless this is a case change)
|
||||
if (!isCaseChange && await exists(toFilePath)) {
|
||||
if (!overwrite) {
|
||||
throw createFileSystemProviderError(new Error('File at target already exists'), FileSystemProviderErrorCode.FileExists);
|
||||
}
|
||||
|
||||
await this.delete(to, { recursive: true, useTrash: false });
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region File Watching
|
||||
|
||||
private _onDidWatchErrorOccur: Emitter<Error> = this._register(new Emitter<Error>());
|
||||
get onDidErrorOccur(): Event<Error> { return this._onDidWatchErrorOccur.event; }
|
||||
|
||||
private _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
|
||||
get onDidChangeFile(): Event<IFileChange[]> { return this._onDidChangeFile.event; }
|
||||
|
||||
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
|
||||
private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
|
||||
private recursiveWatchRequestDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(0));
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
if (opts.recursive) {
|
||||
return this.watchRecursive(resource, opts.excludes);
|
||||
}
|
||||
|
||||
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
|
||||
}
|
||||
|
||||
private watchRecursive(resource: URI, excludes: string[]): IDisposable {
|
||||
|
||||
// Add to list of folders to watch recursively
|
||||
const folderToWatch = { path: this.toFilePath(resource), excludes };
|
||||
this.recursiveFoldersToWatch.push(folderToWatch);
|
||||
|
||||
// Trigger update
|
||||
this.refreshRecursiveWatchers();
|
||||
|
||||
return toDisposable(() => {
|
||||
|
||||
// Remove from list of folders to watch recursively
|
||||
this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1);
|
||||
|
||||
// Trigger update
|
||||
this.refreshRecursiveWatchers();
|
||||
});
|
||||
}
|
||||
|
||||
private refreshRecursiveWatchers(): void {
|
||||
|
||||
// Buffer requests for recursive watching to decide on right watcher
|
||||
// that supports potentially watching more than one folder at once
|
||||
this.recursiveWatchRequestDelayer.trigger(() => {
|
||||
this.doRefreshRecursiveWatchers();
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private doRefreshRecursiveWatchers(): void {
|
||||
|
||||
// Reuse existing
|
||||
if (this.recursiveWatcher instanceof NsfwWatcherService) {
|
||||
this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch);
|
||||
}
|
||||
|
||||
// Create new
|
||||
else {
|
||||
|
||||
// Dispose old
|
||||
dispose(this.recursiveWatcher);
|
||||
|
||||
// Create new if we actually have folders to watch
|
||||
if (this.recursiveFoldersToWatch.length > 0) {
|
||||
let watcherImpl: {
|
||||
new(
|
||||
folders: { path: string, excludes: string[] }[],
|
||||
onChange: (changes: IDiskFileChange[]) => void,
|
||||
onError: (msg: string) => void,
|
||||
verboseLogging: boolean
|
||||
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
|
||||
};
|
||||
|
||||
// Single Folder Watcher
|
||||
if (this.recursiveFoldersToWatch.length === 1) {
|
||||
if (isWindows) {
|
||||
watcherImpl = WindowsWatcherService;
|
||||
} else {
|
||||
watcherImpl = UnixWatcherService;
|
||||
}
|
||||
}
|
||||
|
||||
// Multi Folder Watcher
|
||||
else {
|
||||
watcherImpl = NsfwWatcherService;
|
||||
}
|
||||
|
||||
// Create and start watching
|
||||
this.recursiveWatcher = new watcherImpl(
|
||||
this.recursiveFoldersToWatch,
|
||||
event => this._onDidChangeFile.fire(toFileChanges(event)),
|
||||
error => this._onDidWatchErrorOccur.fire(new Error(error)),
|
||||
this.logService.getLevel() === LogLevel.Trace
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private watchNonRecursive(resource: URI): IDisposable {
|
||||
return new NodeJSWatcherService(
|
||||
this.toFilePath(resource),
|
||||
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
|
||||
error => this._onDidWatchErrorOccur.fire(new Error(error)),
|
||||
info => this.logService.trace(info),
|
||||
this.logService.getLevel() === LogLevel.Trace
|
||||
);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Helpers
|
||||
|
||||
protected toFilePath(resource: URI): string {
|
||||
return normalize(resource.fsPath);
|
||||
}
|
||||
|
||||
private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {
|
||||
if (error instanceof FileSystemProviderError) {
|
||||
return error; // avoid double conversion
|
||||
}
|
||||
|
||||
let code: FileSystemProviderErrorCode;
|
||||
switch (error.code) {
|
||||
case 'ENOENT':
|
||||
code = FileSystemProviderErrorCode.FileNotFound;
|
||||
break;
|
||||
case 'EISDIR':
|
||||
code = FileSystemProviderErrorCode.FileIsADirectory;
|
||||
break;
|
||||
case 'EEXIST':
|
||||
code = FileSystemProviderErrorCode.FileExists;
|
||||
break;
|
||||
case 'EPERM':
|
||||
case 'EACCES':
|
||||
code = FileSystemProviderErrorCode.NoPermissions;
|
||||
break;
|
||||
default:
|
||||
code = FileSystemProviderErrorCode.Unknown;
|
||||
}
|
||||
|
||||
return createFileSystemProviderError(error, code);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
dispose(this.recursiveWatcher);
|
||||
this.recursiveWatcher = undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +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 { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { statLink } from 'vs/base/node/pfs';
|
||||
import { realpath } from 'vs/base/node/extpath';
|
||||
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { join, basename } from 'vs/base/common/path';
|
||||
|
||||
export class FileWatcher extends Disposable {
|
||||
private isDisposed: boolean;
|
||||
|
||||
private fileChangesDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */));
|
||||
private fileChangesBuffer: IDiskFileChange[] = [];
|
||||
|
||||
constructor(
|
||||
private path: string,
|
||||
private onFileChanges: (changes: IDiskFileChange[]) => void,
|
||||
private errorLogger: (msg: string) => void,
|
||||
private verboseLogger: (msg: string) => void,
|
||||
private verboseLogging: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this.startWatching();
|
||||
}
|
||||
|
||||
private async startWatching(): Promise<void> {
|
||||
try {
|
||||
const { stat, isSymbolicLink } = await statLink(this.path);
|
||||
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pathToWatch = this.path;
|
||||
if (isSymbolicLink) {
|
||||
try {
|
||||
pathToWatch = await realpath(pathToWatch);
|
||||
} catch (error) {
|
||||
this.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Watch Folder
|
||||
if (stat.isDirectory()) {
|
||||
this._register(watchFolder(pathToWatch, (eventType, path) => {
|
||||
this.onFileChange({
|
||||
type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED,
|
||||
path: join(this.path, basename(path)) // ensure path is identical with what was passed in
|
||||
});
|
||||
}, error => this.onError(error)));
|
||||
}
|
||||
|
||||
// Watch File
|
||||
else {
|
||||
this._register(watchFile(pathToWatch, eventType => {
|
||||
this.onFileChange({
|
||||
type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED,
|
||||
path: this.path // ensure path is identical with what was passed in
|
||||
});
|
||||
}, error => this.onError(error)));
|
||||
}
|
||||
} catch (error) {
|
||||
this.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private onFileChange(event: IDiskFileChange): void {
|
||||
|
||||
// Add to buffer
|
||||
this.fileChangesBuffer.push(event);
|
||||
|
||||
// Logging
|
||||
if (this.verboseLogging) {
|
||||
this.onVerbose(`[File Watcher (node.js)] ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
|
||||
}
|
||||
|
||||
// Handle emit through delayer to accommodate for bulk changes and thus reduce spam
|
||||
this.fileChangesDelayer.trigger(() => {
|
||||
const fileChanges = this.fileChangesBuffer;
|
||||
this.fileChangesBuffer = [];
|
||||
|
||||
// Event normalization
|
||||
const normalizedFileChanges = normalizeFileChanges(fileChanges);
|
||||
|
||||
// Logging
|
||||
if (this.verboseLogging) {
|
||||
normalizedFileChanges.forEach(event => {
|
||||
this.onVerbose(`[File Watcher (node.js)] >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Fire
|
||||
if (normalizedFileChanges.length > 0) {
|
||||
this.onFileChanges(normalizedFileChanges);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private onError(error: string): void {
|
||||
if (!this.isDisposed) {
|
||||
this.errorLogger(error);
|
||||
}
|
||||
}
|
||||
|
||||
private onVerbose(msg: string): void {
|
||||
if (!this.isDisposed) {
|
||||
this.verboseLogger(msg);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,243 +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 glob from 'vs/base/common/glob';
|
||||
import * as extpath from 'vs/base/common/extpath';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import * as nsfw from 'vscode-nsfw';
|
||||
import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
|
||||
|
||||
const nsfwActionToRawChangeType: { [key: number]: number } = [];
|
||||
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
|
||||
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
|
||||
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
|
||||
|
||||
interface IWatcherObjet {
|
||||
start(): any;
|
||||
stop(): any;
|
||||
}
|
||||
|
||||
interface IPathWatcher {
|
||||
ready: Promise<IWatcherObjet>;
|
||||
watcher?: IWatcherObjet;
|
||||
ignored: glob.ParsedPattern[];
|
||||
}
|
||||
|
||||
export class NsfwWatcherService implements IWatcherService {
|
||||
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
|
||||
|
||||
private _pathWatchers: { [watchPath: string]: IPathWatcher } = {};
|
||||
private _verboseLogging: boolean;
|
||||
private enospcErrorLogged: boolean;
|
||||
|
||||
private _onWatchEvent = new Emitter<IDiskFileChange[] | IWatchError>();
|
||||
readonly onWatchEvent = this._onWatchEvent.event;
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
|
||||
this._verboseLogging = options.verboseLogging;
|
||||
return this.onWatchEvent;
|
||||
}
|
||||
|
||||
private _watch(request: IWatcherRequest): void {
|
||||
let undeliveredFileEvents: IDiskFileChange[] = [];
|
||||
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
|
||||
|
||||
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
|
||||
this._pathWatchers[request.path] = {
|
||||
ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
|
||||
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
|
||||
};
|
||||
|
||||
process.on('uncaughtException', (e: Error | string) => {
|
||||
|
||||
// Specially handle ENOSPC errors that can happen when
|
||||
// the watcher consumes so many file descriptors that
|
||||
// we are running into a limit. We only want to warn
|
||||
// once in this case to avoid log spam.
|
||||
// See https://github.com/Microsoft/vscode/issues/7950
|
||||
if (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
|
||||
this.enospcErrorLogged = true;
|
||||
this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' });
|
||||
}
|
||||
});
|
||||
|
||||
// NSFW does not report file changes in the path provided on macOS if
|
||||
// - the path uses wrong casing
|
||||
// - the path is a symbolic link
|
||||
// We have to detect this case and massage the events to correct this.
|
||||
let realBasePathDiffers = false;
|
||||
let realBasePathLength = request.path.length;
|
||||
if (platform.isMacintosh) {
|
||||
try {
|
||||
|
||||
// First check for symbolic link
|
||||
let realBasePath = realpathSync(request.path);
|
||||
|
||||
// Second check for casing difference
|
||||
if (request.path === realBasePath) {
|
||||
realBasePath = (realcaseSync(request.path) || request.path);
|
||||
}
|
||||
|
||||
if (request.path !== realBasePath) {
|
||||
realBasePathLength = realBasePath.length;
|
||||
realBasePathDiffers = true;
|
||||
|
||||
console.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
nsfw(request.path, events => {
|
||||
for (const e of events) {
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
|
||||
console.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
|
||||
}
|
||||
|
||||
// Convert nsfw event to IRawFileChange and add to queue
|
||||
let absolutePath: string;
|
||||
if (e.action === nsfw.actions.RENAMED) {
|
||||
// Rename fires when a file's name changes within a single directory
|
||||
absolutePath = path.join(e.directory, e.oldFile || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
|
||||
} else if (this._verboseLogging) {
|
||||
console.log(' >> ignored', absolutePath);
|
||||
}
|
||||
absolutePath = path.join(e.directory, e.newFile || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
|
||||
} else if (this._verboseLogging) {
|
||||
console.log(' >> ignored', absolutePath);
|
||||
}
|
||||
} else {
|
||||
absolutePath = path.join(e.directory, e.file || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({
|
||||
type: nsfwActionToRawChangeType[e.action],
|
||||
path: absolutePath
|
||||
});
|
||||
} else if (this._verboseLogging) {
|
||||
console.log(' >> ignored', absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delay and send buffer
|
||||
fileEventDelayer.trigger(() => {
|
||||
const events = undeliveredFileEvents;
|
||||
undeliveredFileEvents = [];
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
events.forEach(e => {
|
||||
|
||||
// Mac uses NFD unicode form on disk, but we want NFC
|
||||
e.path = normalizeNFC(e.path);
|
||||
|
||||
// Convert paths back to original form in case it differs
|
||||
if (realBasePathDiffers) {
|
||||
e.path = request.path + e.path.substr(realBasePathLength);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Broadcast to clients normalized
|
||||
const res = normalizeFileChanges(events);
|
||||
this._onWatchEvent.fire(res);
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
res.forEach(r => {
|
||||
console.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
}).then(watcher => {
|
||||
this._pathWatchers[request.path].watcher = watcher;
|
||||
const startPromise = watcher.start();
|
||||
startPromise.then(() => readyPromiseResolve(watcher));
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
|
||||
public setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
const normalizedRoots = this._normalizeRoots(roots);
|
||||
|
||||
// Gather roots that are not currently being watched
|
||||
const rootsToStartWatching = normalizedRoots.filter(r => {
|
||||
return !(r.path in this._pathWatchers);
|
||||
});
|
||||
|
||||
// Gather current roots that don't exist in the new roots array
|
||||
const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => {
|
||||
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
|
||||
});
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
console.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
|
||||
}
|
||||
|
||||
// Stop watching some roots
|
||||
rootsToStopWatching.forEach(root => {
|
||||
this._pathWatchers[root].ready.then(watcher => watcher.stop());
|
||||
delete this._pathWatchers[root];
|
||||
});
|
||||
|
||||
// Start watching some roots
|
||||
rootsToStartWatching.forEach(root => this._watch(root));
|
||||
|
||||
// Refresh ignored arrays in case they changed
|
||||
roots.forEach(root => {
|
||||
if (root.path in this._pathWatchers) {
|
||||
this._pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
}
|
||||
|
||||
public setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
this._verboseLogging = enabled;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public stop(): Promise<void> {
|
||||
for (let path in this._pathWatchers) {
|
||||
let watcher = this._pathWatchers[path];
|
||||
watcher.ready.then(watcher => watcher.stop());
|
||||
delete this._pathWatchers[path];
|
||||
}
|
||||
this._pathWatchers = Object.create(null);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a set of root paths by removing any root paths that are
|
||||
* sub-paths of other roots.
|
||||
*/
|
||||
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
|
||||
return roots.filter(r => roots.every(other => {
|
||||
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
|
||||
}));
|
||||
}
|
||||
|
||||
private _isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
|
||||
return ignored && ignored.some(i => i(absolutePath));
|
||||
}
|
||||
}
|
||||
@@ -1,52 +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 { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
|
||||
import { IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
|
||||
|
||||
class TestNsfwWatcherService extends NsfwWatcherService {
|
||||
public normalizeRoots(roots: string[]): string[] {
|
||||
// Work with strings as paths to simplify testing
|
||||
const requests: IWatcherRequest[] = roots.map(r => {
|
||||
return { path: r, excludes: [] };
|
||||
});
|
||||
return this._normalizeRoots(requests).map(r => r.path);
|
||||
}
|
||||
}
|
||||
|
||||
suite('NSFW Watcher Service', () => {
|
||||
suite('_normalizeRoots', () => {
|
||||
test('should not impacts roots that don\'t overlap', () => {
|
||||
const service = new TestNsfwWatcherService();
|
||||
if (platform.isWindows) {
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
|
||||
} else {
|
||||
assert.deepEqual(service.normalizeRoots(['/a']), ['/a']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
|
||||
}
|
||||
});
|
||||
|
||||
test('should remove sub-folders of other roots', () => {
|
||||
const service = new TestNsfwWatcherService();
|
||||
if (platform.isWindows) {
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
|
||||
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
|
||||
} else {
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
|
||||
assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
|
||||
assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,27 +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 { Event } from 'vs/base/common/event';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
|
||||
export interface IWatcherRequest {
|
||||
path: string;
|
||||
excludes: string[];
|
||||
}
|
||||
|
||||
export interface IWatcherOptions {
|
||||
verboseLogging: boolean;
|
||||
}
|
||||
|
||||
export interface IWatchError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IWatcherService {
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError>;
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void>;
|
||||
setVerboseLogging(enabled: boolean): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
@@ -1,13 +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 { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
|
||||
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new NsfwWatcherService();
|
||||
const channel = new WatcherChannel(service);
|
||||
server.registerChannel('watcher', channel);
|
||||
@@ -1,53 +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 { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
|
||||
export class WatcherChannel implements IServerChannel {
|
||||
|
||||
constructor(private service: IWatcherService) { }
|
||||
|
||||
listen(_: unknown, event: string, arg?: any): Event<any> {
|
||||
switch (event) {
|
||||
case 'watch': return this.service.watch(arg);
|
||||
}
|
||||
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_: unknown, command: string, arg?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'setRoots': return this.service.setRoots(arg);
|
||||
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
|
||||
case 'stop': return this.service.stop();
|
||||
}
|
||||
|
||||
throw new Error(`Call not found: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class WatcherChannelClient implements IWatcherService {
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
|
||||
return this.channel.listen('watch', options);
|
||||
}
|
||||
|
||||
setVerboseLogging(enable: boolean): Promise<void> {
|
||||
return this.channel.call('setVerboseLogging', enable);
|
||||
}
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
return this.channel.call('setRoots', roots);
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return this.channel.call('stop');
|
||||
}
|
||||
}
|
||||
@@ -1,92 +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 { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IWatchError, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
export class FileWatcher extends Disposable {
|
||||
private static readonly MAX_RESTARTS = 5;
|
||||
|
||||
private service: WatcherChannelClient;
|
||||
private isDisposed: boolean;
|
||||
private restartCounter: number;
|
||||
|
||||
constructor(
|
||||
private folders: IWatcherRequest[],
|
||||
private onFileChanges: (changes: IDiskFileChange[]) => void,
|
||||
private errorLogger: (msg: string) => void,
|
||||
private verboseLogging: boolean,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.isDisposed = false;
|
||||
this.restartCounter = 0;
|
||||
|
||||
this.startWatching();
|
||||
}
|
||||
|
||||
private startWatching(): void {
|
||||
const client = this._register(new Client(
|
||||
getPathFromAmdModule(require, 'bootstrap-fork'),
|
||||
{
|
||||
serverName: 'File Watcher (nsfw)',
|
||||
args: ['--type=watcherService'],
|
||||
env: {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: this.verboseLogging
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
this._register(client.onDidProcessExit(() => {
|
||||
// our watcher app should never be completed because it keeps on watching. being in here indicates
|
||||
// that the watcher process died and we want to restart it here. we only do it a max number of times
|
||||
if (!this.isDisposed) {
|
||||
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
|
||||
this.errorLogger('[File Watcher (nsfw)] terminated unexpectedly and is restarted again...');
|
||||
this.restartCounter++;
|
||||
this.startWatching();
|
||||
} else {
|
||||
this.errorLogger('[File Watcher (nsfw)] failed to start after retrying for some time, giving up. Please report this as a bug report!');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialize watcher
|
||||
const channel = getNextTickChannel(client.getChannel('watcher'));
|
||||
this.service = new WatcherChannelClient(channel);
|
||||
|
||||
const options = { verboseLogging: this.verboseLogging };
|
||||
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
|
||||
|
||||
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
|
||||
this._register(onError(err => this.errorLogger(`[File Watcher (nsfw)] ${err.message}`)));
|
||||
|
||||
const onFileChanges = Event.filter<any, IDiskFileChange[]>(onWatchEvent, (e): e is IDiskFileChange[] => Array.isArray(e) && e.length > 0);
|
||||
this._register(onFileChanges(e => this.onFileChanges(e)));
|
||||
|
||||
// Start watching
|
||||
this.setFolders(this.folders);
|
||||
}
|
||||
|
||||
setFolders(folders: IWatcherRequest[]): void {
|
||||
this.folders = folders;
|
||||
|
||||
this.service.setRoots(folders);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,359 +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 chokidar from 'vscode-chokidar';
|
||||
import * as fs from 'fs';
|
||||
import * as gracefulFs from 'graceful-fs';
|
||||
gracefulFs.gracefulify(fs);
|
||||
import * as extpath from 'vs/base/common/extpath';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { realcaseSync } from 'vs/base/node/extpath';
|
||||
import { isMacintosh, isLinux } from 'vs/base/common/platform';
|
||||
import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
||||
interface IWatcher {
|
||||
requests: ExtendedWatcherRequest[];
|
||||
stop(): any;
|
||||
}
|
||||
|
||||
export interface IChockidarWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
}
|
||||
|
||||
interface ExtendedWatcherRequest extends IWatcherRequest {
|
||||
parsedPattern?: glob.ParsedPattern;
|
||||
}
|
||||
|
||||
export class ChokidarWatcherService implements IWatcherService {
|
||||
|
||||
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
|
||||
private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam
|
||||
|
||||
private _watchers: { [watchPath: string]: IWatcher };
|
||||
private _watcherCount: number;
|
||||
|
||||
private _pollingInterval?: number;
|
||||
private _verboseLogging: boolean;
|
||||
|
||||
private spamCheckStartTime: number;
|
||||
private spamWarningLogged: boolean;
|
||||
private enospcErrorLogged: boolean;
|
||||
|
||||
private _onWatchEvent = new Emitter<IDiskFileChange[] | IWatchError>();
|
||||
readonly onWatchEvent = this._onWatchEvent.event;
|
||||
|
||||
public watch(options: IWatcherOptions & IChockidarWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
|
||||
this._verboseLogging = options.verboseLogging;
|
||||
this._pollingInterval = options.pollingInterval;
|
||||
this._watchers = Object.create(null);
|
||||
this._watcherCount = 0;
|
||||
return this.onWatchEvent;
|
||||
}
|
||||
|
||||
public setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
this._verboseLogging = enabled;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public setRoots(requests: IWatcherRequest[]): Promise<void> {
|
||||
const watchers = Object.create(null);
|
||||
const newRequests: string[] = [];
|
||||
|
||||
const requestsByBasePath = normalizeRoots(requests);
|
||||
|
||||
// evaluate new & remaining watchers
|
||||
for (let basePath in requestsByBasePath) {
|
||||
let watcher = this._watchers[basePath];
|
||||
if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) {
|
||||
watchers[basePath] = watcher;
|
||||
delete this._watchers[basePath];
|
||||
} else {
|
||||
newRequests.push(basePath);
|
||||
}
|
||||
}
|
||||
// stop all old watchers
|
||||
for (let path in this._watchers) {
|
||||
this._watchers[path].stop();
|
||||
}
|
||||
// start all new watchers
|
||||
for (let basePath of newRequests) {
|
||||
let requests = requestsByBasePath[basePath];
|
||||
watchers[basePath] = this._watch(basePath, requests);
|
||||
}
|
||||
|
||||
this._watchers = watchers;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// for test purposes
|
||||
public get wacherCount() {
|
||||
return this._watcherCount;
|
||||
}
|
||||
|
||||
private _watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
|
||||
if (this._verboseLogging) {
|
||||
console.log(`Start watching: ${basePath}]`);
|
||||
}
|
||||
|
||||
const pollingInterval = this._pollingInterval || 1000;
|
||||
|
||||
const watcherOpts: chokidar.IOptions = {
|
||||
ignoreInitial: true,
|
||||
ignorePermissionErrors: true,
|
||||
followSymlinks: true, // this is the default of chokidar and supports file events through symlinks
|
||||
interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
|
||||
binaryInterval: pollingInterval,
|
||||
disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586
|
||||
};
|
||||
|
||||
const excludes: string[] = [];
|
||||
// if there's only one request, use the built-in ignore-filterering
|
||||
const isSingleFolder = requests.length === 1;
|
||||
if (isSingleFolder) {
|
||||
excludes.push(...requests[0].excludes);
|
||||
}
|
||||
|
||||
if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) {
|
||||
excludes.push('/dev/**');
|
||||
if (isLinux) {
|
||||
excludes.push('/proc/**', '/sys/**');
|
||||
}
|
||||
}
|
||||
watcherOpts.ignored = excludes;
|
||||
|
||||
// Chokidar fails when the basePath does not match case-identical to the path on disk
|
||||
// so we have to find the real casing of the path and do some path massaging to fix this
|
||||
// see https://github.com/paulmillr/chokidar/issues/418
|
||||
const realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath;
|
||||
const realBasePathLength = realBasePath.length;
|
||||
const realBasePathDiffers = (basePath !== realBasePath);
|
||||
|
||||
if (realBasePathDiffers) {
|
||||
console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
|
||||
}
|
||||
|
||||
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
|
||||
this._watcherCount++;
|
||||
|
||||
// Detect if for some reason the native watcher library fails to load
|
||||
if (isMacintosh && !chokidarWatcher.options.useFsEvents) {
|
||||
console.error('Watcher is not using native fsevents library and is falling back to unefficient polling.');
|
||||
}
|
||||
|
||||
let undeliveredFileEvents: IDiskFileChange[] = [];
|
||||
let fileEventDelayer: ThrottledDelayer<undefined> | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
|
||||
|
||||
const watcher: IWatcher = {
|
||||
requests,
|
||||
stop: () => {
|
||||
try {
|
||||
if (this._verboseLogging) {
|
||||
console.log(`Stop watching: ${basePath}]`);
|
||||
}
|
||||
if (chokidarWatcher) {
|
||||
chokidarWatcher.close();
|
||||
this._watcherCount--;
|
||||
chokidarWatcher = null;
|
||||
}
|
||||
if (fileEventDelayer) {
|
||||
fileEventDelayer.cancel();
|
||||
fileEventDelayer = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chokidarWatcher.on('all', (type: string, path: string) => {
|
||||
if (isMacintosh) {
|
||||
// Mac: uses NFD unicode form on disk, but we want NFC
|
||||
// See also https://github.com/nodejs/node/issues/2165
|
||||
path = normalizeNFC(path);
|
||||
}
|
||||
|
||||
if (path.indexOf(realBasePath) < 0) {
|
||||
return; // we really only care about absolute paths here in our basepath context here
|
||||
}
|
||||
|
||||
// Make sure to convert the path back to its original basePath form if the realpath is different
|
||||
if (realBasePathDiffers) {
|
||||
path = basePath + path.substr(realBasePathLength);
|
||||
}
|
||||
|
||||
let eventType: FileChangeType;
|
||||
switch (type) {
|
||||
case 'change':
|
||||
eventType = FileChangeType.UPDATED;
|
||||
break;
|
||||
case 'add':
|
||||
case 'addDir':
|
||||
eventType = FileChangeType.ADDED;
|
||||
break;
|
||||
case 'unlink':
|
||||
case 'unlinkDir':
|
||||
eventType = FileChangeType.DELETED;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// if there's more than one request we need to do
|
||||
// extra filtering due to potentially overlapping roots
|
||||
if (!isSingleFolder) {
|
||||
if (isIgnored(path, watcher.requests)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let event = { type: eventType, path };
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
console.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`);
|
||||
}
|
||||
|
||||
// Check for spam
|
||||
const now = Date.now();
|
||||
if (undeliveredFileEvents.length === 0) {
|
||||
this.spamWarningLogged = false;
|
||||
this.spamCheckStartTime = now;
|
||||
} else if (!this.spamWarningLogged && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) {
|
||||
this.spamWarningLogged = true;
|
||||
console.warn(`Watcher is busy catching up with ${undeliveredFileEvents.length} file changes in 60 seconds. Latest changed path is "${event.path}"`);
|
||||
}
|
||||
|
||||
// Add to buffer
|
||||
undeliveredFileEvents.push(event);
|
||||
|
||||
if (fileEventDelayer) {
|
||||
// Delay and send buffer
|
||||
fileEventDelayer.trigger(() => {
|
||||
const events = undeliveredFileEvents;
|
||||
undeliveredFileEvents = [];
|
||||
|
||||
// Broadcast to clients normalized
|
||||
const res = normalizeFileChanges(events);
|
||||
this._onWatchEvent.fire(res);
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
res.forEach(r => {
|
||||
console.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error) {
|
||||
|
||||
// Specially handle ENOSPC errors that can happen when
|
||||
// the watcher consumes so many file descriptors that
|
||||
// we are running into a limit. We only want to warn
|
||||
// once in this case to avoid log spam.
|
||||
// See https://github.com/Microsoft/vscode/issues/7950
|
||||
if (error.code === 'ENOSPC') {
|
||||
if (!this.enospcErrorLogged) {
|
||||
this.enospcErrorLogged = true;
|
||||
this.stop();
|
||||
this._onWatchEvent.fire({ message: 'Inotify limit reached (ENOSPC)' });
|
||||
}
|
||||
} else {
|
||||
console.error(error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
return watcher;
|
||||
}
|
||||
|
||||
public stop(): Promise<void> {
|
||||
for (let path in this._watchers) {
|
||||
let watcher = this._watchers[path];
|
||||
watcher.stop();
|
||||
}
|
||||
this._watchers = Object.create(null);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
|
||||
for (let request of requests) {
|
||||
if (request.path === path) {
|
||||
return false;
|
||||
}
|
||||
if (extpath.isEqualOrParent(path, request.path)) {
|
||||
if (!request.parsedPattern) {
|
||||
if (request.excludes && request.excludes.length > 0) {
|
||||
let pattern = `{${request.excludes.join(',')}}`;
|
||||
request.parsedPattern = glob.parse(pattern);
|
||||
} else {
|
||||
request.parsedPattern = () => false;
|
||||
}
|
||||
}
|
||||
const relPath = path.substr(request.path.length + 1);
|
||||
if (!request.parsedPattern(relPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a set of root paths by grouping by the most parent root path.
|
||||
* equests with Sub paths are skipped if they have the same ignored set as the parent.
|
||||
*/
|
||||
export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } {
|
||||
requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
let prevRequest: IWatcherRequest | null = null;
|
||||
let result: { [basePath: string]: IWatcherRequest[] } = Object.create(null);
|
||||
for (let request of requests) {
|
||||
let basePath = request.path;
|
||||
let ignored = (request.excludes || []).sort();
|
||||
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) {
|
||||
if (!isEqualIgnore(ignored, prevRequest.excludes)) {
|
||||
result[prevRequest.path].push({ path: basePath, excludes: ignored });
|
||||
}
|
||||
} else {
|
||||
prevRequest = { path: basePath, excludes: ignored };
|
||||
result[basePath] = [prevRequest];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isEqualRequests(r1: IWatcherRequest[], r2: IWatcherRequest[]) {
|
||||
if (r1.length !== r2.length) {
|
||||
return false;
|
||||
}
|
||||
for (let k = 0; k < r1.length; k++) {
|
||||
if (r1[k].path !== r2[k].path || !isEqualIgnore(r1[k].excludes, r2[k].excludes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isEqualIgnore(i1: string[], i2: string[]) {
|
||||
if (i1.length !== i2.length) {
|
||||
return false;
|
||||
}
|
||||
for (let k = 0; k < i1.length; k++) {
|
||||
if (i1[k] !== i2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,319 +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 os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { normalizeRoots, ChokidarWatcherService } from '../chokidarWatcherService';
|
||||
import { IWatcherRequest } from '../watcher';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
|
||||
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
|
||||
return { path: basePath, excludes: ignored };
|
||||
}
|
||||
|
||||
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
|
||||
const requests = inputPaths.map(path => newRequest(path));
|
||||
const actual = normalizeRoots(requests);
|
||||
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
|
||||
}
|
||||
|
||||
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
|
||||
const actual = normalizeRoots(inputRequests);
|
||||
const actualPath = Object.keys(actual).sort();
|
||||
const expectedPaths = Object.keys(expectedRequests).sort();
|
||||
assert.deepEqual(actualPath, expectedPaths);
|
||||
for (let path of actualPath) {
|
||||
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
assert.deepEqual(a, e);
|
||||
}
|
||||
}
|
||||
|
||||
function sort(changes: IDiskFileChange[]) {
|
||||
return changes.sort((c1, c2) => {
|
||||
return c1.path.localeCompare(c2.path);
|
||||
});
|
||||
}
|
||||
|
||||
function wait(time: number) {
|
||||
return new Delayer<void>(time).trigger(() => { });
|
||||
}
|
||||
|
||||
async function assertFileEvents(actuals: IDiskFileChange[], expected: IDiskFileChange[]) {
|
||||
let repeats = 40;
|
||||
while ((actuals.length < expected.length) && repeats-- > 0) {
|
||||
await wait(50);
|
||||
}
|
||||
assert.deepEqual(sort(actuals), sort(expected));
|
||||
actuals.length = 0;
|
||||
}
|
||||
|
||||
suite('Chockidar normalizeRoots', () => {
|
||||
test('should not impacts roots that don\'t overlap', () => {
|
||||
if (platform.isWindows) {
|
||||
assertNormalizedRootPath(['C:\\a'], ['C:\\a']);
|
||||
assertNormalizedRootPath(['C:\\a', 'C:\\b'], ['C:\\a', 'C:\\b']);
|
||||
assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\c\\d\\e'], ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
|
||||
} else {
|
||||
assertNormalizedRootPath(['/a'], ['/a']);
|
||||
assertNormalizedRootPath(['/a', '/b'], ['/a', '/b']);
|
||||
assertNormalizedRootPath(['/a', '/b', '/c/d/e'], ['/a', '/b', '/c/d/e']);
|
||||
}
|
||||
});
|
||||
|
||||
test('should remove sub-folders of other roots', () => {
|
||||
if (platform.isWindows) {
|
||||
assertNormalizedRootPath(['C:\\a', 'C:\\a\\b'], ['C:\\a']);
|
||||
assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']);
|
||||
assertNormalizedRootPath(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']);
|
||||
assertNormalizedRootPath(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d'], ['C:\\a']);
|
||||
} else {
|
||||
assertNormalizedRootPath(['/a', '/a/b'], ['/a']);
|
||||
assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']);
|
||||
assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']);
|
||||
assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']);
|
||||
assertNormalizedRootPath(['/a/c/d/e', '/a/b/d', '/a/c/d', '/a/c/e/f', '/a/b'], ['/a/b', '/a/c/d', '/a/c/e/f']);
|
||||
}
|
||||
});
|
||||
|
||||
test('should remove duplicates', () => {
|
||||
if (platform.isWindows) {
|
||||
assertNormalizedRootPath(['C:\\a', 'C:\\a\\', 'C:\\a'], ['C:\\a']);
|
||||
} else {
|
||||
assertNormalizedRootPath(['/a', '/a/', '/a'], ['/a']);
|
||||
assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']);
|
||||
assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']);
|
||||
assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']);
|
||||
}
|
||||
});
|
||||
|
||||
test('nested requests', () => {
|
||||
let p1, p2, p3;
|
||||
if (platform.isWindows) {
|
||||
p1 = 'C:\\a';
|
||||
p2 = 'C:\\a\\b';
|
||||
p3 = 'C:\\a\\b\\c';
|
||||
} else {
|
||||
p1 = '/a';
|
||||
p2 = '/a/b';
|
||||
p3 = '/a/b/c';
|
||||
}
|
||||
const r1 = newRequest(p1, ['**/*.ts']);
|
||||
const r2 = newRequest(p2, ['**/*.js']);
|
||||
const r3 = newRequest(p3, ['**/*.ts']);
|
||||
assertNormalizedRequests([r1, r2], { [p1]: [r1, r2] });
|
||||
assertNormalizedRequests([r2, r1], { [p1]: [r1, r2] });
|
||||
assertNormalizedRequests([r1, r2, r3], { [p1]: [r1, r2, r3] });
|
||||
assertNormalizedRequests([r1, r3], { [p1]: [r1] });
|
||||
assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] });
|
||||
});
|
||||
});
|
||||
|
||||
suite.skip('Chockidar watching', () => {
|
||||
const tmpdir = os.tmpdir();
|
||||
const testDir = path.join(tmpdir, 'chockidartest-' + Date.now());
|
||||
const aFolder = path.join(testDir, 'a');
|
||||
const bFolder = path.join(testDir, 'b');
|
||||
const b2Folder = path.join(bFolder, 'b2');
|
||||
|
||||
const service = new ChokidarWatcherService();
|
||||
const result: IDiskFileChange[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
suiteSetup(async () => {
|
||||
await pfs.mkdirp(testDir);
|
||||
await pfs.mkdirp(aFolder);
|
||||
await pfs.mkdirp(bFolder);
|
||||
await pfs.mkdirp(b2Folder);
|
||||
|
||||
const opts = { verboseLogging: false, pollingInterval: 200 };
|
||||
service.watch(opts)(e => {
|
||||
if (Array.isArray(e)) {
|
||||
result.push(...e);
|
||||
} else {
|
||||
console.log('set error', e.message);
|
||||
error = e.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
await pfs.rimraf(testDir, pfs.RimRafMode.MOVE);
|
||||
await service.stop();
|
||||
});
|
||||
|
||||
setup(() => {
|
||||
result.length = 0;
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test('simple file operations, single root, no ignore', async () => {
|
||||
let request: IWatcherRequest = { path: testDir, excludes: [] };
|
||||
service.setRoots([request]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// create a file
|
||||
let testFilePath = path.join(testDir, 'file.txt');
|
||||
await pfs.writeFile(testFilePath, '');
|
||||
await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.ADDED }]);
|
||||
|
||||
// modify a file
|
||||
await pfs.writeFile(testFilePath, 'Hello');
|
||||
await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.UPDATED }]);
|
||||
|
||||
// create a folder
|
||||
let testFolderPath = path.join(testDir, 'newFolder');
|
||||
await pfs.mkdirp(testFolderPath);
|
||||
// copy a file
|
||||
let copiedFilePath = path.join(testFolderPath, 'file2.txt');
|
||||
await pfs.copy(testFilePath, copiedFilePath);
|
||||
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.ADDED }, { path: testFolderPath, type: FileChangeType.ADDED }]);
|
||||
|
||||
// delete a file
|
||||
await pfs.rimraf(copiedFilePath, pfs.RimRafMode.MOVE);
|
||||
let renamedFilePath = path.join(testFolderPath, 'file3.txt');
|
||||
// move a file
|
||||
await pfs.rename(testFilePath, renamedFilePath);
|
||||
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.DELETED }, { path: testFilePath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.ADDED }]);
|
||||
|
||||
// delete a folder
|
||||
await pfs.rimraf(testFolderPath, pfs.RimRafMode.MOVE);
|
||||
await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
test('simple file operations, ignore', async () => {
|
||||
let request: IWatcherRequest = { path: testDir, excludes: ['**/b/**', '**/*.js', '.git/**'] };
|
||||
service.setRoots([request]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// create various ignored files
|
||||
let file1 = path.join(bFolder, 'file1.txt'); // hidden
|
||||
await pfs.writeFile(file1, 'Hello');
|
||||
let file2 = path.join(b2Folder, 'file2.txt'); // hidden
|
||||
await pfs.writeFile(file2, 'Hello');
|
||||
let folder1 = path.join(bFolder, 'folder1'); // hidden
|
||||
await pfs.mkdirp(folder1);
|
||||
let folder2 = path.join(aFolder, 'b'); // hidden
|
||||
await pfs.mkdirp(folder2);
|
||||
let folder3 = path.join(testDir, '.git'); // hidden
|
||||
await pfs.mkdirp(folder3);
|
||||
let folder4 = path.join(testDir, '.git1');
|
||||
await pfs.mkdirp(folder4);
|
||||
let folder5 = path.join(aFolder, '.git');
|
||||
await pfs.mkdirp(folder5);
|
||||
let file3 = path.join(aFolder, 'file3.js'); // hidden
|
||||
await pfs.writeFile(file3, 'var x;');
|
||||
let file4 = path.join(aFolder, 'file4.txt');
|
||||
await pfs.writeFile(file4, 'Hello');
|
||||
await assertFileEvents(result, [{ path: file4, type: FileChangeType.ADDED }, { path: folder4, type: FileChangeType.ADDED }, { path: folder5, type: FileChangeType.ADDED }]);
|
||||
|
||||
// move some files
|
||||
let movedFile1 = path.join(folder2, 'file1.txt'); // from ignored to ignored
|
||||
await pfs.rename(file1, movedFile1);
|
||||
let movedFile2 = path.join(aFolder, 'file2.txt'); // from ignored to visible
|
||||
await pfs.rename(file2, movedFile2);
|
||||
let movedFile3 = path.join(aFolder, 'file3.txt'); // from ignored file ext to visible
|
||||
await pfs.rename(file3, movedFile3);
|
||||
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.ADDED }, { path: movedFile3, type: FileChangeType.ADDED }]);
|
||||
|
||||
// delete all files
|
||||
await pfs.rimraf(movedFile1); // hidden
|
||||
await pfs.rimraf(movedFile2, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(movedFile3, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(folder1); // hidden
|
||||
await pfs.rimraf(folder2); // hidden
|
||||
await pfs.rimraf(folder3); // hidden
|
||||
await pfs.rimraf(folder4, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(folder5, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(file4, pfs.RimRafMode.MOVE);
|
||||
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.DELETED }, { path: movedFile3, type: FileChangeType.DELETED }, { path: file4, type: FileChangeType.DELETED }, { path: folder4, type: FileChangeType.DELETED }, { path: folder5, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
test('simple file operations, multiple roots', async () => {
|
||||
let request1: IWatcherRequest = { path: aFolder, excludes: ['**/*.js'] };
|
||||
let request2: IWatcherRequest = { path: b2Folder, excludes: ['**/*.ts'] };
|
||||
service.setRoots([request1, request2]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 2);
|
||||
|
||||
// create some files
|
||||
let folderPath1 = path.join(aFolder, 'folder1');
|
||||
await pfs.mkdirp(folderPath1);
|
||||
let filePath1 = path.join(folderPath1, 'file1.json');
|
||||
await pfs.writeFile(filePath1, '');
|
||||
let filePath2 = path.join(folderPath1, 'file2.js'); // filtered
|
||||
await pfs.writeFile(filePath2, '');
|
||||
let folderPath2 = path.join(b2Folder, 'folder2');
|
||||
await pfs.mkdirp(folderPath2);
|
||||
let filePath3 = path.join(folderPath2, 'file3.ts'); // filtered
|
||||
await pfs.writeFile(filePath3, '');
|
||||
let filePath4 = path.join(testDir, 'file4.json'); // outside roots
|
||||
await pfs.writeFile(filePath4, '');
|
||||
|
||||
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.ADDED }, { path: filePath1, type: FileChangeType.ADDED }, { path: folderPath2, type: FileChangeType.ADDED }]);
|
||||
|
||||
// change roots
|
||||
let request3: IWatcherRequest = { path: aFolder, excludes: ['**/*.json'] };
|
||||
service.setRoots([request3]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// delete all
|
||||
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(folderPath2, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(filePath4, pfs.RimRafMode.MOVE);
|
||||
|
||||
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
test('simple file operations, nested roots', async () => {
|
||||
let request1: IWatcherRequest = { path: testDir, excludes: ['**/b2/**'] };
|
||||
let request2: IWatcherRequest = { path: bFolder, excludes: ['**/b3/**'] };
|
||||
service.setRoots([request1, request2]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// create files
|
||||
let filePath1 = path.join(bFolder, 'file1.xml'); // visible by both
|
||||
await pfs.writeFile(filePath1, '');
|
||||
let filePath2 = path.join(b2Folder, 'file2.xml'); // filtered by root1, but visible by root2
|
||||
await pfs.writeFile(filePath2, '');
|
||||
let folderPath1 = path.join(b2Folder, 'b3'); // filtered
|
||||
await pfs.mkdirp(folderPath1);
|
||||
let filePath3 = path.join(folderPath1, 'file3.xml'); // filtered
|
||||
await pfs.writeFile(filePath3, '');
|
||||
|
||||
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.ADDED }, { path: filePath2, type: FileChangeType.ADDED }]);
|
||||
|
||||
let renamedFilePath2 = path.join(folderPath1, 'file2.xml');
|
||||
// move a file
|
||||
await pfs.rename(filePath2, renamedFilePath2);
|
||||
await assertFileEvents(result, [{ path: filePath2, type: FileChangeType.DELETED }]);
|
||||
|
||||
// delete all
|
||||
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(filePath1, pfs.RimRafMode.MOVE);
|
||||
|
||||
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,27 +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 { Event } from 'vs/base/common/event';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
|
||||
export interface IWatcherRequest {
|
||||
path: string;
|
||||
excludes: string[];
|
||||
}
|
||||
|
||||
export interface IWatcherOptions {
|
||||
verboseLogging: boolean;
|
||||
}
|
||||
|
||||
export interface IWatchError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IWatcherService {
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError>;
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void>;
|
||||
setVerboseLogging(enabled: boolean): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
@@ -1,13 +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 { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
|
||||
import { ChokidarWatcherService } from 'vs/workbench/services/files/node/watcher/unix/chokidarWatcherService';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new ChokidarWatcherService();
|
||||
const channel = new WatcherChannel(service);
|
||||
server.registerChannel('watcher', channel);
|
||||
@@ -1,53 +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 { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
|
||||
export class WatcherChannel implements IServerChannel {
|
||||
|
||||
constructor(private service: IWatcherService) { }
|
||||
|
||||
listen(_: unknown, event: string, arg?: any): Event<any> {
|
||||
switch (event) {
|
||||
case 'watch': return this.service.watch(arg);
|
||||
}
|
||||
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_: unknown, command: string, arg?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'setRoots': return this.service.setRoots(arg);
|
||||
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
|
||||
case 'stop': return this.service.stop();
|
||||
}
|
||||
|
||||
throw new Error(`Call not found: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class WatcherChannelClient implements IWatcherService {
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
|
||||
return this.channel.listen('watch', options);
|
||||
}
|
||||
|
||||
setVerboseLogging(enable: boolean): Promise<void> {
|
||||
return this.channel.call('setVerboseLogging', enable);
|
||||
}
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
return this.channel.call('setRoots', roots);
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return this.channel.call('stop');
|
||||
}
|
||||
}
|
||||
@@ -1,92 +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 { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IWatchError, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/unix/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
export class FileWatcher extends Disposable {
|
||||
private static readonly MAX_RESTARTS = 5;
|
||||
|
||||
private isDisposed: boolean;
|
||||
private restartCounter: number;
|
||||
private service: WatcherChannelClient;
|
||||
|
||||
constructor(
|
||||
private folders: IWatcherRequest[],
|
||||
private onFileChanges: (changes: IDiskFileChange[]) => void,
|
||||
private errorLogger: (msg: string) => void,
|
||||
private verboseLogging: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this.isDisposed = false;
|
||||
this.restartCounter = 0;
|
||||
|
||||
this.startWatching();
|
||||
}
|
||||
|
||||
private startWatching(): void {
|
||||
const client = this._register(new Client(
|
||||
getPathFromAmdModule(require, 'bootstrap-fork'),
|
||||
{
|
||||
serverName: 'File Watcher (chokidar)',
|
||||
args: ['--type=watcherService'],
|
||||
env: {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/unix/watcherApp',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: this.verboseLogging
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
this._register(client.onDidProcessExit(() => {
|
||||
// our watcher app should never be completed because it keeps on watching. being in here indicates
|
||||
// that the watcher process died and we want to restart it here. we only do it a max number of times
|
||||
if (!this.isDisposed) {
|
||||
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
|
||||
this.errorLogger('[File Watcher (chokidar)] terminated unexpectedly and is restarted again...');
|
||||
this.restartCounter++;
|
||||
this.startWatching();
|
||||
} else {
|
||||
this.errorLogger('[File Watcher (chokidar)] failed to start after retrying for some time, giving up. Please report this as a bug report!');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Initialize watcher
|
||||
const channel = getNextTickChannel(client.getChannel('watcher'));
|
||||
this.service = new WatcherChannelClient(channel);
|
||||
|
||||
const options = { verboseLogging: this.verboseLogging };
|
||||
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
|
||||
|
||||
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
|
||||
this._register(onError(err => this.errorLogger(`[File Watcher (chokidar)] ${err.message}`)));
|
||||
|
||||
const onFileChanges = Event.filter<any, IDiskFileChange[]>(onWatchEvent, (e): e is IDiskFileChange[] => Array.isArray(e) && e.length > 0);
|
||||
this._register(onFileChanges(e => this.onFileChanges(e)));
|
||||
|
||||
// Start watching
|
||||
this.service.setRoots(this.folders);
|
||||
}
|
||||
|
||||
setFolders(folders: IWatcherRequest[]): void {
|
||||
this.folders = folders;
|
||||
|
||||
this.service.setRoots(folders);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,104 +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 { URI as uri } from 'vs/base/common/uri';
|
||||
import { FileChangeType, isParent, IFileChange } from 'vs/platform/files/common/files';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
|
||||
export interface IDiskFileChange {
|
||||
type: FileChangeType;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] {
|
||||
return changes.map(change => ({
|
||||
type: change.type,
|
||||
resource: uri.file(change.path)
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] {
|
||||
|
||||
// Build deltas
|
||||
const normalizer = new EventNormalizer();
|
||||
for (const event of changes) {
|
||||
normalizer.processEvent(event);
|
||||
}
|
||||
|
||||
return normalizer.normalize();
|
||||
}
|
||||
|
||||
class EventNormalizer {
|
||||
private normalized: IDiskFileChange[] = [];
|
||||
private mapPathToChange: Map<string, IDiskFileChange> = new Map();
|
||||
|
||||
processEvent(event: IDiskFileChange): void {
|
||||
const existingEvent = this.mapPathToChange.get(event.path);
|
||||
|
||||
// Event path already exists
|
||||
if (existingEvent) {
|
||||
const currentChangeType = existingEvent.type;
|
||||
const newChangeType = event.type;
|
||||
|
||||
// ignore CREATE followed by DELETE in one go
|
||||
if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) {
|
||||
this.mapPathToChange.delete(event.path);
|
||||
this.normalized.splice(this.normalized.indexOf(existingEvent), 1);
|
||||
}
|
||||
|
||||
// flatten DELETE followed by CREATE into CHANGE
|
||||
else if (currentChangeType === FileChangeType.DELETED && newChangeType === FileChangeType.ADDED) {
|
||||
existingEvent.type = FileChangeType.UPDATED;
|
||||
}
|
||||
|
||||
// Do nothing. Keep the created event
|
||||
else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { }
|
||||
|
||||
// Otherwise apply change type
|
||||
else {
|
||||
existingEvent.type = newChangeType;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise store new
|
||||
else {
|
||||
this.normalized.push(event);
|
||||
this.mapPathToChange.set(event.path, event);
|
||||
}
|
||||
}
|
||||
|
||||
normalize(): IDiskFileChange[] {
|
||||
const addedChangeEvents: IDiskFileChange[] = [];
|
||||
const deletedPaths: string[] = [];
|
||||
|
||||
// This algorithm will remove all DELETE events up to the root folder
|
||||
// that got deleted if any. This ensures that we are not producing
|
||||
// DELETE events for each file inside a folder that gets deleted.
|
||||
//
|
||||
// 1.) split ADD/CHANGE and DELETED events
|
||||
// 2.) sort short deleted paths to the top
|
||||
// 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case
|
||||
return this.normalized.filter(e => {
|
||||
if (e.type !== FileChangeType.DELETED) {
|
||||
addedChangeEvents.push(e);
|
||||
|
||||
return false; // remove ADD / CHANGE
|
||||
}
|
||||
|
||||
return true; // keep DELETE
|
||||
}).sort((e1, e2) => {
|
||||
return e1.path.length - e2.path.length; // shortest path first
|
||||
}).filter(e => {
|
||||
if (deletedPaths.some(d => isParent(e.path, d, !isLinux /* ignorecase */))) {
|
||||
return false; // DELETE is ignored if parent is deleted already
|
||||
}
|
||||
|
||||
// otherwise mark as deleted
|
||||
deletedPaths.push(e.path);
|
||||
|
||||
return true;
|
||||
}).concat(addedChangeEvents);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
# Native File Watching for Windows using C# FileSystemWatcher
|
||||
|
||||
- Repository: https://github.com/Microsoft/vscode-filewatcher-windows
|
||||
|
||||
# Build
|
||||
|
||||
- Build in "Release" config
|
||||
- Copy CodeHelper.exe over into this folder
|
||||
@@ -1,132 +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 cp from 'child_process';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import * as decoder from 'vs/base/node/decoder';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
export class OutOfProcessWin32FolderWatcher {
|
||||
|
||||
private static readonly MAX_RESTARTS = 5;
|
||||
|
||||
private static changeTypeMap: FileChangeType[] = [FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED];
|
||||
|
||||
private ignored: glob.ParsedPattern[];
|
||||
|
||||
private handle: cp.ChildProcess;
|
||||
private restartCounter: number;
|
||||
|
||||
constructor(
|
||||
private watchedFolder: string,
|
||||
ignored: string[],
|
||||
private eventCallback: (events: IDiskFileChange[]) => void,
|
||||
private errorCallback: (error: string) => void,
|
||||
private verboseLogging: boolean
|
||||
) {
|
||||
this.restartCounter = 0;
|
||||
|
||||
if (Array.isArray(ignored)) {
|
||||
this.ignored = ignored.map(i => glob.parse(i));
|
||||
} else {
|
||||
this.ignored = [];
|
||||
}
|
||||
|
||||
// Logging
|
||||
if (this.verboseLogging) {
|
||||
console.log('%c[File Watcher (C#)]', 'color: blue', `Start watching: ${watchedFolder}`);
|
||||
}
|
||||
|
||||
this.startWatcher();
|
||||
}
|
||||
|
||||
private startWatcher(): void {
|
||||
const args = [this.watchedFolder];
|
||||
if (this.verboseLogging) {
|
||||
args.push('-verbose');
|
||||
}
|
||||
|
||||
this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/workbench/services/files/node/watcher/win32/CodeHelper.exe'), args);
|
||||
|
||||
const stdoutLineDecoder = new decoder.LineDecoder();
|
||||
|
||||
// Events over stdout
|
||||
this.handle.stdout.on('data', (data: Buffer) => {
|
||||
|
||||
// Collect raw events from output
|
||||
const rawEvents: IDiskFileChange[] = [];
|
||||
stdoutLineDecoder.write(data).forEach((line) => {
|
||||
const eventParts = line.split('|');
|
||||
if (eventParts.length === 2) {
|
||||
const changeType = Number(eventParts[0]);
|
||||
const absolutePath = eventParts[1];
|
||||
|
||||
// File Change Event (0 Changed, 1 Created, 2 Deleted)
|
||||
if (changeType >= 0 && changeType < 3) {
|
||||
|
||||
// Support ignores
|
||||
if (this.ignored && this.ignored.some(ignore => ignore(absolutePath))) {
|
||||
if (this.verboseLogging) {
|
||||
console.log('%c[File Watcher (C#)]', 'color: blue', ' >> ignored', absolutePath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise record as event
|
||||
rawEvents.push({
|
||||
type: OutOfProcessWin32FolderWatcher.changeTypeMap[changeType],
|
||||
path: absolutePath
|
||||
});
|
||||
}
|
||||
|
||||
// 3 Logging
|
||||
else {
|
||||
console.log('%c[File Watcher (C#)]', 'color: blue', eventParts[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger processing of events through the delayer to batch them up properly
|
||||
if (rawEvents.length > 0) {
|
||||
this.eventCallback(rawEvents);
|
||||
}
|
||||
});
|
||||
|
||||
// Errors
|
||||
this.handle.on('error', (error: Error) => this.onError(error));
|
||||
this.handle.stderr.on('data', (data: Buffer) => this.onError(data));
|
||||
|
||||
// Exit
|
||||
this.handle.on('exit', (code: number, signal: string) => this.onExit(code, signal));
|
||||
}
|
||||
|
||||
private onError(error: Error | Buffer): void {
|
||||
this.errorCallback('[File Watcher (C#)] process error: ' + error.toString());
|
||||
}
|
||||
|
||||
private onExit(code: number, signal: string): void {
|
||||
if (this.handle) { // exit while not yet being disposed is unexpected!
|
||||
this.errorCallback(`[File Watcher (C#)] terminated unexpectedly (code: ${code}, signal: ${signal})`);
|
||||
|
||||
if (this.restartCounter <= OutOfProcessWin32FolderWatcher.MAX_RESTARTS) {
|
||||
this.errorCallback('[File Watcher (C#)] is restarted again...');
|
||||
this.restartCounter++;
|
||||
this.startWatcher(); // restart
|
||||
} else {
|
||||
this.errorCallback('[File Watcher (C#)] Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.handle) {
|
||||
this.handle.kill();
|
||||
this.handle = null!; // StrictNullOverride: nulling out ok in dispose
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +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 { IDiskFileChange } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { OutOfProcessWin32FolderWatcher } from 'vs/workbench/services/files/node/watcher/win32/csharpWatcherService';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { rtrim, endsWith } from 'vs/base/common/strings';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class FileWatcher extends Disposable {
|
||||
private isDisposed: boolean;
|
||||
private folder: { path: string, excludes: string[] };
|
||||
|
||||
constructor(
|
||||
folders: { path: string, excludes: string[] }[],
|
||||
private onFileChanges: (changes: IDiskFileChange[]) => void,
|
||||
private errorLogger: (msg: string) => void,
|
||||
private verboseLogging: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this.folder = folders[0];
|
||||
|
||||
if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) {
|
||||
// for some weird reason, node adds a trailing slash to UNC paths
|
||||
// we never ever want trailing slashes as our base path unless
|
||||
// someone opens root ("/").
|
||||
// See also https://github.com/nodejs/io.js/issues/1765
|
||||
this.folder.path = rtrim(this.folder.path, posix.sep);
|
||||
}
|
||||
|
||||
this.startWatching();
|
||||
}
|
||||
|
||||
private startWatching(): void {
|
||||
this._register(new OutOfProcessWin32FolderWatcher(
|
||||
this.folder.path,
|
||||
this.folder.excludes,
|
||||
events => this.onFileEvents(events),
|
||||
error => this.onError(error),
|
||||
this.verboseLogging
|
||||
));
|
||||
}
|
||||
|
||||
private onFileEvents(events: IDiskFileChange[]): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit through event emitter
|
||||
if (events.length > 0) {
|
||||
this.onFileChanges(events);
|
||||
}
|
||||
}
|
||||
|
||||
private onError(error: string): void {
|
||||
if (!this.isDisposed) {
|
||||
this.errorLogger(error);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,112 +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 { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
suite('File Service', () => {
|
||||
|
||||
test('provider registration', async () => {
|
||||
const service = new FileService(new NullLogService());
|
||||
const resource = URI.parse('test://foo/bar');
|
||||
|
||||
assert.equal(service.canHandleResource(resource), false);
|
||||
|
||||
const registrations: IFileSystemProviderRegistrationEvent[] = [];
|
||||
service.onDidChangeFileSystemProviderRegistrations(e => {
|
||||
registrations.push(e);
|
||||
});
|
||||
|
||||
let registrationDisposable: IDisposable | undefined = undefined;
|
||||
let callCount = 0;
|
||||
service.onWillActivateFileSystemProvider(e => {
|
||||
callCount++;
|
||||
|
||||
if (e.scheme === 'test' && callCount === 1) {
|
||||
e.join(new Promise(resolve => {
|
||||
registrationDisposable = service.registerProvider('test', new NullFileSystemProvider());
|
||||
|
||||
resolve();
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
await service.activateProvider('test');
|
||||
|
||||
assert.equal(service.canHandleResource(resource), true);
|
||||
|
||||
assert.equal(registrations.length, 1);
|
||||
assert.equal(registrations[0].scheme, 'test');
|
||||
assert.equal(registrations[0].added, true);
|
||||
assert.ok(registrationDisposable);
|
||||
|
||||
await service.activateProvider('test');
|
||||
assert.equal(callCount, 2); // activation is called again
|
||||
|
||||
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
|
||||
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
|
||||
|
||||
registrationDisposable!.dispose();
|
||||
|
||||
assert.equal(service.canHandleResource(resource), false);
|
||||
|
||||
assert.equal(registrations.length, 2);
|
||||
assert.equal(registrations[1].scheme, 'test');
|
||||
assert.equal(registrations[1].added, false);
|
||||
});
|
||||
|
||||
test('watch', async () => {
|
||||
const service = new FileService(new NullLogService());
|
||||
|
||||
let disposeCounter = 0;
|
||||
service.registerProvider('test', new NullFileSystemProvider(() => {
|
||||
return toDisposable(() => {
|
||||
disposeCounter++;
|
||||
});
|
||||
}));
|
||||
await service.activateProvider('test');
|
||||
|
||||
const resource1 = URI.parse('test://foo/bar1');
|
||||
const watcher1Disposable = service.watch(resource1);
|
||||
|
||||
await timeout(0); // service.watch() is async
|
||||
assert.equal(disposeCounter, 0);
|
||||
watcher1Disposable.dispose();
|
||||
assert.equal(disposeCounter, 1);
|
||||
|
||||
disposeCounter = 0;
|
||||
const resource2 = URI.parse('test://foo/bar2');
|
||||
const watcher2Disposable1 = service.watch(resource2);
|
||||
const watcher2Disposable2 = service.watch(resource2);
|
||||
const watcher2Disposable3 = service.watch(resource2);
|
||||
|
||||
await timeout(0); // service.watch() is async
|
||||
assert.equal(disposeCounter, 0);
|
||||
watcher2Disposable1.dispose();
|
||||
assert.equal(disposeCounter, 0);
|
||||
watcher2Disposable2.dispose();
|
||||
assert.equal(disposeCounter, 0);
|
||||
watcher2Disposable3.dispose();
|
||||
assert.equal(disposeCounter, 1);
|
||||
|
||||
disposeCounter = 0;
|
||||
const resource3 = URI.parse('test://foo/bar3');
|
||||
const watcher3Disposable1 = service.watch(resource3);
|
||||
const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] });
|
||||
|
||||
await timeout(0); // service.watch() is async
|
||||
assert.equal(disposeCounter, 0);
|
||||
watcher3Disposable1.dispose();
|
||||
assert.equal(disposeCounter, 1);
|
||||
watcher3Disposable2.dispose();
|
||||
assert.equal(disposeCounter, 2);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
/// <reference path="employee.ts" />
|
||||
var Workforce;
|
||||
(function (Workforce_1) {
|
||||
var Company = (function () {
|
||||
function Company() {
|
||||
}
|
||||
return Company;
|
||||
})();
|
||||
(function (property, Workforce, IEmployee) {
|
||||
if (property === undefined) { property = employees; }
|
||||
if (IEmployee === undefined) { IEmployee = []; }
|
||||
property;
|
||||
calculateMonthlyExpenses();
|
||||
{
|
||||
var result = 0;
|
||||
for (var i = 0; i < employees.length; i++) {
|
||||
result += employees[i].calculatePay();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
})(Workforce || (Workforce = {}));
|
||||
@@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
var Conway;
|
||||
(function (Conway) {
|
||||
var Cell = (function () {
|
||||
function Cell() {
|
||||
}
|
||||
return Cell;
|
||||
})();
|
||||
(function (property, number, property, number, property, boolean) {
|
||||
if (property === undefined) { property = row; }
|
||||
if (property === undefined) { property = col; }
|
||||
if (property === undefined) { property = live; }
|
||||
});
|
||||
var GameOfLife = (function () {
|
||||
function GameOfLife() {
|
||||
}
|
||||
return GameOfLife;
|
||||
})();
|
||||
(function () {
|
||||
property;
|
||||
gridSize = 50;
|
||||
property;
|
||||
canvasSize = 600;
|
||||
property;
|
||||
lineColor = '#cdcdcd';
|
||||
property;
|
||||
liveColor = '#666';
|
||||
property;
|
||||
deadColor = '#eee';
|
||||
property;
|
||||
initialLifeProbability = 0.5;
|
||||
property;
|
||||
animationRate = 60;
|
||||
property;
|
||||
cellSize = 0;
|
||||
property;
|
||||
context: ICanvasRenderingContext2D;
|
||||
property;
|
||||
world = createWorld();
|
||||
circleOfLife();
|
||||
function createWorld() {
|
||||
return travelWorld(function (cell) {
|
||||
cell.live = Math.random() < initialLifeProbability;
|
||||
return cell;
|
||||
});
|
||||
}
|
||||
function circleOfLife() {
|
||||
world = travelWorld(function (cell) {
|
||||
cell = world[cell.row][cell.col];
|
||||
draw(cell);
|
||||
return resolveNextGeneration(cell);
|
||||
});
|
||||
setTimeout(function () { circleOfLife(); }, animationRate);
|
||||
}
|
||||
function resolveNextGeneration(cell) {
|
||||
var count = countNeighbors(cell);
|
||||
var newCell = new Cell(cell.row, cell.col, cell.live);
|
||||
if (count < 2 || count > 3)
|
||||
newCell.live = false;
|
||||
else if (count == 3)
|
||||
newCell.live = true;
|
||||
return newCell;
|
||||
}
|
||||
function countNeighbors(cell) {
|
||||
var neighbors = 0;
|
||||
for (var row = -1; row <= 1; row++) {
|
||||
for (var col = -1; col <= 1; col++) {
|
||||
if (row == 0 && col == 0)
|
||||
continue;
|
||||
if (isAlive(cell.row + row, cell.col + col)) {
|
||||
neighbors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return neighbors;
|
||||
}
|
||||
function isAlive(row, col) {
|
||||
// todo - need to guard with worl[row] exists?
|
||||
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
|
||||
return false;
|
||||
return world[row][col].live;
|
||||
}
|
||||
function travelWorld(callback) {
|
||||
var result = [];
|
||||
for (var row = 0; row < gridSize; row++) {
|
||||
var rowData = [];
|
||||
for (var col = 0; col < gridSize; col++) {
|
||||
rowData.push(callback(new Cell(row, col, false)));
|
||||
}
|
||||
result.push(rowData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function draw(cell) {
|
||||
if (context == null)
|
||||
context = createDrawingContext();
|
||||
if (cellSize == 0)
|
||||
cellSize = canvasSize / gridSize;
|
||||
context.strokeStyle = lineColor;
|
||||
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
|
||||
context.fillStyle = cell.live ? liveColor : deadColor;
|
||||
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
|
||||
}
|
||||
function createDrawingContext() {
|
||||
var canvas = document.getElementById('conway-canvas');
|
||||
if (canvas == null) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = "conway-canvas";
|
||||
canvas.width = canvasSize;
|
||||
canvas.height = canvasSize;
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
}
|
||||
});
|
||||
})(Conway || (Conway = {}));
|
||||
var game = new Conway.GameOfLife();
|
||||
@@ -1,38 +0,0 @@
|
||||
'use strict';
|
||||
var Workforce;
|
||||
(function (Workforce) {
|
||||
var Employee = (function () {
|
||||
function Employee() {
|
||||
}
|
||||
return Employee;
|
||||
})();
|
||||
(property);
|
||||
name: string, property;
|
||||
basepay: number;
|
||||
implements;
|
||||
IEmployee;
|
||||
{
|
||||
name;
|
||||
basepay;
|
||||
}
|
||||
var SalesEmployee = (function () {
|
||||
function SalesEmployee() {
|
||||
}
|
||||
return SalesEmployee;
|
||||
})();
|
||||
();
|
||||
Employee(name, basepay);
|
||||
{
|
||||
function calculatePay() {
|
||||
var multiplier = (document.getElementById("mult")), as = any, value;
|
||||
return _super.calculatePay.call(this) * multiplier + bonus;
|
||||
}
|
||||
}
|
||||
var employee = new Employee('Bob', 1000);
|
||||
var salesEmployee = new SalesEmployee('Jim', 800, 400);
|
||||
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
|
||||
})(Workforce || (Workforce = {}));
|
||||
extern;
|
||||
var $;
|
||||
var s = Workforce.salesEmployee.calculatePay();
|
||||
$('#results').text(s);
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
var M;
|
||||
(function (M) {
|
||||
var C = (function () {
|
||||
function C() {
|
||||
}
|
||||
return C;
|
||||
})();
|
||||
(function (x, property, number) {
|
||||
if (property === undefined) { property = w; }
|
||||
var local = 1;
|
||||
// unresolved symbol because x is local
|
||||
//self.x++;
|
||||
self.w--; // ok because w is a property
|
||||
property;
|
||||
f = function (y) {
|
||||
return y + x + local + w + self.w;
|
||||
};
|
||||
function sum(z) {
|
||||
return z + f(z) + w + self.w;
|
||||
}
|
||||
});
|
||||
})(M || (M = {}));
|
||||
var c = new M.C(12, 5);
|
||||
@@ -1,121 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head id='headID'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Strada </title>
|
||||
<link href="site.css" rel="stylesheet" type="text/css" />
|
||||
<script src="jquery-1.4.1.js"></script>
|
||||
<script src="../compiler/dtree.js" type="text/javascript"></script>
|
||||
<script src="../compiler/typescript.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// Compile strada source into resulting javascript
|
||||
function compile(prog, libText) {
|
||||
var outfile = {
|
||||
source: "",
|
||||
Write: function (s) { this.source += s; },
|
||||
WriteLine: function (s) { this.source += s + "\r"; },
|
||||
}
|
||||
|
||||
var parseErrors = []
|
||||
|
||||
var compiler=new Tools.TypeScriptCompiler(outfile,true);
|
||||
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
|
||||
compiler.addUnit(libText,"lib.ts");
|
||||
compiler.addUnit(prog,"input.ts");
|
||||
compiler.typeCheck();
|
||||
compiler.emit();
|
||||
|
||||
if(parseErrors.length > 0 ) {
|
||||
//throw new Error(parseErrors);
|
||||
}
|
||||
|
||||
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
|
||||
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
|
||||
}
|
||||
var errorPrefix = "";
|
||||
for(var i = 0;i<parseErrors.length;i++) {
|
||||
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
|
||||
}
|
||||
|
||||
return errorPrefix + outfile.source;
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var libText = "";
|
||||
$.get("../compiler/lib.ts", function(newLibText) {
|
||||
libText = newLibText;
|
||||
});
|
||||
|
||||
|
||||
// execute the javascript in the compiledOutput pane
|
||||
function execute() {
|
||||
$('#compilation').text("Running...");
|
||||
var txt = $('#compiledOutput').val();
|
||||
var res;
|
||||
try {
|
||||
var ret = eval(txt);
|
||||
res = "Ran successfully!";
|
||||
} catch(e) {
|
||||
res = "Exception thrown: " + e;
|
||||
}
|
||||
$('#compilation').text(String(res));
|
||||
}
|
||||
|
||||
// recompile the stradaSrc and populate the compiledOutput pane
|
||||
function srcUpdated() {
|
||||
var newText = $('#stradaSrc').val();
|
||||
var compiledSource;
|
||||
try {
|
||||
compiledSource = compile(newText, libText);
|
||||
} catch (e) {
|
||||
compiledSource = "//Parse error"
|
||||
for(var i in e)
|
||||
compiledSource += "\r// " + e[i];
|
||||
}
|
||||
$('#compiledOutput').val(compiledSource);
|
||||
}
|
||||
|
||||
// Populate the stradaSrc pane with one of the built in samples
|
||||
function exampleSelectionChanged() {
|
||||
var examples = document.getElementById('examples');
|
||||
var selectedExample = examples.options[examples.selectedIndex].value;
|
||||
if (selectedExample != "") {
|
||||
$.get('examples/' + selectedExample, function (srcText) {
|
||||
$('#stradaSrc').val(srcText);
|
||||
setTimeout(srcUpdated,100);
|
||||
}, function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TypeScript</h1>
|
||||
<br />
|
||||
<select id="examples" onchange='exampleSelectionChanged()'>
|
||||
<option value="">Select...</option>
|
||||
<option value="small.ts">Small</option>
|
||||
<option value="employee.ts">Employees</option>
|
||||
<option value="conway.ts">Conway Game of Life</option>
|
||||
<option value="typescript.ts">TypeScript Compiler</option>
|
||||
</select>
|
||||
|
||||
<div>
|
||||
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
|
||||
//Type your TypeScript here...
|
||||
</textarea>
|
||||
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
|
||||
//Compiled code will show up here...
|
||||
</textarea>
|
||||
<br />
|
||||
<button onclick='execute()'/>Run</button>
|
||||
<div id='compilation'>Press 'run' to execute code...</div>
|
||||
<div id='results'>...write your results into #results...</div>
|
||||
</div>
|
||||
<div id='bod' style='display:none'></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
/// <reference path="employee.ts" />
|
||||
var Workforce;
|
||||
(function (Workforce_1) {
|
||||
var Company = (function () {
|
||||
function Company() {
|
||||
}
|
||||
return Company;
|
||||
})();
|
||||
(function (property, Workforce, IEmployee) {
|
||||
if (property === undefined) { property = employees; }
|
||||
if (IEmployee === undefined) { IEmployee = []; }
|
||||
property;
|
||||
calculateMonthlyExpenses();
|
||||
{
|
||||
var result = 0;
|
||||
for (var i = 0; i < employees.length; i++) {
|
||||
result += employees[i].calculatePay();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
})(Workforce || (Workforce = {}));
|
||||
@@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
var Conway;
|
||||
(function (Conway) {
|
||||
var Cell = (function () {
|
||||
function Cell() {
|
||||
}
|
||||
return Cell;
|
||||
})();
|
||||
(function (property, number, property, number, property, boolean) {
|
||||
if (property === undefined) { property = row; }
|
||||
if (property === undefined) { property = col; }
|
||||
if (property === undefined) { property = live; }
|
||||
});
|
||||
var GameOfLife = (function () {
|
||||
function GameOfLife() {
|
||||
}
|
||||
return GameOfLife;
|
||||
})();
|
||||
(function () {
|
||||
property;
|
||||
gridSize = 50;
|
||||
property;
|
||||
canvasSize = 600;
|
||||
property;
|
||||
lineColor = '#cdcdcd';
|
||||
property;
|
||||
liveColor = '#666';
|
||||
property;
|
||||
deadColor = '#eee';
|
||||
property;
|
||||
initialLifeProbability = 0.5;
|
||||
property;
|
||||
animationRate = 60;
|
||||
property;
|
||||
cellSize = 0;
|
||||
property;
|
||||
context: ICanvasRenderingContext2D;
|
||||
property;
|
||||
world = createWorld();
|
||||
circleOfLife();
|
||||
function createWorld() {
|
||||
return travelWorld(function (cell) {
|
||||
cell.live = Math.random() < initialLifeProbability;
|
||||
return cell;
|
||||
});
|
||||
}
|
||||
function circleOfLife() {
|
||||
world = travelWorld(function (cell) {
|
||||
cell = world[cell.row][cell.col];
|
||||
draw(cell);
|
||||
return resolveNextGeneration(cell);
|
||||
});
|
||||
setTimeout(function () { circleOfLife(); }, animationRate);
|
||||
}
|
||||
function resolveNextGeneration(cell) {
|
||||
var count = countNeighbors(cell);
|
||||
var newCell = new Cell(cell.row, cell.col, cell.live);
|
||||
if (count < 2 || count > 3)
|
||||
newCell.live = false;
|
||||
else if (count == 3)
|
||||
newCell.live = true;
|
||||
return newCell;
|
||||
}
|
||||
function countNeighbors(cell) {
|
||||
var neighbors = 0;
|
||||
for (var row = -1; row <= 1; row++) {
|
||||
for (var col = -1; col <= 1; col++) {
|
||||
if (row == 0 && col == 0)
|
||||
continue;
|
||||
if (isAlive(cell.row + row, cell.col + col)) {
|
||||
neighbors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return neighbors;
|
||||
}
|
||||
function isAlive(row, col) {
|
||||
// todo - need to guard with worl[row] exists?
|
||||
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
|
||||
return false;
|
||||
return world[row][col].live;
|
||||
}
|
||||
function travelWorld(callback) {
|
||||
var result = [];
|
||||
for (var row = 0; row < gridSize; row++) {
|
||||
var rowData = [];
|
||||
for (var col = 0; col < gridSize; col++) {
|
||||
rowData.push(callback(new Cell(row, col, false)));
|
||||
}
|
||||
result.push(rowData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function draw(cell) {
|
||||
if (context == null)
|
||||
context = createDrawingContext();
|
||||
if (cellSize == 0)
|
||||
cellSize = canvasSize / gridSize;
|
||||
context.strokeStyle = lineColor;
|
||||
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
|
||||
context.fillStyle = cell.live ? liveColor : deadColor;
|
||||
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
|
||||
}
|
||||
function createDrawingContext() {
|
||||
var canvas = document.getElementById('conway-canvas');
|
||||
if (canvas == null) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = "conway-canvas";
|
||||
canvas.width = canvasSize;
|
||||
canvas.height = canvasSize;
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
}
|
||||
});
|
||||
})(Conway || (Conway = {}));
|
||||
var game = new Conway.GameOfLife();
|
||||
@@ -1,38 +0,0 @@
|
||||
'use strict';
|
||||
var Workforce;
|
||||
(function (Workforce) {
|
||||
var Employee = (function () {
|
||||
function Employee() {
|
||||
}
|
||||
return Employee;
|
||||
})();
|
||||
(property);
|
||||
name: string, property;
|
||||
basepay: number;
|
||||
implements;
|
||||
IEmployee;
|
||||
{
|
||||
name;
|
||||
basepay;
|
||||
}
|
||||
var SalesEmployee = (function () {
|
||||
function SalesEmployee() {
|
||||
}
|
||||
return SalesEmployee;
|
||||
})();
|
||||
();
|
||||
Employee(name, basepay);
|
||||
{
|
||||
function calculatePay() {
|
||||
var multiplier = (document.getElementById("mult")), as = any, value;
|
||||
return _super.calculatePay.call(this) * multiplier + bonus;
|
||||
}
|
||||
}
|
||||
var employee = new Employee('Bob', 1000);
|
||||
var salesEmployee = new SalesEmployee('Jim', 800, 400);
|
||||
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
|
||||
})(Workforce || (Workforce = {}));
|
||||
extern;
|
||||
var $;
|
||||
var s = Workforce.salesEmployee.calculatePay();
|
||||
$('#results').text(s);
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
var M;
|
||||
(function (M) {
|
||||
var C = (function () {
|
||||
function C() {
|
||||
}
|
||||
return C;
|
||||
})();
|
||||
(function (x, property, number) {
|
||||
if (property === undefined) { property = w; }
|
||||
var local = 1;
|
||||
// unresolved symbol because x is local
|
||||
//self.x++;
|
||||
self.w--; // ok because w is a property
|
||||
property;
|
||||
f = function (y) {
|
||||
return y + x + local + w + self.w;
|
||||
};
|
||||
function sum(z) {
|
||||
return z + f(z) + w + self.w;
|
||||
}
|
||||
});
|
||||
})(M || (M = {}));
|
||||
var c = new M.C(12, 5);
|
||||
@@ -1,40 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*----------------------------------------------------------
|
||||
The base color for this template is #5c87b2. If you'd like
|
||||
to use a different color start by replacing all instances of
|
||||
#5c87b2 with your new color.
|
||||
----------------------------------------------------------*/
|
||||
body
|
||||
{
|
||||
background-color: #5c87b2;
|
||||
font-size: .75em;
|
||||
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
textarea
|
||||
{
|
||||
font-family: Consolas
|
||||
}
|
||||
|
||||
#results
|
||||
{
|
||||
margin-top: 2em;
|
||||
margin-left: 2em;
|
||||
color: black;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 274 B |
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
/// <reference path="employee.ts" />
|
||||
var Workforce;
|
||||
(function (Workforce_1) {
|
||||
var Company = (function () {
|
||||
function Company() {
|
||||
}
|
||||
return Company;
|
||||
})();
|
||||
(function (property, Workforce, IEmployee) {
|
||||
if (property === undefined) { property = employees; }
|
||||
if (IEmployee === undefined) { IEmployee = []; }
|
||||
property;
|
||||
calculateMonthlyExpenses();
|
||||
{
|
||||
var result = 0;
|
||||
for (var i = 0; i < employees.length; i++) {
|
||||
result += employees[i].calculatePay();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
})(Workforce || (Workforce = {}));
|
||||
@@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
var Conway;
|
||||
(function (Conway) {
|
||||
var Cell = (function () {
|
||||
function Cell() {
|
||||
}
|
||||
return Cell;
|
||||
})();
|
||||
(function (property, number, property, number, property, boolean) {
|
||||
if (property === undefined) { property = row; }
|
||||
if (property === undefined) { property = col; }
|
||||
if (property === undefined) { property = live; }
|
||||
});
|
||||
var GameOfLife = (function () {
|
||||
function GameOfLife() {
|
||||
}
|
||||
return GameOfLife;
|
||||
})();
|
||||
(function () {
|
||||
property;
|
||||
gridSize = 50;
|
||||
property;
|
||||
canvasSize = 600;
|
||||
property;
|
||||
lineColor = '#cdcdcd';
|
||||
property;
|
||||
liveColor = '#666';
|
||||
property;
|
||||
deadColor = '#eee';
|
||||
property;
|
||||
initialLifeProbability = 0.5;
|
||||
property;
|
||||
animationRate = 60;
|
||||
property;
|
||||
cellSize = 0;
|
||||
property;
|
||||
context: ICanvasRenderingContext2D;
|
||||
property;
|
||||
world = createWorld();
|
||||
circleOfLife();
|
||||
function createWorld() {
|
||||
return travelWorld(function (cell) {
|
||||
cell.live = Math.random() < initialLifeProbability;
|
||||
return cell;
|
||||
});
|
||||
}
|
||||
function circleOfLife() {
|
||||
world = travelWorld(function (cell) {
|
||||
cell = world[cell.row][cell.col];
|
||||
draw(cell);
|
||||
return resolveNextGeneration(cell);
|
||||
});
|
||||
setTimeout(function () { circleOfLife(); }, animationRate);
|
||||
}
|
||||
function resolveNextGeneration(cell) {
|
||||
var count = countNeighbors(cell);
|
||||
var newCell = new Cell(cell.row, cell.col, cell.live);
|
||||
if (count < 2 || count > 3)
|
||||
newCell.live = false;
|
||||
else if (count == 3)
|
||||
newCell.live = true;
|
||||
return newCell;
|
||||
}
|
||||
function countNeighbors(cell) {
|
||||
var neighbors = 0;
|
||||
for (var row = -1; row <= 1; row++) {
|
||||
for (var col = -1; col <= 1; col++) {
|
||||
if (row == 0 && col == 0)
|
||||
continue;
|
||||
if (isAlive(cell.row + row, cell.col + col)) {
|
||||
neighbors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return neighbors;
|
||||
}
|
||||
function isAlive(row, col) {
|
||||
// todo - need to guard with worl[row] exists?
|
||||
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
|
||||
return false;
|
||||
return world[row][col].live;
|
||||
}
|
||||
function travelWorld(callback) {
|
||||
var result = [];
|
||||
for (var row = 0; row < gridSize; row++) {
|
||||
var rowData = [];
|
||||
for (var col = 0; col < gridSize; col++) {
|
||||
rowData.push(callback(new Cell(row, col, false)));
|
||||
}
|
||||
result.push(rowData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function draw(cell) {
|
||||
if (context == null)
|
||||
context = createDrawingContext();
|
||||
if (cellSize == 0)
|
||||
cellSize = canvasSize / gridSize;
|
||||
context.strokeStyle = lineColor;
|
||||
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
|
||||
context.fillStyle = cell.live ? liveColor : deadColor;
|
||||
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
|
||||
}
|
||||
function createDrawingContext() {
|
||||
var canvas = document.getElementById('conway-canvas');
|
||||
if (canvas == null) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = "conway-canvas";
|
||||
canvas.width = canvasSize;
|
||||
canvas.height = canvasSize;
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
}
|
||||
});
|
||||
})(Conway || (Conway = {}));
|
||||
var game = new Conway.GameOfLife();
|
||||
@@ -1,38 +0,0 @@
|
||||
'use strict';
|
||||
var Workforce;
|
||||
(function (Workforce) {
|
||||
var Employee = (function () {
|
||||
function Employee() {
|
||||
}
|
||||
return Employee;
|
||||
})();
|
||||
(property);
|
||||
name: string, property;
|
||||
basepay: number;
|
||||
implements;
|
||||
IEmployee;
|
||||
{
|
||||
name;
|
||||
basepay;
|
||||
}
|
||||
var SalesEmployee = (function () {
|
||||
function SalesEmployee() {
|
||||
}
|
||||
return SalesEmployee;
|
||||
})();
|
||||
();
|
||||
Employee(name, basepay);
|
||||
{
|
||||
function calculatePay() {
|
||||
var multiplier = (document.getElementById("mult")), as = any, value;
|
||||
return _super.calculatePay.call(this) * multiplier + bonus;
|
||||
}
|
||||
}
|
||||
var employee = new Employee('Bob', 1000);
|
||||
var salesEmployee = new SalesEmployee('Jim', 800, 400);
|
||||
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
|
||||
})(Workforce || (Workforce = {}));
|
||||
extern;
|
||||
var $;
|
||||
var s = Workforce.salesEmployee.calculatePay();
|
||||
$('#results').text(s);
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
var M;
|
||||
(function (M) {
|
||||
var C = (function () {
|
||||
function C() {
|
||||
}
|
||||
return C;
|
||||
})();
|
||||
(function (x, property, number) {
|
||||
if (property === undefined) { property = w; }
|
||||
var local = 1;
|
||||
// unresolved symbol because x is local
|
||||
//self.x++;
|
||||
self.w--; // ok because w is a property
|
||||
property;
|
||||
f = function (y) {
|
||||
return y + x + local + w + self.w;
|
||||
};
|
||||
function sum(z) {
|
||||
return z + f(z) + w + self.w;
|
||||
}
|
||||
});
|
||||
})(M || (M = {}));
|
||||
var c = new M.C(12, 5);
|
||||
@@ -1,121 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head id='headID'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Strada </title>
|
||||
<link href="site.css" rel="stylesheet" type="text/css" />
|
||||
<script src="jquery-1.4.1.js"></script>
|
||||
<script src="../compiler/dtree.js" type="text/javascript"></script>
|
||||
<script src="../compiler/typescript.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// Compile strada source into resulting javascript
|
||||
function compile(prog, libText) {
|
||||
var outfile = {
|
||||
source: "",
|
||||
Write: function (s) { this.source += s; },
|
||||
WriteLine: function (s) { this.source += s + "\r"; },
|
||||
}
|
||||
|
||||
var parseErrors = []
|
||||
|
||||
var compiler=new Tools.TypeScriptCompiler(outfile,true);
|
||||
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
|
||||
compiler.addUnit(libText,"lib.ts");
|
||||
compiler.addUnit(prog,"input.ts");
|
||||
compiler.typeCheck();
|
||||
compiler.emit();
|
||||
|
||||
if(parseErrors.length > 0 ) {
|
||||
//throw new Error(parseErrors);
|
||||
}
|
||||
|
||||
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
|
||||
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
|
||||
}
|
||||
var errorPrefix = "";
|
||||
for(var i = 0;i<parseErrors.length;i++) {
|
||||
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
|
||||
}
|
||||
|
||||
return errorPrefix + outfile.source;
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var libText = "";
|
||||
$.get("../compiler/lib.ts", function(newLibText) {
|
||||
libText = newLibText;
|
||||
});
|
||||
|
||||
|
||||
// execute the javascript in the compiledOutput pane
|
||||
function execute() {
|
||||
$('#compilation').text("Running...");
|
||||
var txt = $('#compiledOutput').val();
|
||||
var res;
|
||||
try {
|
||||
var ret = eval(txt);
|
||||
res = "Ran successfully!";
|
||||
} catch(e) {
|
||||
res = "Exception thrown: " + e;
|
||||
}
|
||||
$('#compilation').text(String(res));
|
||||
}
|
||||
|
||||
// recompile the stradaSrc and populate the compiledOutput pane
|
||||
function srcUpdated() {
|
||||
var newText = $('#stradaSrc').val();
|
||||
var compiledSource;
|
||||
try {
|
||||
compiledSource = compile(newText, libText);
|
||||
} catch (e) {
|
||||
compiledSource = "//Parse error"
|
||||
for(var i in e)
|
||||
compiledSource += "\r// " + e[i];
|
||||
}
|
||||
$('#compiledOutput').val(compiledSource);
|
||||
}
|
||||
|
||||
// Populate the stradaSrc pane with one of the built in samples
|
||||
function exampleSelectionChanged() {
|
||||
var examples = document.getElementById('examples');
|
||||
var selectedExample = examples.options[examples.selectedIndex].value;
|
||||
if (selectedExample != "") {
|
||||
$.get('examples/' + selectedExample, function (srcText) {
|
||||
$('#stradaSrc').val(srcText);
|
||||
setTimeout(srcUpdated,100);
|
||||
}, function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TypeScript</h1>
|
||||
<br />
|
||||
<select id="examples" onchange='exampleSelectionChanged()'>
|
||||
<option value="">Select...</option>
|
||||
<option value="small.ts">Small</option>
|
||||
<option value="employee.ts">Employees</option>
|
||||
<option value="conway.ts">Conway Game of Life</option>
|
||||
<option value="typescript.ts">TypeScript Compiler</option>
|
||||
</select>
|
||||
|
||||
<div>
|
||||
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
|
||||
//Type your TypeScript here...
|
||||
</textarea>
|
||||
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
|
||||
//Compiled code will show up here...
|
||||
</textarea>
|
||||
<br />
|
||||
<button onclick='execute()'/>Run</button>
|
||||
<div id='compilation'>Press 'run' to execute code...</div>
|
||||
<div id='results'>...write your results into #results...</div>
|
||||
</div>
|
||||
<div id='bod' style='display:none'></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,283 +0,0 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vulputate, ipsum quis interdum fermentum, lorem sem fermentum eros, vitae auctor neque lacus in nisi. Suspendisse potenti. Maecenas et scelerisque elit, in tincidunt quam. Sed eu tincidunt quam. Nullam justo ex, imperdiet a imperdiet et, fermentum sit amet eros. Aenean quis tempus sem. Pellentesque accumsan magna mi, ut mollis velit sagittis id. Etiam quis ipsum orci. Fusce purus ante, accumsan a lobortis at, venenatis eu nisl. Praesent ornare sed ante placerat accumsan. Suspendisse tempus dignissim fermentum. Nunc a leo ac lacus sodales iaculis eu vitae mi. In feugiat ante at massa finibus cursus. Suspendisse posuere fringilla ornare. Mauris elementum ac quam id convallis. Vestibulum non elit quis urna volutpat aliquam a eu lacus.
|
||||
|
||||
Aliquam vestibulum imperdiet neque, suscipit aliquam elit ultrices bibendum. Suspendisse ultrices pulvinar cursus. Morbi risus nisi, cursus consequat rutrum vitae, molestie sed dui. Fusce posuere, augue quis dignissim aliquam, nisi ipsum porttitor ante, quis fringilla nisl turpis ac nisi. Nulla varius enim eget lorem vehicula gravida. Donec finibus malesuada leo nec semper. Proin ac enim eros. Vivamus non tincidunt nisi, vel tristique lorem.
|
||||
|
||||
Nunc consequat ex id eros dignissim, id rutrum risus laoreet. Sed euismod non erat eu ultricies. Etiam vehicula gravida lacus ut porta. Vestibulum eu eros quis nunc aliquet luctus. Cras quis semper ligula. Nullam gravida vehicula quam sed porta. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In porta cursus vulputate. Quisque porta a nisi eget cursus. Aliquam risus leo, luctus ac magna in, efficitur cursus magna. In condimentum non mi id semper. Donec interdum ante eget commodo maximus.
|
||||
|
||||
Vivamus sit amet vestibulum lectus. Fusce tincidunt mi sapien, dictum sollicitudin diam vulputate in. Integer fringilla consequat mollis. Cras aliquet consequat felis eget feugiat. Nunc tempor cursus arcu, vitae ornare nunc varius et. Vestibulum et tortor vel ante viverra porttitor. Nam at tortor ullamcorper, facilisis augue quis, tristique erat. Aenean ut euismod nibh. Quisque eu tincidunt est, nec euismod eros.
|
||||
|
||||
Proin vehicula nibh non viverra egestas. Phasellus sem dolor, ultricies ac sagittis tristique, lacinia a purus. Vestibulum in ante eros. Pellentesque lacus nulla, tristique vitae interdum vel, malesuada ac diam. Aenean bibendum posuere turpis in accumsan. Ut est nulla, ullamcorper quis turpis at, viverra sagittis mauris. Sed in interdum purus. Praesent scelerisque nibh eget sem euismod, ut imperdiet mi venenatis. Vivamus pulvinar orci sed dapibus auctor. Nulla facilisi. Vestibulum tincidunt erat nec porttitor egestas. Mauris quis risus ante. Nulla facilisi.
|
||||
|
||||
Aliquam ullamcorper ornare lobortis. Phasellus quis sem et ipsum mollis malesuada sed in ex. Ut aliquam ex eget metus finibus maximus. Proin suscipit mauris eu nibh lacinia, quis feugiat dui dapibus. Nam sed libero est. Aenean vulputate orci sit amet diam faucibus, eu sagittis sapien volutpat. Nam imperdiet felis turpis, at pretium odio pulvinar in. Sed vestibulum id eros nec ultricies. Sed quis aliquam tortor, vitae ullamcorper tellus. Donec egestas laoreet eros, id suscipit est rutrum nec. Sed auctor nulla eget metus aliquam, ut condimentum enim elementum.
|
||||
|
||||
Aliquam suscipit non turpis sit amet bibendum. Fusce velit ligula, euismod et maximus at, luctus sed neque. Quisque pretium, nisl at ullamcorper finibus, lectus leo mattis sapien, vel euismod mauris diam ullamcorper ex. Nulla ut risus finibus, lacinia ligula at, auctor erat. Mauris consectetur sagittis ligula vel dapibus. Nullam libero libero, lobortis aliquam libero vel, venenatis ultricies leo. Duis porttitor, nibh congue fermentum posuere, erat libero pulvinar tortor, a pellentesque nunc ipsum vel sem. Nullam volutpat, eros sit amet facilisis consectetur, ipsum est vehicula massa, non vestibulum neque elit in mauris. Nunc hendrerit ipsum non enim bibendum, vitae rhoncus mi egestas. Etiam ullamcorper massa vel nisl sagittis, nec bibendum arcu malesuada. Aenean aliquet turpis justo, a consectetur arcu mollis convallis. Etiam tellus ipsum, ultricies vitae lorem et, ornare facilisis orci. Praesent fringilla justo urna, vel mollis neque pulvinar vestibulum.
|
||||
|
||||
Donec non iaculis erat. Aliquam et mi sed nunc pulvinar ultricies in ut ipsum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent feugiat lacus ac dignissim semper. Phasellus vitae quam nisi. Morbi vel diam ultricies risus lobortis ornare. Fusce maximus et ligula quis iaculis. Sed congue ex eget felis convallis, sit amet hendrerit elit tempor. Donec vehicula blandit ante eget commodo. Vestibulum eleifend diam at feugiat euismod. Etiam magna tellus, dignissim eget fermentum vel, vestibulum vitae mauris. Nam accumsan et erat id sagittis. Donec lacinia, odio ut ornare ultricies, dolor velit accumsan tortor, non finibus erat tellus quis ligula. Nunc quis metus in leo volutpat ornare vulputate eu nisl.
|
||||
|
||||
Donec quis viverra ex. Nullam id feugiat mauris, eu fringilla nulla. Vestibulum id maximus elit. Cras elementum elit sed felis lobortis, eget sagittis nisi hendrerit. Vivamus vitae elit neque. Donec vulputate lacus ut libero ultrices accumsan. Vivamus accumsan nulla orci, in dignissim est laoreet sagittis. Proin at commodo velit. Curabitur in velit felis. Aliquam erat volutpat. Sed consequat, nulla et cursus sodales, nisi lacus mattis risus, quis eleifend erat ex nec turpis. Sed suscipit ultrices lorem in hendrerit.
|
||||
|
||||
Morbi vitae lacus nec libero ornare tempus eu et diam. Suspendisse magna ipsum, fermentum vel odio quis, molestie aliquam urna. Fusce mollis turpis a eros accumsan porttitor. Pellentesque rhoncus dolor sit amet magna rutrum, et dapibus justo tempor. Sed purus nisi, maximus vitae fringilla eu, molestie nec urna. Fusce malesuada finibus pretium. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec sed aliquet eros. Pellentesque luctus diam ante, eget euismod nisl aliquet eu. Sed accumsan elit purus, tempor varius ligula tempus nec. Curabitur ornare leo suscipit suscipit fermentum. Morbi eget nulla est. Maecenas faucibus interdum tristique.
|
||||
|
||||
Etiam ut elit eros. Nulla pharetra suscipit molestie. Nulla facilisis bibendum nisl non molestie. Curabitur turpis lectus, facilisis vel diam non, vulputate ultrices mauris. Aenean placerat aliquam convallis. Suspendisse sed scelerisque tellus. Vivamus lacinia neque eget risus cursus suscipit. Proin consequat dolor vel neque tempor, eu aliquam sem scelerisque. Duis non eros a purus malesuada pharetra non et nulla. Suspendisse potenti. Mauris libero eros, finibus vel nulla id, sagittis dapibus ante. Proin iaculis sed nunc et cursus.
|
||||
|
||||
Quisque accumsan lorem sit amet lorem aliquet euismod. Curabitur fermentum rutrum posuere. Etiam ultricies, sem id pellentesque suscipit, urna magna lacinia eros, quis efficitur risus nisl at lacus. Nulla quis lacus tortor. Mauris placerat ex in dolor tincidunt, vel aliquet nisi pretium. Cras iaculis risus vitae pellentesque aliquet. Quisque a enim imperdiet, ullamcorper arcu vitae, rutrum risus. Nullam consectetur libero at felis fringilla, nec congue nibh dignissim. Nam et lobortis felis, eu pellentesque ligula. Aenean facilisis, ligula non imperdiet maximus, massa orci gravida sapien, at sagittis lacus nisl in lacus. Nulla quis mauris luctus, scelerisque felis consequat, tempus risus. Fusce auctor nisl non nulla luctus molestie. Maecenas sapien nisl, auctor non dolor et, iaculis scelerisque lorem. Suspendisse egestas enim aliquet, accumsan mauris nec, posuere quam. Nulla iaculis dui dui, sit amet vestibulum erat ultricies ac.
|
||||
|
||||
Cras eget dolor erat. Proin at nisl ut leo consectetur ultricies vel ut arcu. Nulla in felis malesuada, ullamcorper tortor et, convallis massa. Nunc urna justo, ornare in nibh vitae, hendrerit condimentum libero. Etiam vitae libero in purus venenatis fringilla. Nullam velit nulla, consequat ut turpis non, egestas hendrerit nibh. Duis tortor turpis, interdum non ante ac, cursus accumsan lectus. Cras pharetra bibendum augue quis dictum. Sed euismod vestibulum justo. Proin porta lobortis purus. Duis venenatis diam tortor, sit amet condimentum eros rhoncus a. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc at magna nec diam lobortis efficitur sit amet ut lacus. Nulla quis orci tortor. Pellentesque tempus velit a odio finibus porta.
|
||||
|
||||
Proin feugiat mauris a tellus scelerisque convallis. Maecenas libero magna, blandit nec ultrices id, congue vel mi. Aliquam lacinia, quam vel condimentum convallis, tortor turpis aliquam odio, sed blandit libero lacus et eros. In eleifend iaculis magna ac finibus. Praesent auctor facilisis tellus in congue. Sed molestie lobortis dictum. Nam quis dignissim augue, vel euismod lorem. Curabitur posuere dapibus luctus. Donec ultricies dictum lectus, quis blandit arcu commodo ac. Aenean tincidunt ligula in nunc imperdiet dignissim. Curabitur egestas sollicitudin sapien ut semper. Aenean nec dignissim lacus.
|
||||
|
||||
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec aliquam dictum vehicula. Donec tortor est, volutpat non nisi nec, varius gravida ex. Nunc vel tristique nunc, vitae mattis nisi. Nunc nec luctus ex, vitae tincidunt lectus. In hac habitasse platea dictumst. Curabitur lobortis ex eget tincidunt tempor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut a vehicula mi.
|
||||
|
||||
Fusce eu libero finibus, interdum nulla a, placerat neque. Cras bibendum tempor libero nec feugiat. Cras ut sodales eros. Proin viverra, massa sit amet viverra egestas, neque nisl porta ex, sit amet hendrerit libero ligula vel urna. Mauris suscipit lacus id justo rhoncus suscipit. Etiam vel libero tellus. Maecenas non diam molestie, condimentum tellus a, bibendum enim. Mauris aliquet imperdiet tellus, eget sagittis dolor. Sed blandit in neque et luctus. Cras elementum sagittis nunc, vel mollis lorem euismod et. Donec posuere at lacus eget suscipit.
|
||||
|
||||
Nulla nunc mi, pretium non massa vel, tempor semper magna. Nunc a leo pulvinar, tincidunt nunc at, dignissim mi. Aliquam erat volutpat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut viverra nulla a nisl finibus, at hendrerit ligula ullamcorper. Donec a lorem semper, tempor magna et, lobortis libero. Mauris id sapien leo. Donec dignissim, quam vitae porttitor dignissim, quam justo mattis dui, vel consequat odio elit quis orci. Etiam nec pretium neque, sit amet pretium orci. Duis ac tortor venenatis, feugiat purus non, feugiat nunc. Proin scelerisque nisl in turpis aliquam vulputate.
|
||||
|
||||
Praesent sed est semper, fringilla lorem vitae, tincidunt nibh. Cras eros metus, auctor at mauris sit amet, sodales semper orci. Nunc a ornare ex. Curabitur bibendum arcu congue urna vulputate egestas. Vestibulum finibus id risus et accumsan. Aenean ut volutpat tellus. Aenean tincidunt malesuada urna sit amet vestibulum. Mauris vel tellus dictum, varius lacus quis, dictum arcu.
|
||||
|
||||
Aenean quis metus eu erat feugiat cursus vel at ligula. Proin dapibus sodales urna, id euismod lectus tempus id. Pellentesque ex ligula, convallis et erat vel, vulputate condimentum nisl. Pellentesque pharetra nulla quis massa eleifend hendrerit. Praesent sed massa ipsum. Maecenas vehicula dolor massa, id sodales urna faucibus et. Mauris ac quam non massa tincidunt feugiat et at lacus. Fusce libero massa, vulputate vel scelerisque non, mollis in leo. Ut sit amet ultricies odio. Suspendisse in sapien viverra, facilisis purus ut, pretium libero.
|
||||
|
||||
Vivamus tristique pharetra molestie. Nam a volutpat purus. Praesent consequat gravida nisi, ac blandit nisi suscipit ut. Quisque posuere, ligula a ultrices laoreet, ligula nunc vulputate libero, ut rutrum erat odio tincidunt justo. Sed vitae leo at leo fringilla bibendum. Vestibulum ut augue nec dolor auctor accumsan. Praesent laoreet id eros pulvinar commodo. Suspendisse potenti. Ut pharetra, mauris vitae blandit fringilla, odio ante tincidunt lorem, sit amet tempor metus diam ut turpis.
|
||||
|
||||
Praesent quis egestas arcu. Nullam at porta arcu. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi vulputate ligula malesuada ligula luctus, vulputate tempus erat bibendum. Nunc ullamcorper non lectus at euismod. Etiam nibh felis, tincidunt a metus vel, pellentesque rhoncus neque. Etiam at diam in erat luctus interdum. Nunc vel ipsum pulvinar, sollicitudin lacus ac, tempus urna. Etiam vel lacinia sapien. Pellentesque sagittis velit vel mi efficitur iaculis. Integer euismod sit amet urna in sagittis. Cras eleifend ut nibh in facilisis. Donec et lacus vitae nunc placerat sodales. Nulla sed hendrerit ligula, at dapibus sapien.
|
||||
|
||||
Praesent at iaculis ex. Curabitur est purus, cursus a faucibus quis, dictum id velit. Donec dignissim fringilla viverra. Nunc mauris felis, laoreet sit amet sagittis at, vestibulum in libero. Maecenas quis orci turpis. Quisque ut nibh vitae magna mollis consequat id at mauris. Aliquam eu odio eget nulla bibendum sodales. Quisque vel orci eleifend nisi pretium lacinia. Suspendisse eget risus eget mi volutpat molestie eget quis lacus. Duis nisi libero, tincidunt nec nulla id, faucibus cursus felis.
|
||||
|
||||
Donec tempor eget risus pellentesque molestie. Phasellus porta neque vel arcu egestas, nec blandit velit fringilla. Nullam porta faucibus justo vitae laoreet. Pellentesque viverra id nunc eu varius. Nulla pulvinar lobortis iaculis. Etiam vestibulum odio nec velit tristique, a tristique nisi mattis. In sed fringilla orci, vitae efficitur odio. Quisque dui odio, ornare eget velit at, lacinia consequat libero. Quisque lectus nulla, aliquet eu leo in, porta rutrum diam. Donec nec mattis neque. Nam rutrum, odio ac eleifend bibendum, dolor arcu rutrum neque, eget porta elit tellus a lacus. Sed massa metus, sollicitudin et sapien eu, finibus tempus orci. Proin et sapien sit amet erat molestie interdum. In quis rutrum velit, faucibus ultrices tellus.
|
||||
|
||||
Sed sagittis sed justo eget tincidunt. Maecenas ut leo sagittis, feugiat magna et, viverra velit. Maecenas ex arcu, feugiat at consequat vitae, auctor eu massa. Integer egestas, enim vitae maximus convallis, est lectus pretium mauris, ac posuere lectus nisl quis quam. Aliquam tempus laoreet mi, vitae dapibus dolor varius dapibus. Suspendisse potenti. Donec sit amet purus nec libero dapibus tristique. Pellentesque viverra bibendum ligula. Donec sed felis et ex lobortis laoreet. Phasellus a fringilla libero, vitae malesuada nulla. Pellentesque blandit mattis lacus, et blandit tortor laoreet consequat. Suspendisse libero nunc, viverra sed fermentum in, accumsan egestas arcu. Proin in placerat elit. Sed interdum imperdiet malesuada. Suspendisse aliquet quis mauris eget sollicitudin.
|
||||
|
||||
Vivamus accumsan tellus non erat volutpat, quis dictum dolor feugiat. Praesent rutrum nunc ac est mollis cursus. Fusce semper volutpat dui ut egestas. Curabitur sit amet posuere massa. Cras tincidunt nulla et mi mollis imperdiet. Suspendisse scelerisque ex id sodales vulputate. In nunc augue, pharetra in placerat eu, mattis id tellus. Vivamus cursus efficitur vehicula. Nulla aliquet vehicula aliquet.
|
||||
|
||||
Sed cursus tellus sed porta pulvinar. Sed vitae nisi neque. Nullam aliquet, lorem et efficitur scelerisque, arcu diam aliquam felis, sed pulvinar lorem odio et turpis. Praesent convallis pulvinar turpis eu iaculis. Aliquam nec gravida mi. Curabitur eu nibh tempor, blandit justo in, ultrices felis. Fusce placerat metus non mi sagittis rutrum. Morbi sed dui fringilla, sagittis mauris eget, imperdiet nunc. Phasellus hendrerit sem elit, id hendrerit libero auctor sit amet. Integer sodales elit sit amet consequat cursus.
|
||||
|
||||
Nam semper est eget nunc mollis, in pellentesque lectus fringilla. In finibus vel diam id semper. Nunc mattis quis erat eu consectetur. In hac habitasse platea dictumst. Nullam et ipsum vestibulum ex pulvinar ultricies sit amet id velit. Aenean suscipit mi tortor, a lobortis magna viverra non. Nulla condimentum aliquet ante et ullamcorper. Pellentesque porttitor arcu a posuere tempus. Aenean lacus quam, imperdiet eu justo vitae, pretium efficitur ex. Duis id purus id magna rhoncus ultrices id eu risus. Nunc dignissim et libero id dictum.
|
||||
|
||||
Quisque a tincidunt neque. Phasellus commodo mi sit amet tempor fringilla. Ut rhoncus, neque non porttitor elementum, libero nulla egestas augue, sed fringilla sapien felis ac velit. Phasellus viverra rhoncus mollis. Nam ullamcorper leo vel erat laoreet luctus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus semper a metus a cursus. Nulla sed orci egestas, efficitur purus ac, malesuada tellus. Aenean rutrum velit at tellus fermentum mollis. Aliquam eleifend euismod metus.
|
||||
|
||||
In hac habitasse platea dictumst. Vestibulum volutpat neque vitae porttitor laoreet. Nam at tellus consequat, sodales quam in, pulvinar arcu. Maecenas varius convallis diam, ac lobortis tellus pellentesque quis. Maecenas eget augue massa. Nullam volutpat nibh ac justo rhoncus, ut iaculis tellus rutrum. Fusce efficitur efficitur libero quis condimentum. Curabitur congue neque non tincidunt tristique. Fusce eget tempor ex, at pellentesque odio. Praesent luctus dictum vestibulum. Etiam non orci nunc. Vivamus vitae laoreet purus, a lobortis velit. Curabitur tincidunt purus ac lectus elementum pellentesque. Quisque sed tincidunt est.
|
||||
|
||||
Sed vel ultrices massa, vitae ultricies justo. Cras finibus mauris nec lacus tempus dignissim. Cras faucibus maximus velit, eget faucibus orci luctus vehicula. Nulla massa nunc, porta ac consequat eget, rhoncus non tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce sed maximus metus, vel imperdiet ipsum. Ut scelerisque lectus at blandit porttitor. Ut vulputate nunc pharetra, aliquet sapien ac, sollicitudin sapien. Aenean eget ante lorem. Nam accumsan venenatis tellus id dignissim.
|
||||
|
||||
Curabitur fringilla, magna non maximus dapibus, nulla sapien vestibulum lectus, sit amet semper dolor neque vitae nisl. Nunc ultrices vehicula augue sed iaculis. Maecenas nec diam mollis, suscipit orci et, vestibulum ante. Pellentesque eu nisl tortor. Nunc eleifend, lacus quis volutpat volutpat, nisi mi molestie sem, quis mollis ipsum libero a tellus. Ut viverra dolor mattis convallis interdum. Sed tempus nisl at nunc scelerisque aliquet. Quisque tempor tempor lorem id feugiat. Nullam blandit lectus velit, vitae porta lacus tincidunt a. Vivamus sit amet arcu ultrices, tincidunt mi quis, viverra quam. Aenean fringilla libero elementum lorem semper, quis pulvinar eros gravida. Nullam sodales blandit mauris, sed fermentum velit fermentum sit amet. Donec malesuada mauris in augue sodales vulputate. Vestibulum gravida turpis id elit rhoncus dignissim. Integer non congue lorem, eu viverra orci.
|
||||
|
||||
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec at dolor magna. Aliquam consectetur erat augue, id iaculis velit pharetra ac. Integer rutrum venenatis dignissim. Integer non sodales elit. Curabitur ut magna ut nibh feugiat aliquam ac ut risus. Morbi nibh quam, aliquam id placerat nec, vestibulum eget velit. Suspendisse at dignissim quam. Vivamus aliquet sem sed nisl volutpat, ut cursus orci ultrices. Aliquam ultrices lacinia enim, vitae aliquet neque.
|
||||
|
||||
Quisque scelerisque finibus diam in mattis. Cras cursus auctor velit. Aliquam sem leo, fermentum et maximus et, molestie a libero. Aenean justo elit, rutrum a ornare id, egestas eget enim. Aenean auctor tristique erat. Curabitur condimentum libero lacus, nec consequat orci vestibulum sed. Fusce elit ligula, blandit vitae sapien vitae, dictum ultrices risus. Nam laoreet suscipit sapien, at interdum velit faucibus sit amet. Duis quis metus egestas lectus elementum posuere non nec libero. Aliquam a dolor bibendum, facilisis nunc a, maximus diam. Vestibulum suscipit tristique magna, non dignissim turpis sodales sed. Nunc ornare, velit ac facilisis fringilla, dolor mi consectetur lorem, vitae finibus erat justo suscipit urna. Maecenas sit amet eros erat. Nunc non arcu ornare, suscipit lorem eget, sodales mauris. Aliquam tincidunt, quam nec mollis lacinia, nisi orci fermentum libero, consequat eleifend lectus quam et sapien. Vestibulum a quam urna.
|
||||
|
||||
Cras arcu leo, euismod ac ullamcorper at, faucibus sed massa. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus porttitor velit in enim interdum, non commodo metus ornare. Morbi vel lorem quis nisl luctus tristique quis vitae nisl. Suspendisse condimentum tortor enim, nec eleifend ipsum euismod et. Sed gravida quam ut tristique lacinia. Mauris eu interdum ipsum, ac ultrices odio. Nullam auctor tellus a risus porttitor vehicula. Nulla blandit euismod dictum. In pharetra, enim iaculis pulvinar interdum, dui nunc placerat nunc, sit amet pretium lectus nulla vitae quam. Phasellus quis enim sollicitudin, varius nulla id, ornare purus. Donec quam lacus, vestibulum quis nunc ac, mollis dictum nisi. Cras ut mollis elit. Maecenas ultrices ligula at risus faucibus scelerisque. Etiam vitae porttitor purus. Curabitur blandit lectus urna, ut hendrerit tortor feugiat ut.
|
||||
|
||||
Phasellus fringilla, sapien pellentesque commodo pharetra, ante libero aliquam tellus, ut consectetur augue libero a sapien. Maecenas blandit luctus nisl eget aliquet. Maecenas vitae porta dolor, faucibus laoreet sapien. Suspendisse lobortis, ipsum sed vehicula aliquam, elit purus scelerisque dui, rutrum consectetur diam odio et lorem. In nec lacinia metus. Donec viverra libero est, vel bibendum erat condimentum quis. Donec feugiat purus leo. In laoreet vitae felis a porttitor. Mauris ullamcorper, lacus id condimentum suscipit, neque magna pellentesque arcu, eget cursus neque tellus id metus. Curabitur volutpat ac orci vel ultricies.
|
||||
|
||||
Sed ut finibus erat. Sed diam purus, varius non tincidunt quis, ultrices sit amet ipsum. Donec et egestas nulla. Suspendisse placerat nisi at dui laoreet iaculis. Aliquam aliquet leo at augue faucibus molestie. Nullam lacus augue, hendrerit sed nisi eu, faucibus porta est. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam ut leo aliquet sem fermentum rutrum quis ac justo. Integer placerat aliquam nisl ut sagittis. Proin erat orci, lobortis et sem eget, eleifend fringilla augue. Mauris varius laoreet arcu, sed tincidunt felis. Pellentesque venenatis lorem odio, id pulvinar velit molestie feugiat. Donec mattis lacus sed eleifend pulvinar.
|
||||
|
||||
Sed condimentum ex in tincidunt hendrerit. Etiam eget risus lacinia, euismod nibh eu, pellentesque quam. Proin elit eros, convallis id mauris ac, bibendum ultrices lectus. Morbi venenatis, purus id fermentum consequat, nunc libero tincidunt ligula, non dictum ligula orci nec quam. Nulla nec ultrices lorem. Aenean maximus augue vel dictum pharetra. Etiam turpis urna, pellentesque quis malesuada eu, molestie faucibus felis.
|
||||
|
||||
Vestibulum pharetra augue ut quam blandit congue in nec risus. Proin eu nibh eu dui eleifend porta vitae id lectus. Proin lacus nibh, lobortis sed ligula vitae, interdum lobortis erat. Suspendisse potenti. In sollicitudin quis sapien ut aliquet. Mauris ac nulla arcu. Fusce tristique justo quis lectus mollis, eu volutpat lectus finibus. Vivamus venenatis facilisis ex ut vestibulum.
|
||||
|
||||
Etiam varius lobortis purus, in hendrerit elit tristique at. In tempus, augue vestibulum fermentum gravida, ligula tellus vulputate arcu, eu molestie ex sapien at purus. Vestibulum nec egestas metus. Duis pulvinar quam nec consequat interdum. Aenean non dapibus lacus. Aliquam sit amet aliquet nulla. Sed venenatis volutpat purus nec convallis. Phasellus aliquet semper sodales. Cras risus sapien, condimentum auctor urna a, pulvinar ornare nisl. Sed tincidunt felis elit, ut elementum est bibendum ac. Morbi interdum justo vel dui faucibus condimentum.
|
||||
|
||||
Sed convallis eu sem at tincidunt. Nullam at auctor est, et ullamcorper ipsum. Pellentesque eget ante ante. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer euismod, sapien sed dapibus ornare, nibh enim maximus lacus, lacinia placerat urna quam quis felis. Morbi accumsan id nisl ut condimentum. Donec bibendum nisi est, sed volutpat lorem rhoncus in. Vestibulum ac lacinia nunc, eget volutpat magna. Integer aliquam pharetra ipsum, id placerat nunc volutpat quis. Etiam urna diam, rhoncus sit amet varius vel, euismod vel sem. Nullam vel molestie urna. Vivamus ornare erat at venenatis euismod. Suspendisse potenti. Fusce diam justo, tincidunt vel sem at, commodo faucibus nisl. Duis gravida efficitur diam, vel sagittis erat pulvinar ut.
|
||||
|
||||
Quisque vel pharetra felis. Duis efficitur tortor dolor, vitae porttitor erat fermentum sed. Sed eu mi purus. Etiam dignissim tortor eu tempus molestie. Aenean pretium erat enim, in hendrerit ante hendrerit at. Sed ut risus vel nunc venenatis ultricies quis in lacus. Pellentesque vitae purus euismod, placerat risus non, ullamcorper augue. Quisque varius quam ligula, nec aliquet ex faucibus vitae. Quisque rhoncus sit amet leo tincidunt mattis. Cras id mauris eget purus pretium gravida sit amet eu augue. Aliquam dapibus odio augue, id lacinia velit pulvinar eu.
|
||||
|
||||
Mauris fringilla, tellus nec pharetra iaculis, neque nisi ultrices massa, et tincidunt sem dui sed mi. Curabitur erat lorem, venenatis quis tempus lacinia, tempus sit amet nunc. Aliquam at neque ac metus commodo dictum quis vitae justo. Phasellus eget lacus tempus, blandit lorem vel, rutrum est. Aenean pharetra sem ut augue lobortis dignissim. Sed rhoncus at nulla id ultrices. Cras id condimentum felis. In suscipit luctus vulputate. Donec tincidunt lacus nec enim tincidunt sollicitudin ut quis enim. Nam at libero urna. Praesent sit amet massa vitae massa ullamcorper vehicula.
|
||||
|
||||
Nullam bibendum augue ut turpis condimentum bibendum. Proin sit amet urna hendrerit, sodales tortor a, lobortis lectus. Integer sagittis velit turpis, et tincidunt nisi commodo eget. Duis tincidunt elit finibus accumsan cursus. Aenean dignissim scelerisque felis vel lacinia. Nunc lacinia maximus luctus. In hac habitasse platea dictumst. Vestibulum eget urna et enim tempor tempor. Nam feugiat, felis vel vestibulum tempus, orci justo viverra diam, id dapibus lorem justo in ligula.
|
||||
|
||||
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In ac pellentesque sem. Vestibulum lacinia magna dui, eu lacinia augue placerat et. Maecenas pulvinar congue est. Pellentesque commodo dui non pulvinar scelerisque. Etiam interdum est posuere sem bibendum, ac commodo magna dictum. Cras ipsum turpis, rhoncus nec posuere vitae, laoreet a arcu. Integer ac massa sit amet enim placerat lacinia sed ultrices arcu. Suspendisse sem nibh, luctus sit amet volutpat in, pellentesque eu metus. Ut gravida neque eget mi accumsan tempus. Nam sit amet aliquet nibh.
|
||||
|
||||
Pellentesque a purus cursus nulla hendrerit congue quis et odio. Aenean hendrerit, leo ullamcorper sagittis hendrerit, erat dui molestie quam, sed condimentum lacus risus sed tellus. Morbi a dapibus lectus, ut feugiat ex. Phasellus pretium quam et sapien mollis, vel iaculis dui dignissim. Sed ullamcorper est turpis, a viverra lorem consectetur in. Aenean aliquet nibh non cursus rutrum. Suspendisse at tristique urna, id lobortis urna. In hac habitasse platea dictumst. Phasellus libero velit, rutrum sed tellus nec, dapibus tincidunt ligula. Quisque vel dui venenatis, consequat nisl ut, lacinia ipsum. Phasellus vitae magna pellentesque, lobortis est id, faucibus quam. Nam eleifend faucibus dui vel pellentesque.
|
||||
|
||||
Etiam ut est non lacus tincidunt interdum. Maecenas sed massa urna. Quisque ut nibh tortor. Pellentesque felis ipsum, tempor finibus ipsum et, euismod pretium metus. Donec sit amet est ipsum. Quisque rhoncus justo non finibus elementum. Nulla nec lectus ac tortor placerat fringilla. Phasellus ac ultrices nunc, eu efficitur nisl. Nulla rhoncus nunc vitae ante dictum tincidunt. Nunc ultrices, massa sit amet malesuada dignissim, lectus lacus consequat sapien, non eleifend metus sem in eros. Phasellus mauris ante, dictum sit amet suscipit ac, rhoncus eget nisi. Phasellus at orci mollis, imperdiet neque eget, faucibus nulla. In at purus massa. Pellentesque quis rutrum lectus.
|
||||
|
||||
Integer eu faucibus turpis, sit amet mollis massa. Vestibulum id nulla commodo, rutrum ipsum sed, semper ante. Phasellus condimentum orci nec nibh convallis, ac maximus orci ullamcorper. Maecenas vitae sollicitudin mi. Integer et finibus lectus, et condimentum ligula. Donec elementum tristique quam vitae dapibus. Morbi euismod ipsum in tristique ullamcorper.
|
||||
|
||||
Duis fermentum non enim eu auctor. Quisque lacinia nibh vehicula nibh posuere, eu volutpat turpis facilisis. Ut ac faucibus nulla. Sed eleifend quis ex et pellentesque. Vestibulum sollicitudin in libero id fringilla. Phasellus dignissim purus consequat, condimentum dui sit amet, condimentum ante. Pellentesque ac consectetur massa, quis sagittis est. Nulla maximus tristique risus accumsan convallis. Curabitur imperdiet ac lacus a ultrices. Nulla facilisi. Sed quis quam quis lectus placerat lobortis vel sed turpis. In mollis dui id neque iaculis, ut aliquet tellus malesuada. Proin at luctus odio, vel blandit sapien. Praesent dignissim tortor vehicula libero fringilla, nec ultrices erat suscipit. Maecenas scelerisque purus in dapibus fermentum.
|
||||
|
||||
Curabitur magna odio, mattis in tortor ut, porttitor congue est. Vestibulum mollis lacinia elementum. Fusce maximus erat vitae nunc rutrum lobortis. Integer ligula eros, auctor vel elit non, posuere luctus lacus. Maecenas quis auctor massa. Ut ipsum lacus, efficitur posuere euismod et, hendrerit efficitur est. Phasellus fringilla, quam id tincidunt pretium, nunc dui sollicitudin orci, eu dignissim nisi metus ut magna. Integer lobortis interdum dolor, non bibendum purus posuere et. Donec non lectus aliquet, pretium dolor eu, cursus massa. Sed ut dui sapien. In sed vestibulum massa. Pellentesque blandit, dui non sodales vehicula, orci metus mollis nunc, non pharetra ex tellus ac est. Mauris sagittis metus et fermentum pretium. Nulla facilisi. Quisque quis ante ut nulla placerat mattis ut quis nisi.
|
||||
|
||||
Sed quis nulla ligula. Quisque dignissim ligula urna, sed aliquam purus semper at. Suspendisse potenti. Nunc massa lectus, pharetra vehicula arcu bibendum, imperdiet sodales ipsum. Nam ac sapien diam. Mauris iaculis fringilla mattis. Pellentesque tempus eros sit amet justo volutpat mollis. Phasellus ac turpis ipsum. Morbi vel ante elit. Aenean posuere quam consequat velit varius suscipit. Donec tempor quam ut nibh cursus efficitur.
|
||||
|
||||
Morbi molestie dolor nec sem egestas suscipit. Etiam placerat pharetra lectus, et ullamcorper risus tristique in. Sed faucibus ullamcorper lectus eget fringilla. Maecenas malesuada hendrerit congue. Sed eget neque a erat placerat tincidunt. Aliquam vitae dignissim turpis. Fusce at placerat magna, a laoreet lectus. Maecenas a purus nec diam gravida fringilla. Nam malesuada euismod ante non vehicula. In faucibus bibendum leo, faucibus posuere nisl pretium quis. Fusce finibus bibendum finibus. Vestibulum eu justo maximus, hendrerit diam nec, dignissim sapien. Aenean dolor lacus, malesuada quis vestibulum ac, venenatis ac ipsum. Cras a est id nunc finibus facilisis. Cras lacinia neque et interdum vehicula. Suspendisse vulputate tellus elit, eget tempor dui finibus vel.
|
||||
|
||||
Cras sed pretium odio. Proin hendrerit elementum felis in tincidunt. Nam sed turpis vel justo molestie accumsan condimentum eu nunc. Praesent lobortis euismod rhoncus. Nulla vitae euismod nibh, quis mattis mi. Fusce ultrices placerat porttitor. Duis sem ipsum, pellentesque sit amet odio a, molestie vulputate mauris.
|
||||
|
||||
Duis blandit mollis ligula, sit amet mattis ligula finibus sit amet. Nunc a leo molestie, placerat diam et, vestibulum leo. Suspendisse facilisis neque purus, nec pellentesque ligula fermentum nec. Aenean malesuada mauris lorem, eu blandit arcu pulvinar quis. Duis laoreet urna lacus, non maximus arcu rutrum ultricies. Nulla augue dolor, suscipit eu mollis eu, aliquam condimentum diam. Ut semper orci luctus, pharetra turpis at, euismod mi. Nulla leo diam, finibus sit amet purus sed, maximus dictum lorem. Integer eu mi id turpis laoreet rhoncus.
|
||||
|
||||
Integer a mauris tincidunt, finibus orci ut, pretium mauris. Nulla molestie nunc mi, id finibus lorem elementum sed. Proin quis laoreet ante. Integer nulla augue, commodo id molestie quis, rutrum ut turpis. Suspendisse et tortor turpis. Sed ut pharetra massa. Pellentesque elementum blandit sem, ut elementum tellus egestas a. Fusce eu purus nibh.
|
||||
|
||||
Cras dignissim ligula scelerisque magna faucibus ullamcorper. Proin at condimentum risus, auctor malesuada quam. Nullam interdum interdum egestas. Nulla aliquam nisi vitae felis mollis dictum. Suspendisse dapibus consectetur tortor. Ut ut nisi non sem bibendum tincidunt. Vivamus suscipit leo quis gravida dignissim.
|
||||
|
||||
Aliquam interdum, leo id vehicula mollis, eros eros rhoncus diam, non mollis ligula mi eu mauris. Sed ultrices vel velit sollicitudin tincidunt. Nunc auctor metus at ligula gravida elementum. Praesent interdum eu elit et mollis. Duis egestas quam sit amet velit dignissim consequat. Aliquam ac turpis nec nunc convallis sagittis. Fusce blandit, erat ac fringilla consectetur, dolor eros sodales leo, vel aliquet risus nisl et diam. Aliquam luctus felis vitae est eleifend euismod facilisis et lacus. Sed leo tellus, auctor eu arcu in, volutpat sagittis nisl. Pellentesque nisl ligula, placerat vel ullamcorper at, vulputate ac odio. Morbi ac faucibus orci, et tempus nulla. Proin rhoncus rutrum dolor, in venenatis mauris. Suspendisse a fermentum augue, non semper mi. Nunc eget pretium neque. Phasellus augue erat, feugiat ac aliquam congue, rutrum non sapien. Pellentesque ac diam gravida, consectetur felis at, ornare neque.
|
||||
|
||||
Nullam interdum mattis sapien quis porttitor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus aliquet rutrum ipsum id euismod. Maecenas consectetur massa et mi porta viverra. Nunc quam nibh, dignissim vitae maximus et, ullamcorper nec lorem. Nunc vitae justo dapibus, luctus lacus vitae, pretium elit. Maecenas et efficitur leo. Curabitur mauris lectus, placerat quis vehicula vitae, auctor ut urna. Quisque rhoncus pharetra luctus. In hac habitasse platea dictumst. Integer sit amet metus nec eros malesuada aliquam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi hendrerit mi ac leo aliquam, sit amet ultricies libero commodo. Mauris dapibus purus metus, sit amet viverra nibh imperdiet et. Nullam porta nulla tellus, quis vehicula diam imperdiet non. Vivamus enim massa, bibendum in fermentum in, ultrices at ex.
|
||||
|
||||
Suspendisse fermentum id nibh eget accumsan. Duis dapibus bibendum erat ut sollicitudin. Aliquam nec felis risus. Pellentesque rhoncus ligula id sem maximus mollis sed nec massa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ipsum ipsum, sodales sed enim id, convallis faucibus eros. Donec ultricies dictum tincidunt. Cras vitae nibh arcu. Pellentesque cursus, sapien nec consequat fermentum, ipsum ante suscipit dui, imperdiet hendrerit est nisl eu massa. Quisque vitae sem ligula. Aenean iaculis metus ut mauris interdum laoreet. Vivamus sed gravida dolor.
|
||||
|
||||
Morbi nulla metus, porttitor sed eros sit amet, efficitur efficitur est. In vel nisl urna. Ut aliquet tellus at congue convallis. Phasellus imperdiet lobortis sollicitudin. Integer sodales, sem eu ultricies pharetra, erat erat porttitor odio, eget dapibus libero ipsum eget velit. Phasellus gravida nulla nisl, eu pharetra mi auctor vel. Sed blandit pharetra velit, ut egestas libero placerat non. Aliquam a interdum quam. Proin at tortor nec dui sollicitudin tempus sed vestibulum elit. Nunc non sollicitudin velit.
|
||||
|
||||
Aenean consequat diam velit, sed rutrum tortor faucibus dictum. Quisque at semper augue. Duis ut est eget mi ornare bibendum id et ligula. Phasellus consequat tortor non leo pulvinar posuere. Proin vestibulum eleifend felis, in hendrerit tortor sollicitudin eu. Phasellus hendrerit, lacus vel laoreet interdum, dui tortor consequat justo, commodo ultricies arcu felis vitae enim. Vivamus eu sapien at leo suscipit rutrum eu at justo. Aenean et dolor a libero ullamcorper posuere. Integer laoreet placerat nisi in vulputate. Mauris laoreet eget risus sed cursus. Donec scelerisque neque a libero eleifend hendrerit. Nulla varius condimentum nunc sit amet fermentum. Aliquam lorem ex, varius nec mollis ut, ultrices in neque. Morbi sit amet porta leo. Integer iaculis fermentum lacus in vestibulum.
|
||||
|
||||
Ut gravida, tellus ut maximus ultrices, erat est venenatis nisl, vitae pretium massa ex ac magna. Sed non purus eget ligula aliquet volutpat non quis arcu. Nam aliquam tincidunt risus, sit amet fringilla sapien vulputate ut. Mauris luctus suscipit pellentesque. Nunc porttitor dapibus ex quis tempus. Ut ullamcorper metus a eros vulputate, vitae viverra lectus convallis. Mauris semper imperdiet augue quis tincidunt. Integer porta pretium magna, sed cursus sem scelerisque sollicitudin. Nam efficitur, nibh pretium eleifend vestibulum, purus diam posuere sem, in egestas mauris augue sit amet urna.
|
||||
|
||||
Vestibulum tincidunt euismod massa in congue. Duis interdum metus non laoreet fringilla. Donec at ligula congue, tincidunt nunc non, scelerisque nunc. Donec bibendum magna non est scelerisque feugiat at nec neque. Ut orci tortor, tempus eget massa non, dignissim faucibus dolor. Nam odio risus, accumsan pretium neque eget, accumsan dignissim dui. In ut neque auctor, scelerisque tellus sed, ullamcorper nisi. Suspendisse varius cursus quam at hendrerit. Vivamus elit libero, sagittis vitae sem ac, vulputate iaculis ligula.
|
||||
|
||||
Sed lobortis laoreet purus sit amet rutrum. Pellentesque feugiat non leo vel lacinia. Quisque feugiat nisl a orci bibendum vestibulum. In et sollicitudin urna. Morbi a arcu ac metus faucibus tempus. Nam eu imperdiet sapien, suscipit mattis tortor. Aenean blandit ipsum nisi, a eleifend ligula euismod at. Integer tincidunt pharetra felis, mollis placerat mauris hendrerit at. Curabitur convallis, est sit amet luctus volutpat, massa lacus cursus augue, sed eleifend magna quam et risus. Aliquam lobortis tincidunt metus vitae porttitor. Suspendisse potenti. Aenean ullamcorper, neque id commodo luctus, nulla nunc lobortis quam, id dapibus neque dui nec mauris. Etiam quis lorem quis elit commodo ornare. Ut pharetra purus ultricies enim ultrices efficitur. Proin vehicula tincidunt molestie. Mauris et placerat sem.
|
||||
|
||||
Aliquam erat volutpat. Suspendisse velit turpis, posuere ac lacus eu, lacinia laoreet velit. Sed interdum felis neque, id blandit sem malesuada sit amet. Ut sagittis justo erat, efficitur semper orci tempor sed. Donec enim massa, posuere varius lectus egestas, pellentesque posuere mi. Cras tincidunt ut libero sed mattis. Suspendisse quis magna et tellus posuere interdum vel at purus. Pellentesque fringilla tristique neque, id aliquet tellus ultricies non. Duis ut tellus vel odio lobortis vulputate.
|
||||
|
||||
Integer at magna ac erat convallis vestibulum. Sed lobortis porttitor mauris. Fusce varius lorem et volutpat pulvinar. Aenean ac vulputate lectus, vitae consequat velit. Suspendisse ex dui, varius ut risus ut, dictum scelerisque sem. Vivamus urna orci, volutpat ut convallis ac, venenatis vitae urna. In hac habitasse platea dictumst. Etiam eu purus arcu. Aenean vulputate leo urna, vel tristique dui sagittis euismod. Suspendisse non tellus efficitur ante rhoncus volutpat at et sapien.
|
||||
|
||||
Sed dapibus accumsan porttitor. Phasellus facilisis lectus finibus ligula dignissim, id pulvinar lectus feugiat. Nullam egestas commodo nisi posuere aliquet. Morbi sit amet tortor sagittis, rutrum dui nec, dapibus sapien. Sed posuere tortor tortor, interdum auctor magna varius vitae. Vestibulum id sagittis augue. Curabitur fermentum arcu sem, eu condimentum quam rutrum non. Phasellus rutrum nibh quis lectus rhoncus pretium. Curabitur dictum interdum elit. Vestibulum maximus sodales imperdiet. Mauris auctor nec purus sed venenatis. In in urna purus.
|
||||
|
||||
Duis placerat molestie suscipit. Morbi a elit id purus efficitur consequat. Nunc ac commodo turpis. Etiam sit amet lacus a ipsum tempus venenatis sed vel nibh. Duis elementum aliquam mi sed tristique. Morbi ligula tortor, semper ac est vel, lobortis maximus erat. Curabitur ipsum felis, laoreet vel condimentum eget, ullamcorper sit amet mauris. Nulla facilisi. Nam at purus sed mi egestas placerat vitae vel magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse at dignissim diam. Phasellus consectetur eget neque vel viverra. Donec sollicitudin mattis dolor vel malesuada. Vivamus vehicula leo neque, vitae fermentum leo posuere et. Praesent dui est, finibus sit amet tristique quis, pharetra vel nibh.
|
||||
|
||||
Duis nulla leo, accumsan eu odio eget, sagittis semper orci. Quisque ullamcorper ligula quam, commodo porttitor mauris ullamcorper eu. Cras varius sagittis felis in aliquam. Duis sodales risus ac justo vehicula, nec mattis diam lacinia. Cras eget lectus ipsum. Ut commodo, enim vitae malesuada hendrerit, ex dolor egestas lectus, sit amet hendrerit metus diam nec est. Vestibulum tortor metus, lobortis sit amet ante eget, tempor molestie lacus. In molestie et urna et semper. Mauris mollis, sem non hendrerit condimentum, sapien nisi cursus est, non suscipit quam justo non metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam enim est, porta ac feugiat vitae, rutrum in lorem. Duis vehicula tortor ut posuere maximus.
|
||||
|
||||
Nullam vestibulum non tellus sed commodo. Quisque mattis elit sit amet sapien sollicitudin, ut condimentum nisl congue. Aenean sagittis massa vel elit faucibus fermentum. Donec tincidunt nisi nec nisl sodales pellentesque. Mauris congue congue ligula ut suscipit. Vivamus velit tortor, tempor et gravida eget, fermentum sit amet ante. Nullam fringilla, lorem at ultrices cursus, urna neque ornare dolor, eu lacinia orci enim sed nibh. Ut a ullamcorper lectus, id mattis purus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean maximus sollicitudin posuere. Nunc at augue lacus. Aenean efficitur leo sit amet lacinia efficitur.
|
||||
|
||||
Quisque venenatis quam mi, in pharetra odio vulputate eu. In vel nisl pulvinar, pulvinar ligula ut, sodales risus. Sed efficitur lectus at vestibulum tincidunt. Vestibulum eu ullamcorper elit. Fusce vestibulum magna enim, et tempor lacus posuere vitae. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer leo elit, luctus nec mattis sit amet, sollicitudin in turpis.
|
||||
|
||||
Proin convallis venenatis leo, vitae tristique erat iaculis nec. Nulla facilisi. Duis porttitor, sapien et bibendum vulputate, sem libero sodales lacus, non malesuada felis erat ut libero. Nam non felis semper, finibus est a, mattis mauris. Praesent nec eros quam. Nulla hendrerit, augue consectetur eleifend ultricies, purus mi condimentum nulla, eget dapibus est nunc sed libero. Nullam elementum dui erat, vitae luctus libero sollicitudin et. Nulla odio magna, placerat in augue eu, dapibus imperdiet odio. Suspendisse imperdiet metus sit amet rhoncus dapibus. Cras at enim et urna vehicula cursus eu a mauris. Integer magna ante, eleifend ac placerat vitae, porta at nisi. Cras eget malesuada orci. Curabitur nunc est, vulputate id viverra et, dignissim sed odio. Curabitur non mattis sem. Sed bibendum, turpis vitae vehicula faucibus, nunc quam ultricies lectus, vitae viverra felis turpis at libero.
|
||||
|
||||
Nullam ut egestas ligula. Proin hendrerit justo a lectus commodo venenatis. Nulla facilisi. Ut cursus lorem quis est bibendum condimentum. Aenean in tristique odio. Fusce tempor hendrerit ipsum. Curabitur mollis felis justo, quis dapibus erat auctor vel. Sed augue lectus, finibus ut urna quis, ullamcorper vestibulum dui. Etiam molestie aliquam tempor. Integer mattis sollicitudin erat, et tristique elit varius vel. Mauris a ex justo.
|
||||
|
||||
Nam eros est, imperdiet non volutpat rutrum, pellentesque accumsan ligula. Duis sit amet turpis metus. Aenean in rhoncus metus, ac fringilla ex. Suspendisse condimentum egestas purus, ut pharetra odio vulputate vel. Duis tincidunt massa a placerat ultrices. Mauris ultricies nibh sit amet condimentum malesuada. Duis tincidunt id ipsum sed congue.
|
||||
|
||||
Praesent eu ex augue. Nullam in porta ligula. In tincidunt accumsan arcu, in pellentesque magna tristique in. Mauris eleifend libero ac nisl viverra faucibus. Nam sollicitudin dolor in commodo hendrerit. Cras at orci metus. Ut quis laoreet orci. Vivamus ultrices leo pellentesque tempor aliquet. Maecenas ut eros vitae purus placerat vestibulum. Etiam vitae gravida dolor, quis rhoncus diam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||
|
||||
Suspendisse fringilla lacinia sagittis. Integer tincidunt consectetur tristique. Morbi non orci convallis, congue sapien quis, vulputate nunc. Donec a libero vel magna elementum facilisis non quis mi. Mauris posuere tellus non ipsum ultrices elementum. Vivamus massa velit, facilisis quis placerat aliquet, aliquet nec leo. Praesent a maximus sem. Sed neque elit, feugiat vel quam non, molestie sagittis nunc. Etiam luctus nunc ac mauris scelerisque, nec rhoncus lacus convallis. Nunc pharetra, nunc ac pulvinar aliquam, ex ipsum euismod augue, nec porttitor lacus turpis vitae neque. Fusce bibendum odio id tortor faucibus pellentesque. Sed ac porta nibh, eu gravida erat.
|
||||
|
||||
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam quis ullamcorper felis. Nulla mattis sagittis ante ac tincidunt. Integer ac felis efficitur, viverra libero et, facilisis ligula. Suspendisse a metus a massa rhoncus posuere. Phasellus suscipit ligula ut lacus facilisis, ac pellentesque ex tempor. Quisque consectetur massa mi, ac molestie libero dictum quis. Proin porttitor ligula quis erat tincidunt venenatis. Proin congue nunc sed elit gravida, nec consectetur lectus sodales. Etiam tincidunt convallis ipsum at vestibulum. Quisque maximus enim et mauris porttitor, et molestie magna tristique. Morbi vitae metus elit. Maecenas sed volutpat turpis. Aliquam vitae dolor vestibulum, elementum purus eget, dapibus nibh. Nullam egestas dui ac rutrum semper.
|
||||
|
||||
Etiam hendrerit est metus, et condimentum metus aliquam ac. Pellentesque id neque id ipsum rhoncus vulputate. Aliquam erat nisl, posuere sit amet ligula ac, fermentum blandit felis. Vivamus fermentum mi risus, non lacinia purus viverra id. Aenean ac sapien consequat, finibus mauris nec, porta sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed quis consectetur ex, dignissim bibendum nulla. Phasellus ac libero at quam vehicula euismod non eu leo. Phasellus a sapien augue.
|
||||
|
||||
Maecenas ligula dui, bibendum vitae mauris et, auctor laoreet felis. Duis non libero a mi semper mattis. Quisque consequat luctus massa, quis tristique eros auctor feugiat. Maecenas sodales euismod neque vitae facilisis. Nullam laoreet imperdiet velit at pellentesque. Etiam massa odio, facilisis a consequat vitae, placerat vel magna. Nunc sagittis eros nec urna fringilla, pulvinar vestibulum nibh scelerisque. Sed magna metus, cursus eu consequat et, pharetra a est. Suspendisse elementum neque a dui malesuada lacinia. Donec sed ipsum volutpat, cursus urna id, ullamcorper arcu. Maecenas laoreet nisl eget velit egestas sollicitudin. Etiam nisl turpis, mollis id dignissim vitae, tristique vehicula ante. Maecenas eget placerat est, at rutrum augue. Vivamus faucibus lacinia ullamcorper. Sed pulvinar urna sodales ante sodales, at gravida leo dictum.
|
||||
|
||||
Morbi maximus, quam a lobortis bibendum, enim felis varius elit, ac vehicula elit nisl ut lacus. Quisque ut arcu augue. Praesent id turpis quam. Sed sed arcu eros. Maecenas at cursus lorem, ac eleifend nisi. Fusce mattis felis at commodo pharetra. Praesent ac commodo ipsum. Quisque finibus et eros vitae tincidunt. In hac habitasse platea dictumst. Praesent purus ipsum, luctus lobortis ornare quis, auctor eget justo. Nam vel enim sollicitudin, faucibus tortor eu, sagittis eros. Ut nec consectetur erat. Donec ultricies malesuada ligula, a hendrerit sapien volutpat in. Maecenas sed enim vitae sapien pulvinar faucibus.
|
||||
|
||||
Proin semper nunc nibh, non consequat neque ullamcorper vel. Maecenas lobortis sagittis blandit. Aenean et arcu ultricies turpis malesuada malesuada. Ut quam ex, laoreet ut blandit cursus, feugiat vitae dolor. Etiam ex lacus, scelerisque vel erat vel, efficitur tincidunt magna. Morbi tristique lacinia dolor, in egestas magna ultrices vitae. Integer ultrices leo ac tempus venenatis. Praesent ac porta tortor. Vivamus ornare blandit tristique. Nulla rutrum finibus pellentesque. In non dui elementum, fermentum ipsum vel, varius magna. Pellentesque euismod tortor risus, ac pellentesque nisl faucibus eget.
|
||||
|
||||
Vivamus eu enim purus. Cras ultrices rutrum egestas. Sed mollis erat nibh, at posuere nisl luctus nec. Nunc vulputate, sapien id auctor molestie, nisi diam tristique ante, non convallis tellus nibh at orci. Morbi a posuere purus, in ullamcorper ligula. Etiam elementum sit amet dui imperdiet iaculis. Proin vitae tincidunt ipsum, sit amet placerat lectus. Curabitur commodo sapien quam, et accumsan lectus fringilla non. Nullam eget accumsan enim, ac pharetra mauris. Sed quis tristique velit, vitae commodo nisi. Duis turpis dui, maximus ut risus at, finibus consequat nunc. Maecenas sed est accumsan, aliquet diam in, facilisis risus. Curabitur vehicula rutrum auctor. Nam iaculis risus pulvinar maximus viverra. Nulla vel augue et ex sagittis blandit.
|
||||
|
||||
Ut sem nulla, porta ac ante ac, posuere laoreet eros. Donec sodales posuere justo a auctor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras mollis at orci hendrerit porta. Nullam sodales tortor tortor, non lacinia diam finibus id. Duis libero orci, suscipit ac odio et, dictum consequat ipsum. Pellentesque eu ligula sagittis, volutpat eros at, lacinia lorem. Cras euismod tellus in iaculis tempor. Quisque accumsan, magna a congue venenatis, ante ipsum aliquam lectus, at egestas enim nunc at justo. Quisque sem purus, viverra ut tristique ut, maximus id enim. Etiam quis placerat sem. In sollicitudin, lacus eu rutrum mollis, nulla eros luctus elit, vel dapibus urna purus nec urna. Phasellus egestas massa quam, ac molestie erat hendrerit a. Praesent ultrices neque ut turpis molestie auctor. Etiam molestie placerat purus, et euismod erat aliquam in. Morbi id suscipit justo.
|
||||
|
||||
Proin est ante, consequat at varius a, mattis quis felis. Sed accumsan nibh sit amet ipsum elementum posuere. Vestibulum bibendum id diam sit amet gravida. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi nec dolor vel ipsum dignissim hendrerit vel non ipsum. Praesent facilisis orci quis elit auctor lobortis. Phasellus cursus risus lectus, vel lobortis libero dapibus in. Quisque tristique tempus leo a pulvinar. Pellentesque a magna tincidunt, pellentesque massa nec, laoreet orci. Morbi congue ornare dolor quis commodo. Phasellus massa nisi, tincidunt at eros dictum, hendrerit lobortis urna. Maecenas porta, magna id mattis molestie, nibh tellus lobortis sem, eget tincidunt ipsum quam eu turpis.
|
||||
|
||||
Ut gravida orci risus, vel rutrum mauris vehicula id. Etiam bibendum, neque a placerat condimentum, ex orci imperdiet lectus, quis dapibus arcu lacus eget lectus. Sed consequat non mi sit amet venenatis. Fusce vestibulum erat libero, eget hendrerit risus vulputate sollicitudin. Integer sed eleifend felis. Donec commodo, sem eu mattis placerat, urna odio aliquam tellus, et laoreet justo tellus eget erat. Fusce sed suscipit tortor. Nam hendrerit nibh ac nunc auctor lacinia. Pellentesque placerat condimentum ipsum, eget semper tortor hendrerit vel. Nullam non urna eu lacus pellentesque congue ut id eros.
|
||||
|
||||
Nunc finibus leo in rhoncus tristique. Sed eu ipsum nec nisl egestas faucibus eget a felis. Pellentesque vitae nisi in nulla accumsan fermentum. Sed venenatis feugiat eleifend. Fusce porttitor varius placerat. Aliquam aliquet lacus sit amet mattis mollis. Sed vel nulla quis dolor suscipit vehicula ac viverra lorem. Duis viverra ipsum eget nulla ullamcorper fermentum. Mauris tincidunt arcu quis quam fringilla ornare. Donec et iaculis tortor. Nam ultricies libero vel ipsum aliquet efficitur. Morbi eget dolor aliquam, tempus sapien eget, viverra ante. Donec varius mollis ex, sed efficitur purus euismod interdum. Quisque vel sapien non neque tincidunt semper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
|
||||
|
||||
Suspendisse sit amet purus leo. Fusce lectus lorem, aliquam ac nulla eget, imperdiet ornare eros. Nullam sem augue, varius in nisi non, sollicitudin pellentesque ante. Etiam eu odio condimentum, tempor libero et, egestas arcu. Cras pellentesque eleifend aliquet. Pellentesque non blandit ligula. Ut congue viverra rhoncus. Phasellus mattis mi ac eros placerat, eu feugiat tellus ultrices. Aenean mollis laoreet libero eu imperdiet. Cras sed pulvinar mi, ac vehicula ligula. Vestibulum sit amet ex massa. In a egestas eros.
|
||||
|
||||
Mauris pretium ipsum risus, venenatis cursus ante imperdiet id. Praesent eu turpis nec risus feugiat maximus ullamcorper ac lectus. Integer placerat at mi vel dapibus. Vestibulum fermentum turpis sit amet turpis viverra, id aliquet diam suscipit. Nam nec ex sed ante ullamcorper pharetra quis sit amet risus. Sed ac faucibus velit, id feugiat nibh. Nullam eget ipsum ex. Vivamus tincidunt non nunc non faucibus. Quisque bibendum viverra facilisis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at nisi hendrerit quam suscipit egestas. Curabitur laoreet maximus ultricies. Duis ut tellus ac augue molestie dictum.
|
||||
|
||||
Suspendisse rhoncus iaculis erat, ut ullamcorper est tristique eget. Donec auctor nec risus at gravida. Vivamus volutpat vulputate tellus, vel ultricies eros suscipit eget. Ut pulvinar id mi eu tempus. Morbi malesuada augue in dui varius, nec blandit neque vehicula. Donec ornare nec nisl in mollis. Morbi enim nisi, rhoncus nec est id, dapibus tempus urna. Ut id elit a felis vestibulum consectetur. Duis lectus quam, pharetra sit amet diam sed, posuere vestibulum erat. Fusce vitae maximus massa. Nullam id metus tempus, iaculis risus eu, lobortis urna. Quisque in congue urna. Pellentesque placerat neque in augue dapibus, non varius ex malesuada. Curabitur ut eleifend libero. Fusce vitae ligula luctus, fermentum enim vitae, ultrices erat.
|
||||
|
||||
Sed viverra augue turpis, scelerisque egestas sapien mattis eu. Duis laoreet magna at ex pharetra dapibus. Praesent eget odio vel quam venenatis dictum. Nulla in sollicitudin dolor. Mauris lobortis nec eros vel rhoncus. Vestibulum porta viverra venenatis. Curabitur vel scelerisque quam, a egestas velit. Praesent volutpat tincidunt magna at laoreet.
|
||||
|
||||
Cras nec lorem odio. Pellentesque quis dui urna. Praesent at tellus ac lectus scelerisque placerat nec eu risus. Vestibulum sit amet mattis ligula. Vivamus sed nisi at leo elementum accumsan at sit amet arcu. Aenean mattis tellus nec leo gravida, eget hendrerit nisl faucibus. Mauris pellentesque luctus condimentum. Maecenas pretium sapien nunc, eget commodo dolor maximus id. Mauris vestibulum accumsan massa a dictum. Phasellus interdum quam ligula, ut maximus diam blandit aliquam. Nunc vitae ex eu erat condimentum consectetur. Maecenas interdum condimentum volutpat.
|
||||
|
||||
Donec et enim a libero rutrum laoreet. Praesent a condimentum sem, at tincidunt quam. In vel molestie risus. Sed urna dui, molestie vitae mollis laoreet, tempor quis lectus. Praesent vitae auctor est, et aliquet nunc. Curabitur vulputate blandit nulla, at gravida metus. Maecenas gravida dui eu iaculis tristique. Pellentesque posuere turpis nec auctor eleifend. Suspendisse bibendum diam eu tellus lobortis, et laoreet quam congue. In hac habitasse platea dictumst. Morbi dictum neque velit, eget rutrum eros ultrices sit amet.
|
||||
|
||||
Phasellus fermentum risus pharetra consectetur bibendum. Donec magna tortor, lacinia vitae nibh quis, aliquet pretium lorem. Donec turpis nisi, pretium eu enim volutpat, mattis malesuada augue. Nullam vel tellus iaculis, sollicitudin elit eget, tincidunt lacus. Fusce elementum elementum felis et iaculis. Suspendisse porta eros nec neque malesuada, in malesuada ante sollicitudin. Vivamus bibendum viverra molestie.
|
||||
|
||||
Integer feugiat, erat nec convallis aliquam, velit felis congue erat, molestie eleifend tellus erat in tellus. Nunc et justo purus. Donec egestas fermentum dui non feugiat. Quisque in sapien sagittis, gravida quam id, iaculis lectus. Cras sagittis rhoncus bibendum. Fusce quis metus in velit scelerisque tincidunt at non ipsum. Vivamus efficitur ante eu odio vulputate, vitae ultricies risus vehicula. Proin eget odio eu sem tincidunt feugiat vel id lorem.
|
||||
|
||||
Vestibulum sit amet nulla dignissim, euismod mi in, fermentum tortor. Donec ut aliquet libero, lacinia accumsan velit. Donec et nulla quam. Nullam laoreet odio nec nunc imperdiet, a congue eros venenatis. Quisque nec tellus sit amet neque interdum posuere. Duis quis mi gravida, tincidunt diam convallis, ultricies augue. Mauris consequat risus non porttitor congue. Ut in ligula consequat, viverra nunc a, eleifend enim. Duis ligula urna, imperdiet nec facilisis et, ornare eu ex. Proin lobortis lectus a lobortis porttitor. Nulla leo metus, egestas eu libero sed, pretium faucibus felis. Vestibulum non sem tortor. Nam cursus est leo. Vivamus luctus enim odio, non interdum sem dapibus a. Aenean accumsan consequat lectus in imperdiet.
|
||||
|
||||
Donec vehicula laoreet ipsum in posuere. Quisque vel quam imperdiet, sollicitudin nisi quis, suscipit velit. Morbi id sodales mauris. Curabitur tellus arcu, feugiat sed dui sit amet, sodales sagittis libero. Aenean vel suscipit metus, non placerat leo. Vestibulum quis nulla elit. Proin scelerisque non ante ut commodo. Interdum et malesuada fames ac ante ipsum primis in faucibus.
|
||||
|
||||
Sed non urna dolor. Suspendisse convallis mi porta pulvinar ultrices. Suspendisse quam ipsum, hendrerit non scelerisque molestie, interdum dictum nunc. Morbi condimentum condimentum turpis eu luctus. Pellentesque sagittis sollicitudin odio, sed ultricies felis ornare sit amet. Sed ultrices ex leo, a tincidunt nisl gravida sed. Nullam ornare accumsan porta. Praesent consectetur id est nec sollicitudin.
|
||||
|
||||
In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed sed ultrices nibh. Duis accumsan suscipit eros, a dictum odio tempus sit amet. Aenean imperdiet erat ac lacus finibus, scelerisque cursus massa imperdiet. Mauris molestie risus ut lacinia posuere. Nulla et sodales purus. Maecenas orci erat, placerat in tristique quis, placerat in mi.
|
||||
|
||||
Donec sollicitudin pellentesque odio in feugiat. Morbi eu dolor ut mauris congue sollicitudin. Aliquam erat volutpat. Nulla id varius dui. Curabitur finibus urna ante, consectetur interdum nisi volutpat a. Quisque quis mi tristique, consequat tellus eget, rutrum sapien. Vivamus vitae tellus vulputate, rutrum ex eu, vulputate sem. Suspendisse viverra lorem tellus, vel interdum orci gravida quis. Ut laoreet arcu at mi ullamcorper finibus. Duis porta sagittis vestibulum. Sed commodo nisl vitae urna sollicitudin, nec lacinia est sodales. Curabitur imperdiet sodales dui sed iaculis. Sed ac tellus maximus, eleifend quam sit amet, feugiat elit. Aenean viverra, dui at mattis varius, est odio vestibulum sapien, sit amet mollis libero massa nec velit. Etiam quis sodales justo.
|
||||
|
||||
Ut ultricies, sem eget sodales feugiat, nunc arcu congue elit, ac tempor justo massa nec purus. Maecenas enim nunc, pharetra eget dictum sit amet, tempus pellentesque velit. Suspendisse venenatis ligula in nulla mattis, et imperdiet ex tincidunt. Etiam vulputate, tellus et ultrices suscipit, enim velit laoreet massa, vitae congue odio enim ac urna. Morbi quam lorem, iaculis ac varius sagittis, euismod quis dolor. In ut dui eu purus feugiat consectetur. Vestibulum cursus velit quis lacus pellentesque iaculis. Cras in risus sed mauris porta rutrum. Nulla facilisi. Nullam eu bibendum est, non pellentesque lectus. Sed imperdiet feugiat lorem, quis convallis ante auctor in. Maecenas justo magna, scelerisque sit amet tellus eget, varius elementum risus. Duis placerat et quam sed varius.
|
||||
|
||||
Duis nec nibh vitae nibh dignissim mollis quis sed felis. Curabitur vitae quam placerat, venenatis purus ut, euismod nisl. Curabitur porttitor nibh eu pulvinar ullamcorper. Suspendisse posuere nec ipsum ac dapibus. Cras convallis consectetur urna. Phasellus a nibh in dolor lacinia posuere id eget augue. In eu pharetra lorem, vitae cursus lacus. Aliquam tincidunt nibh lectus. Aenean facilisis ultricies posuere. Sed ut placerat orci. Curabitur scelerisque gravida blandit. Maecenas placerat ligula eget suscipit fringilla. Mauris a tortor justo. Aliquam hendrerit semper mollis. Phasellus et tincidunt libero. Etiam vel quam libero.
|
||||
|
||||
Quisque aliquet tempor ex. Ut ante sem, vehicula at enim vel, gravida porta elit. Etiam vitae lacus a neque lobortis consectetur. Mauris sed interdum odio. Mauris elementum ex blandit tempor cursus. Integer in enim in leo viverra elementum. Fusce consectetur metus et sem rutrum, mattis euismod diam semper. Nunc sed ipsum vel urna consequat vehicula. Donec cursus pretium lorem, vestibulum pretium felis commodo sit amet. Nam blandit felis enim, eget gravida ex faucibus a. In nec neque massa. Etiam laoreet posuere ipsum. Praesent volutpat nunc dolor, ac vulputate magna facilisis non. Aenean congue turpis vel lectus sollicitudin tristique. Sed nec consequat purus, non vehicula quam. Etiam ultricies, est ac dictum tincidunt, turpis turpis pretium massa, a vulputate libero justo at nibh.
|
||||
|
||||
Aliquam erat volutpat. Cras ultrices augue ac sollicitudin lobortis. Curabitur et aliquet purus. Duis feugiat semper facilisis. Phasellus lobortis cursus velit, a sollicitudin tortor. Nam feugiat sapien non dapibus condimentum. Morbi at mi bibendum, commodo quam at, laoreet enim. Integer eu ultrices enim. Sed vestibulum eu urna ut dictum. Curabitur at mattis leo, sed cursus massa. Aliquam porttitor, felis quis fermentum porttitor, justo velit feugiat nulla, eget condimentum sem dui ut sapien.
|
||||
|
||||
In fringilla elit eu orci aliquam consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut eget fringilla tellus. Curabitur fermentum, mi et condimentum suscipit, elit neque bibendum dui, et hendrerit nunc metus id ipsum. Morbi placerat mi in hendrerit congue. Ut feugiat mauris eget scelerisque viverra. Vivamus sit amet erat dictum, sagittis lectus nec, pulvinar lorem. Sed non enim ac dui sollicitudin aliquet. Quisque ut lacus dolor. Fusce hendrerit malesuada euismod. Nulla faucibus vel mauris eu mollis. Mauris est diam, fringilla ac arcu feugiat, efficitur volutpat turpis. Aliquam venenatis cursus massa sed porttitor. Ut ac finibus enim, in tincidunt sapien.
|
||||
|
||||
Nunc faucibus semper turpis a lacinia. Phasellus gravida, libero vel pulvinar ornare, ex sem tincidunt lectus, sit amet convallis augue risus at tortor. Quisque sit amet ipsum id nulla posuere vestibulum. Pellentesque scelerisque mauris vel leo viverra sodales. Nulla viverra aliquam ex, ut rutrum enim fermentum venenatis. Aenean eget dapibus ex, eget faucibus metus. Vestibulum volutpat leo in diam semper, eget porta magna suscipit. Sed sit amet nulla blandit, aliquam dolor ac, gravida velit. Sed vel velit viverra, maximus est id, convallis justo.
|
||||
|
||||
Curabitur nulla ante, vulputate at libero vel, ullamcorper rutrum nibh. Pellentesque porttitor eu mauris id mattis. Duis vulputate augue elit, eget interdum justo pretium vel. Maecenas eu vulputate arcu, eget posuere purus. Suspendisse viverra a velit dictum eleifend. Suspendisse vitae dapibus diam. Donec vehicula justo in ante interdum, eu luctus diam placerat. Vivamus convallis ipsum eu orci suscipit, sed fermentum enim euismod. Maecenas faucibus elit vitae ex ornare tristique. Donec vestibulum nec elit sit amet porttitor. Aenean tempor lectus eget tortor hendrerit luctus. Nullam interdum vitae lectus vel feugiat. Cras in risus non magna consectetur lobortis. Sed faucibus enim quis gravida convallis.
|
||||
|
||||
Phasellus eget massa sit amet libero ultrices suscipit. Vivamus at risus sapien. Nam mollis nunc eget velit dictum maximus. Sed pellentesque, nunc ac fringilla lacinia, quam enim mattis ex, sed euismod tortor metus eu neque. Ut mattis nisl ut lectus rhoncus, sodales bibendum eros porta. Nulla porttitor enim nec diam sagittis, eget porta velit efficitur. Vestibulum ultricies eros neque. Phasellus rutrum suscipit enim, in interdum ante gravida vitae. Sed in sagittis diam, non commodo velit.
|
||||
|
||||
Morbi hendrerit odio orci, nec tincidunt odio rhoncus nec. Mauris neque velit, vehicula a lorem at, suscipit tristique dui. Sed finibus, nisl in mattis convallis, turpis neque sodales lacus, eu porta enim magna non diam. Nam commodo sodales risus consectetur malesuada. In eget elementum justo. Phasellus sit amet massa imperdiet, dapibus nunc sit amet, suscipit orci. Fusce condimentum laoreet feugiat. Ut ut viverra ante. Praesent bibendum interdum commodo. Nulla mollis nisi a est ornare volutpat. Sed at ligula eu nisi dapibus tempus. Proin cursus vestibulum justo, nec efficitur justo dignissim vel. Nunc quis maximus eros.
|
||||
|
||||
Cras viverra, diam a tristique mattis, libero felis vulputate tellus, a ornare felis leo a dui. Nulla ante nulla, finibus ut tellus ut, blandit pharetra nibh. Proin eleifend fermentum ex, eget auctor libero vulputate in. Nullam ultricies, mauris placerat pretium placerat, leo urna lobortis leo, vel placerat arcu libero sed mauris. Aliquam mauris ligula, ornare at urna at, eleifend gravida ligula. Vestibulum consectetur ut nulla non scelerisque. Donec ornare, sem nec elementum aliquam, urna nulla bibendum metus, eu euismod dui ligula ac est. Fusce laoreet erat eu ex lobortis, quis bibendum ligula interdum. Sed vel mi erat. Vivamus id lacus ac enim mattis tempor. Nunc ultricies pellentesque enim sed euismod. Fusce tincidunt convallis elit quis aliquam. Mauris nulla ipsum, sollicitudin quis diam ac, feugiat volutpat tellus. In nibh nibh, vulputate quis tincidunt quis, pulvinar eget magna. Pellentesque quis finibus dolor. Suspendisse viverra vitae lectus non eleifend.
|
||||
|
||||
Nunc ut orci et sapien maximus semper. Nulla dignissim sem urna, ac varius lectus ultricies id. Quisque aliquet pulvinar pretium. In ultricies molestie tellus vehicula porta. Nam enim lorem, aliquam eget ex et, hendrerit volutpat quam. Maecenas diam lacus, pellentesque eget tempus ac, pharetra eu elit. Donec vel eros a sem facilisis vulputate. Nullam ac nisi vulputate, laoreet nisl ac, eleifend sem. Nullam mi massa, rhoncus sed pharetra interdum, tincidunt eget nunc. Aliquam viverra mattis posuere. Mauris et dui sed nisl sollicitudin fermentum quis ut arcu. Nam placerat eget orci at tincidunt. Curabitur vel turpis metus. Phasellus nibh nulla, fermentum scelerisque sem vel, gravida tincidunt velit. Pellentesque vel quam tempor, finibus massa pellentesque, condimentum dui.
|
||||
|
||||
Donec at mattis neque. Etiam velit diam, consequat auctor mauris id, hendrerit faucibus metus. Maecenas ullamcorper eros a est sodales, ac consectetur odio scelerisque. Donec leo metus, imperdiet at pellentesque vel, feugiat id erat. Suspendisse at magna enim. Vestibulum placerat sodales lorem id sollicitudin. Aenean at euismod ligula, eget mollis diam. Phasellus pulvinar, orci nec pretium condimentum, est erat facilisis purus, quis feugiat augue elit aliquam nulla. Aenean vitae tortor id risus congue tincidunt. Sed dolor enim, mattis a ullamcorper id, volutpat ac leo.
|
||||
|
||||
Proin vehicula feugiat augue, id feugiat quam sodales quis. Donec et ultricies massa, a lacinia nulla. Duis aliquam augue ornare euismod viverra. Ut lectus risus, rutrum sit amet efficitur a, luctus nec nisl. Cras volutpat ullamcorper congue. Sed vitae odio metus. Phasellus aliquet euismod varius.
|
||||
|
||||
Nullam sem ex, malesuada ut magna ut, pretium mollis arcu. Nam porttitor eros cursus mi lacinia faucibus. Suspendisse aliquet eleifend iaculis. Maecenas sit amet viverra tortor. Nunc a mollis risus. Etiam tempus dolor in tortor malesuada mattis. Ut tincidunt venenatis est sit amet dignissim. Vestibulum massa enim, tristique sed scelerisque eu, fringilla ac velit. Donec efficitur quis urna sit amet malesuada. Vestibulum consequat ac ligula in dapibus. Maecenas massa massa, molestie non posuere nec, elementum ut magna. In nisi erat, mollis non venenatis eu, faucibus in justo. Morbi gravida non ex non egestas. Pellentesque finibus laoreet diam, eu commodo augue congue vitae.
|
||||
|
||||
Aenean sem mi, ullamcorper dapibus lobortis vitae, interdum tincidunt tortor. Vivamus eget vulputate libero. Ut bibendum posuere lectus, vel tincidunt tortor aliquet at. Phasellus malesuada orci et bibendum accumsan. Aliquam quis libero vel leo mollis porta. Sed sagittis leo ac lacus dictum, ac malesuada elit finibus. Suspendisse pharetra luctus commodo. Vivamus ultricies a odio non interdum. Vivamus scelerisque tincidunt turpis quis tempor. Pellentesque tortor ligula, varius non nunc eu, blandit sollicitudin neque. Nunc imperdiet, diam et tristique luctus, ipsum ex condimentum nunc, sit amet aliquam justo velit sed libero. Duis vel suscipit ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed tincidunt neque vel massa ultricies, id dictum leo consequat. Curabitur lobortis ultricies tellus, eget mattis nisl aliquam sit amet.
|
||||
|
||||
Proin at suscipit justo. Vivamus ut vestibulum nisl. Pellentesque enim odio, pharetra non magna sed, efficitur auctor magna. Praesent tincidunt ante quis ante hendrerit viverra. Pellentesque vel ipsum id magna vulputate efficitur. Sed nec neque accumsan, pulvinar sapien quis, euismod mauris. Donec condimentum laoreet sapien quis gravida. Quisque sed mattis purus. Vestibulum placerat vel neque maximus scelerisque.
|
||||
|
||||
Vestibulum mattis quam quis efficitur elementum. Duis dictum dolor ac scelerisque commodo. Fusce sollicitudin nisi sit amet dictum placerat. Suspendisse euismod pharetra eleifend. In eros nisl, porttitor sed mauris at, consectetur aliquet mauris. Donec euismod viverra neque sed fermentum. Phasellus libero magna, accumsan ut ultricies vitae, dignissim eget metus. Donec tellus turpis, interdum eget maximus nec, hendrerit eget massa. Curabitur auctor ligula in iaculis auctor. In ultrices quam suscipit cursus finibus. Aenean id mi at dolor interdum iaculis vitae ut lorem. Nullam sed nibh fringilla, lacinia odio nec, placerat erat. In dui libero, viverra ac viverra ac, pellentesque sit amet turpis.
|
||||
|
||||
Nulla in enim ex. Sed feugiat est et consectetur venenatis. Cras varius facilisis dui vel convallis. Vestibulum et elit eget tellus feugiat pellentesque. In ut ante eu purus aliquet posuere. Nulla nec ornare sem, sed luctus lorem. Nam varius iaculis odio, eget faucibus nisl ullamcorper in. Sed eget cursus felis, nec efficitur nisi.
|
||||
|
||||
Vivamus commodo et sem quis pulvinar. Pellentesque libero ante, venenatis vitae ligula sit amet, ornare sollicitudin nulla. Mauris eget tellus hendrerit, pulvinar metus quis, tempor nisi. Proin magna ex, laoreet sed tortor quis, varius fermentum enim. Integer eu dolor dictum, vulputate tortor et, aliquet ligula. Vestibulum vitae justo id mauris luctus sollicitudin. Suspendisse eget auctor neque, sodales egestas lorem. Vestibulum lacinia egestas metus vitae euismod. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus ex tellus, volutpat nec pulvinar sit amet, condimentum vitae dui. Curabitur vel felis sodales, lacinia nunc iaculis, ullamcorper augue. Pellentesque consequat dolor quis eros efficitur malesuada. Nulla ut malesuada lectus.
|
||||
|
||||
Morbi et tristique ante. Aliquam erat volutpat. Vivamus vitae dui nec turpis pellentesque fermentum. Quisque eget velit massa. Pellentesque tristique aliquam nisl, eu sollicitudin justo venenatis sed. Duis eleifend sem eros, ut aliquam libero porttitor id. Sed non nunc consequat, rhoncus diam eu, commodo erat. Praesent fermentum in lectus id blandit. Donec quis ipsum at justo volutpat finibus. Nulla blandit justo nulla, at mollis lacus consequat eget. Aenean sollicitudin quis eros ut ullamcorper.
|
||||
|
||||
Pellentesque venenatis nulla ut mi aliquet feugiat. Cras semper vel magna nec pharetra. Integer mattis felis et sapien commodo imperdiet. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis quis luctus felis. Vestibulum justo nibh, aliquam non lectus vitae, molestie placerat justo. Donec lorem nibh, gravida sit amet hendrerit ac, maximus id ipsum. Nunc ac libero sodales risus eleifend sagittis. Phasellus est massa, lobortis elementum ex sed, scelerisque consectetur neque. Nunc faucibus neque id lorem malesuada, eget convallis ex mattis.
|
||||
|
||||
Sed turpis tortor, fermentum non turpis id, posuere varius nibh. Donec iaculis lorem dui. Etiam eros ante, sodales eget venenatis at, consectetur eget risus. Curabitur non aliquam ante, a pretium justo. Maecenas tempor nisl tortor, vitae dictum nisi ultrices eu. Duis eget dui ultrices, porttitor lacus sed, lobortis purus. Quisque mattis elit nec neque sagittis, sed commodo leo blandit. Mauris sodales interdum eleifend. Vestibulum condimentum consectetur augue, id luctus diam convallis et.
|
||||
|
||||
Nunc suscipit risus in justo accumsan, a placerat magna tincidunt. Proin a nisl ipsum. Sed libero dui, tristique in augue quis, auctor tristique risus. Sed porttitor ex augue, eu porta augue molestie a. Duis rhoncus purus libero, eu tempus turpis condimentum at. Sed mollis nisi id lectus placerat tincidunt. Maecenas non scelerisque elit, quis rutrum orci. Donec in tellus pharetra urna ornare lobortis. Phasellus id risus at nisi varius rutrum eu ut turpis.
|
||||
|
||||
Duis dictum justo quis nisl porta, eget tincidunt magna suscipit. Sed velit massa, ullamcorper eu sodales ac, pretium a massa. Duis et rutrum tortor. Nulla accumsan hendrerit sapien, cursus volutpat eros egestas eget. Donec sollicitudin at ante quis sollicitudin. Aenean blandit feugiat diam, id feugiat eros faucibus eget. Donec viverra dolor vel justo scelerisque dignissim. Nulla semper sem nunc, rhoncus semper tellus ultricies sed. Duis in ornare diam. Donec vehicula feugiat varius. Maecenas ut suscipit est. Vivamus sem sem, finibus at dolor sit amet, euismod dapibus ligula. Vestibulum fringilla odio dapibus, congue massa eget, congue sem. Donec feugiat magna eget tortor lacinia scelerisque non et ipsum.
|
||||
|
||||
Suspendisse potenti. Nunc convallis sollicitudin ex eget venenatis. Sed iaculis nibh ex, vel ornare ligula congue dignissim. Quisque sollicitudin dolor ac dui vestibulum, sit amet molestie nisi aliquet. Donec at risus felis. Aenean sollicitudin metus a feugiat porta. Aenean a tortor ut dolor cursus sagittis. Vivamus consectetur porttitor nunc in facilisis. Proin sit amet mi vel lectus consectetur ultrices.
|
||||
|
||||
Sed cursus lectus vitae nunc tristique, nec commodo turpis dapibus. Pellentesque luctus ex id facilisis ornare. Morbi quis placerat dolor. Donec in lectus in arcu mattis porttitor ac sit amet metus. Cras congue mauris non risus sodales, vitae feugiat ipsum bibendum. Nulla venenatis urna sed libero elementum, a cursus lorem commodo. Mauris faucibus lobortis eros nec commodo.
|
||||
|
||||
Nullam suscipit ligula ullamcorper lorem commodo blandit. Nulla porta nibh quis pulvinar placerat. Vivamus eu arcu justo. Vestibulum imperdiet est ut fermentum porttitor. Pellentesque consectetur libero in sapien efficitur scelerisque. Curabitur ac erat sit amet odio aliquet dignissim. Pellentesque mi sem, rhoncus et luctus at, porttitor rutrum lectus. Vestibulum sollicitudin sollicitudin suscipit. Aenean efficitur dolor non ultrices imperdiet. Donec vel sem ex.
|
||||
|
||||
Sed convallis mauris aliquam rutrum cursus. Ut tempor porttitor sodales. Etiam eu risus ac augue gravida egestas et eu dolor. Proin id magna ex. Suspendisse quis lectus quis lorem ultricies tempus. Donec porttitor velit vitae tincidunt faucibus. Aliquam vitae semper nisi. Morbi ultrices, leo non pretium dapibus, dui libero pellentesque ex, vel placerat enim ante vitae dui. Nunc varius, sem sit amet sagittis lobortis, lectus odio scelerisque mauris, ut vestibulum orci magna quis neque. Sed id congue justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris congue nisi est, malesuada mollis elit tincidunt sed. Curabitur sed ex sit amet felis tristique elementum vitae vel nibh.
|
||||
|
||||
Etiam mollis pretium lobortis. Mauris augue lacus, efficitur at lacus sed, mollis tincidunt lectus. Aliquam erat volutpat. Donec at euismod elit, et mattis felis. Sed id lobortis urna. Morbi imperdiet vestibulum leo, sed maximus leo blandit eu. Aliquam semper lorem neque, nec euismod turpis mattis mollis. Quisque lobortis urna ultrices odio pretium, ac venenatis orci faucibus. Suspendisse bibendum odio ligula, sed lobortis massa pharetra nec. Donec turpis justo, iaculis at dictum ac, finibus eu libero. Maecenas quis porttitor mi, sit amet aliquet neque.
|
||||
|
||||
Vivamus auctor vulputate ante, at egestas lorem. Donec eu risus in nulla mollis ultricies at et urna. Duis accumsan porta egestas. Ut vel euismod augue. Fusce convallis nulla ante, nec fringilla velit aliquet at. Nam malesuada dapibus ligula, a aliquam nibh scelerisque ac. Praesent malesuada neque et pellentesque interdum. Curabitur volutpat at turpis vitae tristique. Vivamus porttitor semper congue. Quisque suscipit lacus mi, rhoncus ultrices tortor auctor quis. Maecenas neque neque, molestie ac facilisis eget, luctus ac lorem. In ut odio ut lacus suscipit pulvinar vitae sed elit. Nulla imperdiet, sem quis euismod sagittis, dui erat luctus dolor, faucibus faucibus erat sem eget nunc. Nam accumsan placerat malesuada. Maecenas convallis finibus pulvinar.
|
||||
|
||||
Cras at placerat tortor. Morbi facilisis auctor felis sit amet molestie. Donec sodales sed lorem vitae suscipit. Etiam fermentum pharetra ipsum, nec luctus orci gravida eu. Pellentesque gravida, est non condimentum tempus, mauris ligula molestie est, in congue dolor nisl vel sapien. Duis congue tempor augue, id rutrum eros porta dapibus. Etiam rutrum eget est eget vestibulum. Aenean mollis arcu vel consequat varius. Praesent at condimentum felis. Duis nec interdum nisl. Donec commodo lorem sed sapien scelerisque malesuada non eu urna. In blandit non ipsum at porta. Nam lobortis leo vitae dui auctor, non feugiat quam bibendum. Donec auctor lectus sagittis laoreet maximus. Maecenas rhoncus laoreet porttitor. Vestibulum porttitor augue ut lectus hendrerit, eget posuere mi gravida.
|
||||
|
||||
Sed mattis ex in erat pulvinar, eu imperdiet magna dapibus. Etiam nisi nibh, tempus non tellus sit amet, mattis tempor odio. Quisque nec lorem feugiat, lobortis odio et, commodo nunc. Maecenas semper purus nisi, nec vehicula nibh eleifend vitae. Nulla fermentum a lectus at maximus. Phasellus finibus metus non euismod ultrices. Etiam a pulvinar ante. Quisque convallis nec metus sit amet facilisis. Praesent laoreet massa et sollicitudin laoreet. Vestibulum in mauris aliquet, convallis mi ut, elementum purus. Nulla purus nulla, sodales at hendrerit quis, tempus sed lectus.
|
||||
|
||||
Nam ut laoreet neque, ut maximus nibh. Maecenas quis justo pellentesque, sollicitudin elit at, venenatis velit. Aenean nunc velit, vehicula scelerisque odio at, consectetur laoreet purus. Duis dui purus, malesuada quis ipsum sit amet, tempor interdum libero. Curabitur porta scelerisque sapien, vitae cursus diam condimentum eu. Phasellus sed orci quam. Nullam vitae dui quis purus tincidunt vestibulum. Curabitur quis nulla porta, cursus arcu non, auctor enim. Etiam sollicitudin ex id sem vehicula mollis. Morbi viverra laoreet tincidunt. Praesent ut semper dui. Nam sit amet pretium neque. Mauris vitae luctus diam, in lacinia purus. Maecenas ut placerat justo, ut porta felis. Integer eu mauris ante.
|
||||
|
||||
Aenean porttitor tellus diam, tempor consequat metus efficitur id. Suspendisse ut felis at erat tempor dictum at nec sapien. Sed vestibulum interdum felis, ac mattis mauris porta in. Nunc et condimentum massa. Sed cursus dictum justo et luctus. Integer convallis enim nisl, a rutrum lectus ultricies in. Donec dapibus lacus at nulla dapibus, id sollicitudin velit hendrerit. Fusce a magna at orci mollis rutrum ac a dolor. Aliquam erat volutpat. Morbi varius porta nunc, sit amet sodales ex hendrerit commodo. Donec tincidunt tortor sapien, vitae egestas sapien vehicula eget.
|
||||
|
||||
Suspendisse potenti. Donec pulvinar felis nec leo malesuada interdum. Integer posuere placerat maximus. Donec nibh ipsum, tincidunt vitae luctus vitae, bibendum at leo. Sed cursus nisl ut ex faucibus aliquet sed nec eros. Curabitur molestie posuere felis. Integer faucibus velit eget consequat iaculis. Mauris sed vulputate odio. Phasellus maximus, elit a pharetra egestas, lorem magna semper tellus, vestibulum semper diam felis at sapien. Suspendisse facilisis, nisl sit amet euismod vehicula, libero nulla vehicula dolor, quis fermentum nibh elit sit amet diam.
|
||||
|
||||
Morbi lorem enim, euismod eu varius ut, scelerisque quis odio. Nam tempus vitae eros id molestie. Nunc pretium in nulla eget accumsan. Quisque mattis est ut semper aliquet. Maecenas eget diam elementum, fermentum ipsum a, euismod sapien. Duis quam ligula, cursus et velit nec, ullamcorper tincidunt magna. Donec vulputate nisl est, et ullamcorper urna tempor sit amet.
|
||||
|
||||
Proin lacinia dui non turpis congue pretium. Morbi posuere metus vel purus imperdiet interdum. Morbi venenatis vel eros non ultricies. Nulla vel semper elit. Ut quis purus tincidunt, auctor justo ut, faucibus turpis. Proin quis mattis erat, at faucibus ligula. Mauris in mauris enim. Donec facilisis enim at est feugiat hendrerit. Nam vel nisi lorem. Fusce ultricies convallis diam, in feugiat tortor luctus quis. Donec tempor, leo vitae volutpat aliquam, magna elit feugiat leo, quis placerat sapien felis eget arcu. Donec ornare fermentum eleifend. Integer a est orci.
|
||||
|
||||
Proin rhoncus egestas leo. Nulla ultricies porta elit quis ornare. Nunc fermentum interdum vehicula. In in ligula lorem. Donec nec arcu sit amet orci lobortis iaculis. Mauris at mollis erat, sit amet mollis tortor. Mauris laoreet justo ullamcorper porttitor auctor. Aenean sit amet aliquam lectus, id fermentum eros. Praesent urna sem, vehicula ac fermentum id, dapibus ut purus. Vestibulum vitae tempus nunc. Donec at nunc ornare metus volutpat porta at eget magna. Donec varius aliquet metus, eu lobortis risus aliquam sed. Ut dapibus fermentum velit, ac tincidunt libero faucibus at.
|
||||
|
||||
In in purus auctor, feugiat massa quis, facilisis nisi. Donec dolor purus, gravida eget dolor ac, porttitor imperdiet urna. Donec faucibus placerat erat, a sagittis ante finibus ac. Sed venenatis dignissim elit, in iaculis felis posuere faucibus. Praesent sed viverra dolor. Mauris sed nulla consectetur nunc laoreet molestie in ut metus. Proin ac ex sit amet magna vulputate hendrerit ac condimentum urna. Proin ligula metus, gravida et sollicitudin facilisis, iaculis ut odio. Cras tincidunt urna et augue varius, ut facilisis urna consequat. Aenean vehicula finibus quam. Ut iaculis eu diam ac mollis. Nam mi lorem, tristique eget varius at, sodales at urna.
|
||||
|
||||
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin vitae dictum erat, et auctor ipsum. Nullam nunc nunc, sollicitudin quis magna a, vestibulum fermentum mauris. Praesent at erat dolor. Proin laoreet tristique nulla vel efficitur. Nam sed ultrices nibh, id rutrum nunc. Curabitur eleifend a erat sit amet sollicitudin. Nullam metus quam, laoreet vitae dapibus id, placerat sed leo. Aliquam erat volutpat. Donec turpis nisl, cursus eu ex sit amet, lacinia pellentesque nisl. Sed id ipsum massa. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec interdum scelerisque lorem eu mattis.
|
||||
|
||||
Vivamus ac tristique massa, nec facilisis nisl. Nam ipsum neque, tincidunt vel urna in, cursus imperdiet enim. Nam pellentesque egestas tempus. Morbi facilisis imperdiet libero vitae fringilla. Nam lacinia ligula at sapien facilisis malesuada. Nullam accumsan pulvinar sem, et cursus libero porta sit amet. Curabitur vulputate erat elit, ut pulvinar erat maximus vel.
|
||||
|
||||
Cras aliquet metus ut purus sagittis, vel venenatis ante consectetur. Pellentesque nulla lacus, viverra viverra mattis non, placerat vitae nibh. Donec enim turpis, accumsan sit amet tincidunt eu, imperdiet non metus. Morbi ipsum eros, tincidunt vel est ac, tristique porttitor nibh. Praesent ut ullamcorper mauris. Sed laoreet sit amet diam congue venenatis. Integer porta purus nec orci sagittis posuere.
|
||||
|
||||
Donec vehicula mauris eget lacus mollis venenatis et sed nibh. Nam sodales ligula ipsum, scelerisque lacinia ligula sagittis in. Nam sit amet ipsum at erat malesuada congue. Aenean ut sollicitudin sapien. Etiam at tempor odio. Mauris vitae purus ut magna suscipit consequat. Vivamus quis sapien neque. Nulla vulputate sem sit amet massa pellentesque, eleifend tristique ligula egestas. Suspendisse tincidunt gravida mi, in pulvinar lectus egestas non. Aenean imperdiet ex sit amet nunc sollicitudin porta. Integer justo odio, ultricies at interdum in, rhoncus vitae sem. Sed porttitor arcu quis purus aliquet hendrerit. Praesent tempor tortor at dolor dictum pulvinar. Nulla aliquet nunc non ligula scelerisque accumsan. Donec nulla justo, congue vitae massa in, faucibus hendrerit magna. Donec non egestas purus.
|
||||
|
||||
Vivamus iaculis, lacus efficitur faucibus porta, dui nulla facilisis ligula, ut sodales odio nunc id sapien. Cras viverra auctor ipsum, dapibus mattis neque dictum sed. Sed convallis fermentum molestie. Nulla facilisi turpis duis.
|
||||
@@ -1 +0,0 @@
|
||||
Small File
|
||||
@@ -1 +0,0 @@
|
||||
Small File with Ümlaut
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
This is some UTF 8 with BOM file.
|
||||
@@ -1,227 +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 { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/workbench/services/files/node/watcher/watcher';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
|
||||
return new FileChangesEvent(toFileChanges(changes));
|
||||
}
|
||||
|
||||
class TestFileWatcher {
|
||||
private readonly _onFileChanges: Emitter<FileChangesEvent>;
|
||||
|
||||
constructor() {
|
||||
this._onFileChanges = new Emitter<FileChangesEvent>();
|
||||
}
|
||||
|
||||
get onFileChanges(): Event<FileChangesEvent> {
|
||||
return this._onFileChanges.event;
|
||||
}
|
||||
|
||||
report(changes: IDiskFileChange[]): void {
|
||||
this.onRawFileEvents(changes);
|
||||
}
|
||||
|
||||
private onRawFileEvents(events: IDiskFileChange[]): void {
|
||||
|
||||
// Normalize
|
||||
let normalizedEvents = normalizeFileChanges(events);
|
||||
|
||||
// Emit through event emitter
|
||||
if (normalizedEvents.length > 0) {
|
||||
this._onFileChanges.fire(toFileChangesEvent(normalizedEvents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Path {
|
||||
UNIX,
|
||||
WINDOWS,
|
||||
UNC
|
||||
}
|
||||
|
||||
suite('Normalizer', () => {
|
||||
|
||||
test('simple add/update/delete', function (done: () => void) {
|
||||
const watch = new TestFileWatcher();
|
||||
|
||||
const added = uri.file('/users/data/src/added.txt');
|
||||
const updated = uri.file('/users/data/src/updated.txt');
|
||||
const deleted = uri.file('/users/data/src/deleted.txt');
|
||||
|
||||
const raw: IDiskFileChange[] = [
|
||||
{ path: added.fsPath, type: FileChangeType.ADDED },
|
||||
{ path: updated.fsPath, type: FileChangeType.UPDATED },
|
||||
{ path: deleted.fsPath, type: FileChangeType.DELETED },
|
||||
];
|
||||
|
||||
watch.onFileChanges(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 3);
|
||||
assert.ok(e.contains(added, FileChangeType.ADDED));
|
||||
assert.ok(e.contains(updated, FileChangeType.UPDATED));
|
||||
assert.ok(e.contains(deleted, FileChangeType.DELETED));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
watch.report(raw);
|
||||
});
|
||||
|
||||
let pathSpecs = platform.isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX];
|
||||
pathSpecs.forEach((p) => {
|
||||
test('delete only reported for top level folder (' + p + ')', function (done: () => void) {
|
||||
const watch = new TestFileWatcher();
|
||||
|
||||
const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1');
|
||||
const deletedFolderB = uri.file(p === Path.UNIX ? '/users/data/src/todelete2' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2');
|
||||
const deletedFolderBF1 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/file.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt');
|
||||
const deletedFolderBF2 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt');
|
||||
const deletedFolderBF3 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt');
|
||||
const deletedFileA = uri.file(p === Path.UNIX ? '/users/data/src/deleteme.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt');
|
||||
|
||||
const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
|
||||
const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
|
||||
|
||||
const raw: IDiskFileChange[] = [
|
||||
{ path: deletedFolderA.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: deletedFolderB.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: deletedFolderBF1.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: deletedFolderBF2.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: deletedFolderBF3.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: deletedFileA.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: addedFile.fsPath, type: FileChangeType.ADDED },
|
||||
{ path: updatedFile.fsPath, type: FileChangeType.UPDATED }
|
||||
];
|
||||
|
||||
watch.onFileChanges(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 5);
|
||||
|
||||
assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED));
|
||||
assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED));
|
||||
assert.ok(e.contains(deletedFileA, FileChangeType.DELETED));
|
||||
assert.ok(e.contains(addedFile, FileChangeType.ADDED));
|
||||
assert.ok(e.contains(updatedFile, FileChangeType.UPDATED));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
watch.report(raw);
|
||||
});
|
||||
});
|
||||
|
||||
test('event normalization: ignore CREATE followed by DELETE', function (done: () => void) {
|
||||
const watch = new TestFileWatcher();
|
||||
|
||||
const created = uri.file('/users/data/src/related');
|
||||
const deleted = uri.file('/users/data/src/related');
|
||||
const unrelated = uri.file('/users/data/src/unrelated');
|
||||
|
||||
const raw: IDiskFileChange[] = [
|
||||
{ path: created.fsPath, type: FileChangeType.ADDED },
|
||||
{ path: deleted.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
|
||||
];
|
||||
|
||||
watch.onFileChanges(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 1);
|
||||
|
||||
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
watch.report(raw);
|
||||
});
|
||||
|
||||
test('event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) {
|
||||
const watch = new TestFileWatcher();
|
||||
|
||||
const deleted = uri.file('/users/data/src/related');
|
||||
const created = uri.file('/users/data/src/related');
|
||||
const unrelated = uri.file('/users/data/src/unrelated');
|
||||
|
||||
const raw: IDiskFileChange[] = [
|
||||
{ path: deleted.fsPath, type: FileChangeType.DELETED },
|
||||
{ path: created.fsPath, type: FileChangeType.ADDED },
|
||||
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
|
||||
];
|
||||
|
||||
watch.onFileChanges(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 2);
|
||||
|
||||
assert.ok(e.contains(deleted, FileChangeType.UPDATED));
|
||||
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
watch.report(raw);
|
||||
});
|
||||
|
||||
test('event normalization: ignore UPDATE when CREATE received', function (done: () => void) {
|
||||
const watch = new TestFileWatcher();
|
||||
|
||||
const created = uri.file('/users/data/src/related');
|
||||
const updated = uri.file('/users/data/src/related');
|
||||
const unrelated = uri.file('/users/data/src/unrelated');
|
||||
|
||||
const raw: IDiskFileChange[] = [
|
||||
{ path: created.fsPath, type: FileChangeType.ADDED },
|
||||
{ path: updated.fsPath, type: FileChangeType.UPDATED },
|
||||
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
|
||||
];
|
||||
|
||||
watch.onFileChanges(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 2);
|
||||
|
||||
assert.ok(e.contains(created, FileChangeType.ADDED));
|
||||
assert.ok(!e.contains(created, FileChangeType.UPDATED));
|
||||
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
watch.report(raw);
|
||||
});
|
||||
|
||||
test('event normalization: apply DELETE', function (done: () => void) {
|
||||
const watch = new TestFileWatcher();
|
||||
|
||||
const updated = uri.file('/users/data/src/related');
|
||||
const updated2 = uri.file('/users/data/src/related');
|
||||
const deleted = uri.file('/users/data/src/related');
|
||||
const unrelated = uri.file('/users/data/src/unrelated');
|
||||
|
||||
const raw: IDiskFileChange[] = [
|
||||
{ path: updated.fsPath, type: FileChangeType.UPDATED },
|
||||
{ path: updated2.fsPath, type: FileChangeType.UPDATED },
|
||||
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
|
||||
{ path: updated.fsPath, type: FileChangeType.DELETED }
|
||||
];
|
||||
|
||||
watch.onFileChanges(e => {
|
||||
assert.ok(e);
|
||||
assert.equal(e.changes.length, 2);
|
||||
|
||||
assert.ok(e.contains(deleted, FileChangeType.DELETED));
|
||||
assert.ok(!e.contains(updated, FileChangeType.UPDATED));
|
||||
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
watch.report(raw);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +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 { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IHeapService = createDecorator<IHeapService>('heapService');
|
||||
|
||||
export interface ObjectIdentifier {
|
||||
$ident?: number;
|
||||
}
|
||||
|
||||
export interface IHeapService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly onGarbageCollection: Event<number[]>;
|
||||
|
||||
/**
|
||||
* Track gc-collection for the given object
|
||||
*/
|
||||
trackObject(obj: ObjectIdentifier | undefined): void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class NullHeapService implements IHeapService {
|
||||
_serviceBrand: any;
|
||||
onGarbageCollection = Event.None;
|
||||
trackObject() { }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user